Ir para o conteúdo

SDK Rust

Instale o crate oficial do HiNow e use chat, streaming, busca na web, agentes e busca semântica em Rust.

Atualizado em 09 de ago. de 2026

O SDK oficial em Rust para a API do HiNow. É assíncrono, roda sobre reqwest e tokio, e todos os tipos de resposta são estruturas próprias — o compilador cobra de você antes da API cobrar.

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
cargo add hinow-ai tokio --features tokio/full

O crate é hinow-ai, o use é hinow_ai

Cargo aceita hífen no nome do pacote, mas o Rust não aceita hífen em identificador — então o crate hinow-ai vira o módulo hinow_ai no seu código.

Guarde a chave em HINOW_API_KEY e use Hinow::from_env(). Hinow::new("hi_…") também funciona, mas evite deixar o valor no código.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

A primeira chamada

main.rsrust
use hinow_ai::{ChatCompletionRequest, Hinow, Message};

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    // from_env reads HINOW_API_KEY.
    let client = Hinow::from_env()?;

    let answer = client
        .chat()
        .completions()
        .create(
            ChatCompletionRequest::new("hinow/himax")
                .add_message(Message::system("You answer in English."))
                .add_message(Message::user("What is an API? Answer in one paragraph.")),
        )
        .await?;

    // text() gives you the string; content holds the raw value.
    println!("{}", answer.choices[0].message.text());

    if let Some(uso) = &answer.usage {
        println!("\ninput: {} · output: {}", uso.prompt_tokens, uso.completion_tokens);
    }

    Ok(())
}

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

content é um serde_json::Value porque uma mensagem pode carregar texto ou uma lista de partes. text() resolve os dois casos e devolve String.

Mostrando a resposta enquanto ela chega

create_stream devolve um ChatStream, e next().await? entrega um pedaço por vez. Numa tela de conversa isso muda a percepção de velocidade mais do que trocar de modelo.

streaming.rsrust
use hinow_ai::{ChatCompletionRequest, Hinow, Message};
use std::io::Write;

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    let client = Hinow::from_env()?;

    let mut stream = client
        .chat()
        .completions()
        .create_stream(
            ChatCompletionRequest::new("hinow/hinova")
                .add_message(Message::user("Write three lines about the sea at dawn.")),
        )
        .await?;

    while let Some(chunk) = stream.next().await? {
        // Note the delta: each chunk carries the new fragment, not the whole answer.
        if let Some(escolha) = chunk.choices.first() {
            print!("{}", escolha.delta.content.as_deref().unwrap_or(""));
            let _ = std::io::stdout().flush();
        }
    }

    println!();
    Ok(())
}

É 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 search_type, e um tipo inválido é recusado antes de a requisição sair.

busca_web.rsrust
use hinow_ai::{Hinow, SearchRequest};

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    let client = Hinow::from_env()?;

    // Answers on the spot: there is no job to follow.
    let search = client
        .tools()
        .search(
            SearchRequest::new("best beaches in northeast Brazil")
                .country("br")
                .lang("pt-br"),
        )
        .await?;

    for item in &search.results {
        println!("{}. {}", item.position, item.title.clone().unwrap_or_default());
        println!("   {}", item.url.clone().unwrap_or_default());
    }

    println!("\n{} results · US$ {}", search.results.len(), search.cost);
    Ok(())
}
TipoO que vem preenchido em cada resultado
search_type::SEARCH, SCHOLAR, PATENTSposition, title, url, snippet
search_type::NEWSos acima mais source, date, image_url
search_type::IMAGESlink, image_url, thumbnail_url, width, height
search_type::VIDEOSchannel, duration, date, thumbnail_url
search_type::PLACESaddress, category, phone, website, rating, coordenadas
search_type::SHOPPINGprice, delivery, rating, source
search_type::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.

contatos_site.rsrust
use hinow_ai::{Hinow, WebsiteContactsRequest};

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    let client = Hinow::from_env()?;

    // The crawl takes a while, so it runs as a job. This method waits for you.
    let job = client
        .tools()
        .website_contacts_and_wait(
            WebsiteContactsRequest::new(vec!["https://www.teclia.com".to_string()])
                .max_depth(2)
                .max_links_per_page(10),
        )
        .await?;

    let cache = if job.cached == Some(true) { " (from cache)" } else { "" };
    println!("status: {} · US$ {}{}\n", job.status, job.cost.unwrap_or(0.0), cache);

    // Each item carries the type, the value and the exact page it was found on.
    if let Some(resultado) = &job.result {
        for item in &resultado.items {
            match item.item_type.as_str() {
                "email" | "phone" => {
                    println!("{:<9}{}", item.item_type, item.value);
                    println!("{:<9}em {}", "", item.source_url);
                }
                _ => println!("{:<9}{}", item.platform.clone().unwrap_or_default(),
                              item.url.clone().unwrap_or_default()),
            }
        }
    }

    Ok(())
}

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 website_contacts e acompanhe com tools().jobs().retrieve(&job.job_id) — 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.rsrust
use hinow_ai::{AssistantRequest, Hinow, Tool, ToolOutput};
use serde_json::json;

