Skip to content

Go SDK

Install the official Go SDK and use chat, streaming, web search, agents and semantic search on HiNow.

Updated on Aug 09, 2026

The official Go SDK for the HiNow API. It uses the standard library only — no external dependency lands in your go.sum because of it.

The API speaks the OpenAI protocol, and the SDK follows the same shape. If you have integrated with OpenAI before, the design of the calls is the one you already know.

Installation

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

The path carries /v2 and ends in /hinow_ai

The /v2 is part of the module path, not a mistake: Go requires the suffix from version 2 onwards, and go get rejects the tag without it. The package still lives in hinow_ai, so importing the module alone does not compile. The alias keeps the rest of the code shorter:

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

Keep the key in HINOW_API_KEY and pass an empty string to NewClient. Passing the key directly also works, but avoid leaving the value in the code.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

The first call

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)
}

The hinow/ prefix is part of the model name

Sending himax instead of hinow/himax returns 404 model_not_found, and the message does not make it clear that the prefix is missing. This holds for all of them: hinow/himax, hinow/hinova, hinow/higenesis.

Read the response through Message.Text()

Content is an interface{}, because a message can carry text or a list of parts. Text() handles both cases; a type assertion to string only works sometimes.

Optional fields are pointers

That way "not set" and "zero" are not confused: Temperature: 0 is a legitimate request for a deterministic response, and without a pointer the SDK would have no way to tell it apart from leaving the field blank. hinow.Int and hinow.Float make it shorter to write.

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"},
})

Showing the response as it arrives

CreateStream returns two channels: one for chunks and one for errors. On a conversation screen this changes the perception of speed more than switching models does.

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()
}

Read the error channel after the loop

If the connection drops midway, the loop over the chunks simply ends — with no warning. The error stays in the second channel, and ignoring it turns a network failure into a response cut in half.

Responds right away, with no job to track, at US$ 0.005 per call. The nine types are constants, so the compiler will not let you get the name wrong.

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)
}
TypeWhat is populated in each result
SearchTypeSearch, Scholar, PatentsPosition, Title, URL, Snippet
SearchTypeNewsthe above plus Source, Date, ImageURL
SearchTypeImagesLink, ImageURL, ThumbnailURL, Width, Height
SearchTypeVideosChannel, Duration, Date, ThumbnailURL
SearchTypePlacesAddress, Category, Phone, Website, Rating, coordinates
SearchTypeShoppingPrice, Delivery, Rating, Source
SearchTypeAutocompleteSuggestions; Results stays empty

Contacts from a website

It crawls one or more sites looking for e-mails, phone numbers and social profiles, and every finding comes with the exact page where it appeared. Because the crawl takes time, this one runs as a 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)
		}
	}
}

Repeating an identical run is not charged again

The job comes back with Cached true when the result came from the cache. If you would rather drive the loop yourself, use WebsiteContacts and track it with Tools.Jobs.Retrieve(ctx, job.JobID) — note that the field is job_id, not id.

Validate whatever comes back before using it

The crawl returns what it found in the page HTML, without judging it. Truncated or duplicated addresses do show up — treat the list as raw material and validate it before writing to your database.

Agents

Assistants, threads and runs in the same shape as the OpenAI API. You declare the functions and the model decides when to call them: the run stops, you execute and return the result.

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 is not an error

It is the run handing control back to you so you can execute a function. That is why Poll returns in this state instead of spinning: read RequiredAction, call SubmitToolOutputs and resume tracking.

Upload files, group them into a knowledge base and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, with no error, and that is what Poll is for.

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)
	}
}

The knowledge base filter is called RagID

The API ignores vector_store_id on this endpoint: the search simply sweeps every document in the account instead of the knowledge base you meant. It is the kind of detail that makes the search look like it is returning garbage.

Errors

Each failure has a type of its own, with an Is… function built on top of errors.As. You can branch by type instead of comparing message text.

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)
		}
	}
}
CheckWhen it is true
IsAuthenticationError401 — missing, invalid or revoked key
IsPermissionError403 — the key has no access to that resource
IsNotFoundError404 — model, file or assistant does not exist
IsInvalidRequestError400 or 422 — a field is missing or a value is out of range
IsRateLimitError429 — limit reached
IsInsufficientBalanceError402 — balance exhausted
IsServerError5xx — the failure is on the API side
IsConnectionErrorthe request never arrived; nothing was charged

Balance and catalog

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)
}

Everything the client exposes

FieldWhat for
Chat.Completions()Conversation, streaming, function calling, JSON mode
EmbeddingsVectors for semantic search
Images · Audio · VideoGeneration
ModelsCatalog, price and capabilities of each model
ToolsWeb search and website contacts
FilesDocument upload
VectorStoresSearchable knowledge bases
RagSemantic search over your documents
Beta.Assistants · Beta.ThreadsAgents that run on the server
GetBalanceAccount balance

Configuration

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
)

Coming from version 1.x

Three parts of the SDK returned something different from what the code said:

  • Chat packed Temperature, MaxTokens and TopP into a parameters object, with the numbers converted to text. The API accepts that shape and ignores it, so asking for MaxTokens: 10 returned the whole response. Everything now goes at the root level.
  • GetBalance read the response as a flat object, but the endpoint returns {"data": {...}}. Every call reported a zero balance.
  • Models.List looked for fields named inputName and categorys, which the API does not send, so every model came back with an empty ID. And Models.Retrieve escaped the whole id, turning hinow/himax into hinow%2Fhimax and into a 404.

On top of that, ResponseFormat became an object — use &hinow.ResponseFormat{Type: "json_object"} — and BalanceResponse is now an alias of Balance, so old code still compiles.

Choosing between HiMax, HiNova and HiGenesis

What each model does well, how much it costs and how to write the prompt for each one.

Was this page helpful?