Ir para o conteúdo

SDK C#

Instale o SDK oficial em C# e use chat, streaming, busca na web, agentes e busca semântica no HiNow a partir do .NET.

Atualizado em 09 de ago. de 2026

O SDK oficial em C# para a API do HiNow. Compila para .NET 8 e para .NET Standard 2.1, então roda também em projetos mais antigos e no Unity.

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
dotnet add package hinow-ai

Guarde a chave em HINOW_API_KEY e o SDK a encontra sozinho. Passar a chave no construtor também funciona, mas evite deixar o valor no código.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

A primeira chamada

Program.cscsharp
using Hinow;

// With no arguments, the client reads the key from HINOW_API_KEY.
using var client = new HinowClient();

var answer = await client.Chat.Completions.CreateAsync(new ChatCompletionRequest
{
    Model = "hinow/himax",
    Messages =
    {
        new Message("system", "You answer in English."),
        new Message("user", "What is an API? Answer in one paragraph."),
    },
});

// Message.Text gives you the text. Message.Content holds the raw value.
Console.WriteLine(answer.Choices[0].Message.Text);

var uso = answer.Usage!;
Console.WriteLine($"\ninput: {uso.PromptTokens} · output: {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

Message.Content guarda o valor bruto, que numa resposta desserializada é um JsonElement. Text entrega a string já pronta e junta as partes quando a resposta vem em pedaços.

Com injeção de dependência

Passe o seu próprio HttpClient e o SDK não o descarta — quem controla o ciclo de vida continua sendo a IHttpClientFactory.

Program.cscsharp
builder.Services.AddHttpClient<HinowClient>((http, _) =>
    new HinowClient(apiKey: null, baseUrl: null, timeout: null, httpClient: http));

Mostrando a resposta enquanto ela chega

CreateStreamAsync devolve um IAsyncEnumerable: você percorre com await foreach e cada volta traz um pedaço novo. Numa tela de conversa isso muda a percepção de velocidade mais do que trocar de modelo.

Streaming.cscsharp
using Hinow;

using var client = new HinowClient();

var stream = client.Chat.Completions.CreateStreamAsync(new ChatCompletionRequest
{
    Model = "hinow/hinova",
    Messages = { new Message("user", "Write three lines about the sea at dawn.") },
});

await foreach (var chunk in stream)
{
    // Note the Delta: each chunk carries the new fragment, not the whole answer.
    Console.Write(chunk.Choices.Count > 0 ? chunk.Choices[0].Delta.Content : "");
}

Console.WriteLine();

É Delta, não Message

Na resposta completa o texto vem em Message.Text. No streaming, cada pedaço traz apenas o trecho novo, em Delta.Content — juntar tudo é com você.

Busca na web

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

BuscaWeb.cscsharp
using Hinow;

using var client = new HinowClient();

// Answers on the spot: there is no job to follow.
var search = await client.Tools.SearchAsync(
    "best beaches in northeast Brazil",
    SearchType.Search,
    country: "br",
    lang: "pt-br");

foreach (var item in search.Results)
{
    Console.WriteLine($"{item.Position}. {item.Title}");
    Console.WriteLine($"   {item.Url}");
}

Console.WriteLine($"\n{search.Results.Count} results · US$ {search.Cost}");
TipoO que vem preenchido em cada resultado
SearchType.Search, Scholar, PatentsPosition, Title, Url, Snippet
SearchType.Newsos acima mais Source, Date, ImageUrl
SearchType.ImagesLink, ImageUrl, ThumbnailUrl, Width, Height
SearchType.VideosChannel, Duration, Date, ThumbnailUrl
SearchType.PlacesAddress, Category, Phone, Website, Rating, coordenadas
SearchType.ShoppingPrice, Delivery, Rating, Source
SearchType.AutocompleteSuggestions; 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.

ContatosSite.cscsharp
using Hinow;

using var client = new HinowClient();

// The crawl takes a while, so it runs as a job. This method waits for you.
var job = await client.Tools.WebsiteContactsAndWaitAsync(
    new[] { "https://www.teclia.com" },
    maxDepth: 2,
    maxLinksPerPage: 10);

Console.WriteLine($"status: {job.Status} · US$ {job.Cost}"
    + (job.Cached == true ? " (from cache)" : ""));
Console.WriteLine();

// Each item carries the type, the value and the exact page it was found on.
foreach (var item in job.Result!.Items)
{
    if (item.Type == "social")
    {
        Console.WriteLine($"{item.Platform,-10}{item.Url}");
    }
    else
    {
        Console.WriteLine($"{item.Type,-10}{item.Value}");
        Console.WriteLine($"{"",-10}em {item.SourceUrl}");
    }
}

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

O job volta com Cached = true quando o resultado veio do cache. Se quiser controlar o ciclo você mesmo, use WebsiteContactsAsync e acompanhe com Tools.Jobs.RetrieveAsync(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.cscsharp
using System.Text.Json;
using Hinow;

using var client = new HinowClient();

// Your real function: a dictionary here, just for the example.
var orders = new Dictionary<string, object>
{
    ["A-1001"] = new { status = "delivered", delivered = "2026-08-02" },
    ["A-1002"] = new { status = "in transit", estimated = "2026-08-12" },
};

// 1. The assistant: model, instructions and the functions it may call.
var assistant = await client.Beta.Assistants.CreateAsync(new AssistantRequest
{
    Model = "hinow/himax",
    Name = "Support",
    Instructions = "You answer questions about orders. Check the tool before stating any status.",
    Tools = new List<Tool>
    {
        Tool.CreateFunction(
            "get_order",
            "Look up an order by its code.",
            new Dictionary<string, object>
            {
                ["type"] = "object",
                ["properties"] = new Dictionary<string, object>
                {
                    ["code"] = new Dictionary<string, object>
                    {
                        ["type"] = "string",
                        ["description"] = "Order code, e.g. A-1001",
                    },
                },
                ["required"] = new[] { "code" },
            }),
    },
});

// 2. The conversation and the question.
var thread = await client.Beta.Threads.CreateAsync();
await client.Beta.Threads.Messages.CreateAsync(thread.Id, "Has order A-1002 arrived?");

// 3. Run it and wait.
var run = await client.Beta.Threads.Runs.CreateAndPollAsync(thread.Id, assistant.Id);

// 4. While the model asks for functions, run them and hand the result back.
while (run.Status == "requires_action")
{
    var outputs = new List<ToolOutput>();

    foreach (var call in run.RequiredAction!.SubmitToolOutputs!.ToolCalls)
    {
        var callArgs = JsonSerializer.Deserialize<Dictionary<string, string>>(call.Function.Arguments)!;
        var code = callArgs["code"];
        Console.WriteLine($"-> the model called {call.Function.Name}({code})");

        var order = orders.TryGetValue(code, out var achado)
            ? achado
            : new { error = "order not found" };

        outputs.Add(new ToolOutput(call.Id, JsonSerializer.Serialize(order)));
    }

    await client.Beta.Threads.Runs.SubmitToolOutputsAsync(thread.Id, run.Id, outputs);
    run = await client.Beta.Threads.Runs.PollAsync(thread.Id, run.Id);
}

// 5. The final answer is the last message on the thread.
var messages = await client.Beta.Threads.Messages.ListAsync(thread.Id, limit: 1, order: "desc");
Console.WriteLine($"\n{messages.Data[0].Text}");

await client.Beta.Assistants.DeleteAsync(assistant.Id);

requires_action não é erro

É o run devolvendo o controle para você executar uma função. Por isso o PollAsync retorna nesse estado em vez de continuar girando: leia o RequiredAction, chame SubmitToolOutputsAsync 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 PollAsync.

Conhecimento.cscsharp
using Hinow;

using var client = new HinowClient();

// 1. Upload the document.
var file = await client.Files.CreateAsync("returns-policy.txt");
Console.WriteLine($"file: {file.Id} ({file.Bytes} bytes)");

// 2. Create the store and attach the file to it.
var base_ = await client.VectorStores.CreateAsync(name: "Support base");
await client.VectorStores.Files.CreateAsync(base_.Id, file.Id);
Console.WriteLine($"store: {base_.Id}");

// 3. Indexing is asynchronous. Searching before it finishes returns nothing
//    at all, with no error, so PollAsync waits for it.
var indexado = await client.VectorStores.Files.PollAsync(base_.Id, file.Id);
Console.WriteLine($"indexing: {indexado.Status}");

// 4. Search by meaning, not by exact word.
var hits = await client.Rag.SearchAsync(
    "How many days do I have to return an item?",
    ragId: base_.Id,
    topK: 3);

Console.WriteLine();
foreach (var achado in hits.Results)
{
    Console.WriteLine($"{achado.Score:F2}  {achado.Source}");
    var excerpt = achado.Text.Replace("\n", " ");
    Console.WriteLine($"      {excerpt[..Math.Min(110, excerpt.Length)]}…");
}

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 vira uma classe própria, então dá para tratar por tipo em vez de comparar string de mensagem. Todas herdam de HinowException, e as que vieram da API carregam StatusCode, ErrorType e o corpo da resposta.

Erros.cscsharp
using Hinow;

using var client = new HinowClient();

try
{
    await client.Chat.Completions.CreateAsync(new ChatCompletionRequest
    {
        Model = "himax",  // the hinow/ prefix is missing
        Messages = { new Message("user", "Olá") },
    });
}
catch (HinowAuthenticationException)
{
    Console.WriteLine("Invalid or expired key. Check HINOW_API_KEY.");
}
catch (HinowNotFoundException e)
{
    Console.WriteLine($"Not found: {e.Message}");
}
catch (HinowInvalidRequestException e)
{
    Console.WriteLine($"Invalid request: {e.Message}");
}
catch (HinowRateLimitException)
{
    Console.WriteLine("Rate limited. The SDK already retried; wait a moment.");
}
catch (HinowConnectionException e)
{
    // Nothing reached the API, so nothing was charged.
    Console.WriteLine($"Sem answer da API: {e.Message}");
}
catch (HinowException e)
{
    // Safety net: any other error the API reported.
    Console.WriteLine($"Error {e.StatusCode}: {e.Message}");
}
ClasseQuando acontece
HinowAuthenticationException401 — chave ausente, inválida ou revogada
HinowPermissionException403 — a chave não tem acesso a esse recurso
HinowNotFoundException404 — modelo, arquivo ou assistente inexistente
HinowInvalidRequestException400 ou 422 — falta um campo ou um valor está fora da faixa
HinowRateLimitException429 — limite atingido ou saldo esgotado
HinowServerException5xx — a falha é do lado da API
HinowConnectionExceptiona requisição não chegou; nada foi cobrado
Modelos.cscsharp
using Hinow;

using var client = new HinowClient();

// Account credit, in US dollars.
var balance = await client.GetBalanceAsync();
Console.WriteLine($"balance: US$ {balance.Amount:F2}\n");

// One specific model. The id is namespaced: hinow/himax, not himax.
var model = await client.Models.RetrieveAsync("hinow/himax");
Console.WriteLine($"{model.Name} ({model.Id})");
Console.WriteLine($"categories: {string.Join(", ", model.Category)}");

Tudo que o cliente expõe

MembroPara quê
Chat.CompletionsConversa, streaming, chamada de funções, modo JSON
EmbeddingsVetores para busca semântica
Images · Audio · VideoGeração
ModelsCatálogo 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
GetBalanceAsync()Saldo da conta

Configuração

Cliente.cscsharp
using var client = new HinowClient(
    apiKey: Environment.GetEnvironmentVariable("HINOW_API_KEY"),  // ou use a variável
    baseUrl: "https://api.hinow.ai",                              // ou HINOW_BASE_URL
    timeout: TimeSpan.FromSeconds(120),
    httpClient: null,
    maxRetries: 2);                                               // repete 429 e 5xx

Vindo da versão 1.x

Até a 1.0.1, o SDK empacotava Temperature, MaxTokens, TopP e RepetitionPenalty dentro de um objeto parameters antes de enviar, com os números convertidos em texto. A API aceita esse formato e ignora, então essas opções nunca surtiam efeito: pedir MaxTokens = 10 devolvia a resposta inteira. A partir da 2.0 tudo vai no nível raiz, como a API espera.

Outras três mudanças que afetam código existente:

  • Message.GetContentAsString() devolvia null em toda resposta desserializada, porque o valor chega como JsonElement. Agora funciona, e Message.Text é o nome curto.
  • Os erros passaram a ser tipados, mantendo HinowException como classe base para que os catch existentes continuem valendo.
  • Imagem, vídeo e áudio devolvem no formato da OpenAI: resposta.Data[0].Url no lugar de resposta.Data.Urls[0].

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?