// Your real function: a match here, just for the example.
fn get_order(code: &str) -> serde_json::Value {
    match code {
        "A-1001" => json!({ "status": "delivered", "delivered": "2026-08-02" }),
        "A-1002" => json!({ "status": "in transit", "estimated": "2026-08-12" }),
        _ => json!({ "error": "order not found" }),
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Hinow::from_env()?;

    // 1. The assistant: model, instructions and the functions it may call.
    let assistant = client
        .beta()
        .assistants()
        .create(
            AssistantRequest::new("hinow/himax")
                .name("Support")
                .instructions(
                    "You answer questions about orders. \
                     Check the tool before stating any status.",
                )
                .tools(vec![Tool::function(
                    "get_order",
                    "Look up an order by its code.",
                    json!({
                        "type": "object",
                        "properties": {
                            "code": { "type": "string", "description": "Order code, e.g. A-1001" }
                        },
                        "required": ["code"]
                    }),
                )]),
        )
        .await?;

    // 2. The conversation and the question.
    let thread = client.beta().threads().create().await?;
    client
        .beta()
        .threads()
        .messages()
        .create(&thread.id, "Has order A-1002 arrived?", "user")
        .await?;

    // 3. Run it and wait.
    let mut run = client
        .beta()
        .threads()
        .runs()
        .create_and_poll(&thread.id, &assistant.id)
        .await?;

    // 4. While the model asks for functions, run them and hand the result back.
    while run.status == "requires_action" {
        let calls = run
            .required_action
            .as_ref()
            .and_then(|acao| acao.submit_tool_outputs.as_ref())
            .map(|s| s.tool_calls.clone())
            .unwrap_or_default();

        let mut outputs = Vec::new();
        for call in calls {
            let args: serde_json::Value = serde_json::from_str(&call.function.arguments)?;
            let code = args["code"].as_str().unwrap_or_default();
            println!("-> the model called {}({})", call.function.name, code);

            outputs.push(ToolOutput::new(
                &call.id,
                &get_order(code).to_string(),
            ));
        }

        client
            .beta()
            .threads()
            .runs()
            .submit_tool_outputs(&thread.id, &run.id, outputs)
            .await?;

        run = client.beta().threads().runs().poll(&thread.id, &run.id).await?;
    }

    // 5. The final answer is the last message on the thread.
    let messages = client
        .beta()
        .threads()
        .messages()
        .list(&thread.id, Some(1), Some("desc"))
        .await?;
    println!("\n{}", messages.data[0].text());

    client.beta().assistants().delete(&assistant.id).await?;
    Ok(())
}

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 required_action, chame submit_tool_outputs 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.rsrust
use hinow_ai::{Hinow, RagSearchRequest, VectorStoreRequest};

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    let client = Hinow::from_env()?;

    // 1. Upload the document.
    let file = client
        .files()
        .create_from_path("returns-policy.txt", "assistants")
        .await?;
    println!("file: {} ({} bytes)", file.id, file.bytes);

    // 2. Create the store and attach the file to it.
    let base = client
        .vector_stores()
        .create(VectorStoreRequest::new("Support base"))
        .await?;
    client.vector_stores().files().create(&base.id, &file.id).await?;
    println!("store: {}", base.id);

    // 3. Indexing is asynchronous. Searching before it finishes returns nothing
    //    at all, with no error, so poll waits for it.
    let state = client.vector_stores().files().poll(&base.id, &file.id).await?;
    println!("indexing: {}\n", state.status);

    // 4. Search by meaning, not by exact word.
    let hits = client
        .rag()
        .search(
            RagSearchRequest::new("How many days do I have to return an item?")
                .rag_id(&base.id)
                .top_k(3),
        )
        .await?;

    for achado in &hits.results {
        let excerpt: String = achado.text.replace('\n', " ").chars().take(110).collect();
        println!("{:.2}  {}", achado.score, achado.source.clone().unwrap_or_default());
        println!("      {}…", excerpt);
    }

    Ok(())
}

