Ir para o conteúdo

SDK Go

Instale o SDK oficial em Go e use chat, streaming, busca na web, agentes e busca semântica no HiNow.

Atualizado em 09 de ago. de 2026

O SDK oficial em Go para a API do HiNow. Só usa a biblioteca padrão — nenhuma dependência externa entra no seu go.sum por causa dele.

A API fala o protocolo da OpenAI, e o SDK segue o mesmo formato. Se você já integrou com a OpenAI, o desenho das chamadas é o que você conhece.

Instalação

terminalbash
go get github.com/hinow-ai/sdk-go/v2

O caminho tem /v2 e termina em /hinow_ai

O /v2 faz parte do caminho do módulo, não é engano: o Go exige o sufixo a partir da versão 2, e go get recusa a tag sem ele. O pacote ainda fica em hinow_ai, então importar só o módulo não compila. O apelido deixa o resto do código mais curto:

import hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"

Guarde a chave em HINOW_API_KEY e passe uma string vazia para NewClient. Passar a chave direto também funciona, mas evite deixar o valor no código.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

A primeira chamada

main.gogo
package main

import (
	"context"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()

	// An empty key makes the client read HINOW_API_KEY.
	client := hinow.NewClient("")

	answer, err := client.Chat.Completions().Create(ctx, &hinow.ChatCompletionRequest{
		Model: "hinow/himax",
		Messages: []hinow.Message{
			{Role: "system", Content: "You answer in English."},
			{Role: "user", Content: "What is an API? Answer in one paragraph."},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Text() gives you the string; Content holds the raw value.
	fmt.Println(answer.Choices[0].Message.Text())

	uso := answer.Usage
	fmt.Printf("\ninput: %d · output: %d\n", uso.PromptTokens, uso.CompletionTokens)
}

O prefixo hinow/ faz parte do nome do modelo

Mandar himax em vez de hinow/himax devolve 404 model_not_found, e a mensagem não deixa claro que faltou o prefixo. Vale para todos: hinow/himax, hinow/hinova, hinow/higenesis.

Leia a resposta por Message.Text()

Content é um interface{}, porque uma mensagem pode carregar texto ou uma lista de partes. Text() resolve os dois casos; um type assertion para string só funciona às vezes.

Campos opcionais são ponteiros

Assim "não informado" e "zero" não se confundem: Temperature: 0 é um pedido legítimo de resposta determinística, e sem ponteiro o SDK não teria como distinguir isso de deixar em branco. hinow.Int e hinow.Float encurtam a escrita.

opcoes.gogo
resposta, err := client.Chat.Completions().Create(ctx, &hinow.ChatCompletionRequest{
	Model:       "hinow/himax",
	Messages:    mensagens,
	MaxTokens:   hinow.Int(500),
	Temperature: hinow.Float(0.2),

	// Modo JSON: a resposta vem como um objeto válido, sem texto em volta.
	ResponseFormat: &hinow.ResponseFormat{Type: "json_object"},
})

Mostrando a resposta enquanto ela chega

CreateStream devolve dois canais: um de pedaços e um de erro. Numa tela de conversa isso muda a percepção de velocidade mais do que trocar de modelo.

streaming.gogo
package main

import (
	"context"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	chunks, errors := client.Chat.Completions().CreateStream(ctx, &hinow.ChatCompletionRequest{
		Model:    "hinow/hinova",
		Messages: []hinow.Message{{Role: "user", Content: "Write three lines about the sea at dawn."}},
	})

	for chunk := range chunks {
		// Note the Delta: each chunk carries the new fragment, not the whole answer.
		if len(chunk.Choices) > 0 {
			fmt.Print(chunk.Choices[0].Delta.Content)
		}
	}

	// Always read the error channel after the loop: a failure mid-stream
	// shows up here and nowhere else.
	if err := <-errors; err != nil {
		log.Fatal(err)
	}

	fmt.Println()
}

Leia o canal de erro depois do laço

Se a conexão cair no meio, o laço sobre os pedaços simplesmente termina — sem aviso. O erro fica no segundo canal, e ignorá-lo transforma uma falha de rede numa resposta cortada pela metade.

Busca na web

Responde na hora, sem job para acompanhar, a US$ 0,005 por chamada. Os nove tipos são constantes, então o compilador não deixa você errar o nome.

busca_web.gogo
package main

import (
	"context"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	// Answers on the spot: there is no job to follow.
	search, err := client.Tools.Search(ctx, &hinow.SearchRequest{
		Query:   "best beaches in northeast Brazil",
		Type:    hinow.SearchTypeSearch,
		Country: "br",
		Lang:    "pt-br",
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, item := range search.Results {
		fmt.Printf("%d. %s\n   %s\n", item.Position, item.Title, item.URL)
	}

	fmt.Printf("\n%d results · US$ %v\n", len(search.Results), search.Cost)
}
TipoO que vem preenchido em cada resultado
SearchTypeSearch, Scholar, PatentsPosition, Title, URL, Snippet
SearchTypeNewsos acima mais Source, Date, ImageURL
SearchTypeImagesLink, ImageURL, ThumbnailURL, Width, Height
SearchTypeVideosChannel, Duration, Date, ThumbnailURL
SearchTypePlacesAddress, Category, Phone, Website, Rating, coordenadas
SearchTypeShoppingPrice, Delivery, Rating, Source
SearchTypeAutocompleteSuggestions; Results fica vazio

Contatos de um site

Varre um ou mais sites atrás de e-mails, telefones e perfis sociais, e cada achado vem com a página exata onde apareceu. Como a varredura leva tempo, esta roda como job.

contatos_site.gogo
package main

import (
	"context"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	// The crawl takes a while, so it runs as a job. This method waits for you.
	job, err := client.Tools.WebsiteContactsAndWait(ctx, &hinow.WebsiteContactsRequest{
		Websites:        []string{"https://www.teclia.com"},
		MaxDepth:        2,
		MaxLinksPerPage: 10,
	}, nil)
	if err != nil {
		log.Fatal(err)
	}

	origem := ""
	if job.Cached {
		origem = " (from cache)"
	}
	fmt.Printf("status: %s · US$ %v%s\n\n", job.Status, job.Cost, origem)

	// Each item carries the type, the value and the exact page it was found on.
	for _, item := range job.Result.Items {
		switch item.Type {
		case "email", "phone":
			fmt.Printf("%-9s%s\n%-9sem %s\n", item.Type, item.Value, "", item.SourceURL)
		default:
			fmt.Printf("%-9s%s\n", item.Platform, item.URL)
		}
	}
}

Repetir uma execução idêntica não cobra de novo

O job volta com Cached verdadeiro quando o resultado veio do cache. Se quiser controlar o ciclo você mesmo, use WebsiteContacts e acompanhe com Tools.Jobs.Retrieve(ctx, job.JobID) — repare que o campo é job_id, não id.

Valide o que vier antes de usar

A varredura devolve o que encontrou no HTML da página, sem julgar. Endereços truncados ou repetidos aparecem — trate a lista como matéria-prima e valide antes de gravar no seu banco.

Agentes

Assistentes, threads e runs no mesmo formato da API da OpenAI. Você declara as funções, o modelo decide quando chamá-las: o run para, você executa e devolve o resultado.

agente.gogo
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

// Your real function: a map here, just for the example.
func getOrder(code string) map[string]string {
	orders := map[string]map[string]string{
		"A-1001": {"status": "delivered", "delivered": "2026-08-02"},
		"A-1002": {"status": "in transit", "estimated": "2026-08-12"},
	}

	if order, ok := orders[code]; ok {
		return order
	}
	return map[string]string{"error": "order not found"}
}

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	// 1. The assistant: model, instructions and the functions it may call.
	assistant, err := client.Beta.Assistants.Create(ctx, &hinow.AssistantRequest{
		Model:        "hinow/himax",
		Name:         "Support",
		Instructions: "You answer questions about orders. Check the tool before stating any status.",
		Tools: []hinow.Tool{
			hinow.NewTool("get_order", "Look up an order by its code.",
				map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"code": map[string]interface{}{
							"type":        "string",
							"description": "Order code, e.g. A-1001",
						},
					},
					"required": []string{"code"},
				}),
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Beta.Assistants.Delete(ctx, assistant.ID)

	// 2. The conversation and the question.
	thread, err := client.Beta.Threads.Create(ctx)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := client.Beta.Threads.Messages.Create(ctx, thread.ID, "Has order A-1002 arrived?", "user"); err != nil {
		log.Fatal(err)
	}

	// 3. Run it and wait.
	run, err := client.Beta.Threads.Runs.CreateAndPoll(ctx, thread.ID, assistant.ID)
	if err != nil {
		log.Fatal(err)
	}

	// 4. While the model asks for functions, run them and hand the result back.
	for run.Status == "requires_action" {
		var outputs []hinow.ToolOutput

		for _, call := range run.RequiredAction.SubmitToolOutputs.ToolCalls {
			var args struct {
				Code string `json:"code"`
			}
			if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil {
				log.Fatal(err)
			}
			fmt.Printf("-> the model called %s(%s)\n", call.Function.Name, args.Code)

			resultado, _ := json.Marshal(getOrder(args.Code))
			outputs = append(outputs, hinow.ToolOutput{ToolCallID: call.ID, Output: string(resultado)})
		}

		if _, err := client.Beta.Threads.Runs.SubmitToolOutputs(ctx, thread.ID, run.ID, outputs); err != nil {
			log.Fatal(err)
		}
		if run, err = client.Beta.Threads.Runs.Poll(ctx, thread.ID, run.ID); err != nil {
			log.Fatal(err)
		}
	}

	// 5. The final answer is the last message on the thread.
	messages, err := client.Beta.Threads.Messages.List(ctx, thread.ID, 1, "desc")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("\n%s\n", messages.Data[0].Text())
}

requires_action não é erro

É o run devolvendo o controle para você executar uma função. Por isso o Poll retorna nesse estado em vez de continuar girando: leia o RequiredAction, chame SubmitToolOutputs e volte a acompanhar.

Documentos e busca semântica

Suba arquivos, junte numa base e busque por significado. A indexação é assíncrona — buscar antes de terminar devolve zero resultado, sem erro, e é para isso que existe o Poll.

conhecimento.gogo
package main

import (
	"context"
	"fmt"
	"log"
	"strings"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	// 1. Upload the document.
	file, err := client.Files.CreateFromPath(ctx, "returns-policy.txt", "assistants")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("file: %s (%d bytes)\n", file.ID, file.Bytes)

	// 2. Create the store and attach the file to it.
	base, err := client.VectorStores.Create(ctx, &hinow.VectorStoreRequest{Name: "Support base"})
	if err != nil {
		log.Fatal(err)
	}
	if _, err := client.VectorStores.Files.Create(ctx, base.ID, file.ID); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("store: %s\n", base.ID)

	// 3. Indexing is asynchronous. Searching before it finishes returns nothing
	//    at all, with no error, so Poll waits for it.
	state, err := client.VectorStores.Files.Poll(ctx, base.ID, file.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("indexing: %s\n\n", state.Status)

	// 4. Search by meaning, not by exact word.
	hits, err := client.Rag.Search(ctx, &hinow.RagSearchRequest{
		Query: "How many days do I have to return an item?",
		RagID: base.ID,
		TopK:  3,
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, achado := range hits.Results {
		excerpt := strings.ReplaceAll(achado.Text, "\n", " ")
		if len(excerpt) > 110 {
			excerpt = excerpt[:110]
		}
		fmt.Printf("%.2f  %s\n      %s\n", achado.Score, achado.Source, excerpt)
	}
}

O filtro por base chama-se RagID

A API ignora vector_store_id nesse endpoint: a busca simplesmente varre todos os documentos da conta em vez da base que você queria. É o tipo de detalhe que faz parecer que a busca está devolvendo lixo.

Erros

Cada falha tem um tipo próprio, com uma função Is… construída sobre errors.As. Dá para decidir por tipo em vez de comparar texto de mensagem.

erros.gogo
package main

import (
	"context"
	"errors"
	"fmt"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	_, err := client.Chat.Completions().Create(ctx, &hinow.ChatCompletionRequest{
		Model:    "himax", // the hinow/ prefix is missing
		Messages: []hinow.Message{{Role: "user", Content: "Olá"}},
	})

	switch {
	case err == nil:
		fmt.Println("deu certo")
	case hinow.IsAuthenticationError(err):
		fmt.Println("Invalid or expired key. Check HINOW_API_KEY.")
	case hinow.IsNotFoundError(err):
		fmt.Println("Not found:", err)
	case hinow.IsInvalidRequestError(err):
		fmt.Println("Invalid request:", err)
	case hinow.IsRateLimitError(err):
		fmt.Println("Rate limited. The SDK already retried; wait a moment.")
	case hinow.IsConnectionError(err):
		// Nothing reached the API, so nothing was charged.
		fmt.Println("Sem answer da API:", err)
	default:
		// Safety net: any other error the API reported.
		var apiErr *hinow.APIError
		if errors.As(err, &apiErr) {
			fmt.Printf("Error %d: %s\n", apiErr.StatusCode, apiErr.Message)
		}
	}
}
VerificaçãoQuando é verdadeira
IsAuthenticationError401 — chave ausente, inválida ou revogada
IsPermissionError403 — a chave não tem acesso a esse recurso
IsNotFoundError404 — modelo, arquivo ou assistente inexistente
IsInvalidRequestError400 ou 422 — falta um campo ou um valor está fora da faixa
IsRateLimitError429 — limite atingido
IsInsufficientBalanceError402 — saldo esgotado
IsServerError5xx — a falha é do lado da API
IsConnectionErrora requisição não chegou; nada foi cobrado
modelos.gogo
package main

import (
	"context"
	"fmt"
	"log"

	hinow "github.com/hinow-ai/sdk-go/v2/hinow_ai"
)

func main() {
	ctx := context.Background()
	client := hinow.NewClient("")

	// Account credit, in US dollars.
	balance, err := client.GetBalance(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("balance: US$ %.2f\n\n", balance.Balance)

	// One specific model. The id is namespaced: hinow/himax, not himax.
	model, err := client.Models.Retrieve(ctx, "hinow/himax")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s (%s)\n", model.Name, model.ID)
	fmt.Printf("categories: %v\n", model.Category)
	fmt.Printf("price per million tokens: input US$ %v · output US$ %v\n",
		model.Cost.Input, model.Cost.Output)
}

Tudo que o cliente expõe

CampoPara quê
Chat.Completions()Conversa, streaming, chamada de funções, modo JSON
EmbeddingsVetores para busca semântica
Images · Audio · VideoGeração
ModelsCatálogo, preço e recursos de cada modelo
ToolsBusca na web e contatos de site
FilesUpload de documentos
VectorStoresBases de conhecimento pesquisáveis
RagBusca semântica nos seus documentos
Beta.Assistants · Beta.ThreadsAgentes executados no servidor
GetBalanceSaldo da conta

Configuração

cliente.gogo
client := hinow.NewClient(os.Getenv("HINOW_API_KEY"),
	hinow.WithBaseURL("https://api.hinow.ai"), // ou HINOW_BASE_URL
	hinow.WithTimeout(120*time.Second),
	hinow.WithMaxRetries(2),                   // repete 429 e 5xx
)

Vindo da versão 1.x

Três partes do SDK devolviam algo diferente do que o código dizia:

  • Chat empacotava Temperature, MaxTokens e TopP num objeto parameters, com os números convertidos em texto. A API aceita esse formato e ignora, então pedir MaxTokens: 10 devolvia a resposta inteira. Agora tudo vai no nível raiz.
  • GetBalance lia a resposta como um objeto plano, mas o endpoint devolve {"data": {...}}. Toda chamada informava saldo zero.
  • Models.List procurava campos chamados inputName e categorys, que a API não envia, então todo modelo voltava com ID vazio. E Models.Retrieve escapava o id inteiro, transformando hinow/himax em hinow%2Fhimax e num 404.

Além disso, ResponseFormat virou objeto — use &hinow.ResponseFormat{Type: "json_object"} — e BalanceResponse passou a ser um alias de Balance, então o código antigo continua compilando.

Escolhendo entre HiMax, HiNova e HiGenesis

O que cada modelo faz bem, quanto custa e como escrever o prompt para cada um.

Esta página foi útil?