O filtro por base chama-se rag_id

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

Error é um enum, então o match cobre cada tipo de falha e o compilador avisa se você esquecer um caso. e.status() devolve o código HTTP quando a falha veio da API.

erros.rsrust
use hinow_ai::{ChatCompletionRequest, Error, Hinow, Message};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Hinow::from_env()?;

    let resultado = client
        .chat()
        .completions()
        .create(
            ChatCompletionRequest::new("himax") // the hinow/ prefix is missing
                .add_message(Message::user("Olá")),
        )
        .await;

    match resultado {
        Ok(answer) => println!("{}", answer.choices[0].message.text()),
        Err(Error::Authentication { .. }) => {
            println!("Invalid or expired key. Check HINOW_API_KEY.")
        }
        Err(Error::NotFound { message, .. }) => println!("Not found: {}", message),
        Err(Error::InvalidRequest { message, .. }) => println!("Invalid request: {}", message),
        Err(Error::RateLimit { .. }) => {
            println!("Rate limited. The SDK already retried; wait a moment.")
        }
        // Nothing reached the API, so nothing was charged.
        Err(Error::Connection(e)) => println!("Sem answer da API: {}", e),
        // Safety net: any other failure.
        Err(e) => println!("Error {:?}: {}", e.status(), e),
    }

    Ok(())
}
VarianteQuando acontece
Error::Authentication401 — chave ausente, inválida ou revogada
Error::Permission403 — a chave não tem acesso a esse recurso
Error::NotFound404 — modelo, arquivo ou assistente inexistente
Error::InvalidRequest400 ou 422 — falta um campo ou um valor está fora da faixa
Error::RateLimit429 — limite atingido ou saldo esgotado
Error::Server5xx — a falha é do lado da API
Error::Connectiona requisição não chegou; nada foi cobrado
Error::Timeoutum job ou run não terminou no tempo dado
modelos.rsrust
use hinow_ai::Hinow;

#[tokio::main]
async fn main() -> Result<(), hinow_ai::Error> {
    let client = Hinow::from_env()?;

    // Account credit, in US dollars.
    let balance = client.get_balance().await?;
    println!("balance: US$ {:.2}\n", balance.balance);

    // One specific model. The id is namespaced: hinow/himax, not himax.
    let model = client.models().retrieve("hinow/himax").await?;
    println!("{} ({})", model.name.clone().unwrap_or_default(), model.id);
    println!("categories: {}", model.category.join(", "));

    if let Some(cost) = &model.cost {
        println!(
            "price per million tokens: input US$ {} · output US$ {}",
            cost.input.unwrap_or(0.0),
            cost.output.unwrap_or(0.0)
        );
    }

    Ok(())
}

Tudo que o cliente expõe

MétodoPara quê
chat()Conversa, streaming, chamada de funções, modo JSON
embeddings()Vetores para busca semântica
images() · audio() · video()Geração
models()Catálogo, preço e recursos de cada modelo
tools()Busca na web e contatos de site
files()Upload de documentos
vector_stores()Bases de conhecimento pesquisáveis
rag()Busca semântica nos seus documentos
beta()Agentes executados no servidor
get_balance()Saldo da conta

Configuração

cliente.rsrust
use hinow_ai::Hinow;
use std::time::Duration;

let client = Hinow::builder(&std::env::var("HINOW_API_KEY")?)
    .base_url("https://api.hinow.ai")  // ou HINOW_BASE_URL
    .timeout(Duration::from_secs(120))
    .max_retries(2)                    // repete 429 e 5xx
    .build()?;

Vindo da versão 1.x

Até a 1.0.1, ChatCompletionRequest empacotava temperature, max_tokens e top_p num HashMap<String, String> chamado parameters, convertendo todo número em texto. A API aceita esse formato e ignora, então pedir max_tokens(10) devolvia a resposta inteira. Agora são campos tipados, enviados no nível raiz.

Outras mudanças que afetam código existente:

  • get_balance devolve Balance direto, sem o embrulho {success, data}. BalanceResponse virou um alias, então o nome antigo continua compilando.
  • Error ganhou uma variante por tipo de falha; Error::Api continua existindo para o que não se encaixa.
  • ModelInfo passou a trazer nome, categoria e preço, que a API já enviava e o SDK descartava.
  • content_as_string() continua funcionando, e text() é o nome curto que também junta conteúdo em partes.

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?