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.
cargo add hinow-ai tokio --features tokio/full[dependencies]
hinow-ai = "2.0"
tokio = { version = "1", features = ["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.
export HINOW_API_KEY="hi_sua_chave_aqui"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.
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.
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ê.
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.
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(())
}| Tipo | O que vem preenchido em cada resultado |
|---|---|
search_type::SEARCH, SCHOLAR, PATENTS | position, title, url, snippet |
search_type::NEWS | os acima mais source, date, image_url |
search_type::IMAGES | link, image_url, thumbnail_url, width, height |
search_type::VIDEOS | channel, duration, date, thumbnail_url |
search_type::PLACES | address, category, phone, website, rating, coordenadas |
search_type::SHOPPING | price, delivery, rating, source |
search_type::AUTOCOMPLETE | suggestions; results fica vazio |
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.
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.
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.
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.
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.
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.
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.
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(())
}| Variante | Quando acontece |
|---|---|
Error::Authentication | 401 — chave ausente, inválida ou revogada |
Error::Permission | 403 — a chave não tem acesso a esse recurso |
Error::NotFound | 404 — modelo, arquivo ou assistente inexistente |
Error::InvalidRequest | 400 ou 422 — falta um campo ou um valor está fora da faixa |
Error::RateLimit | 429 — limite atingido ou saldo esgotado |
Error::Server | 5xx — a falha é do lado da API |
Error::Connection | a requisição não chegou; nada foi cobrado |
Error::Timeout | um job ou run não terminou no tempo dado |
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(())
}| Método | Para 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 |
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()?;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_balancedevolveBalancedireto, sem o embrulho{success, data}.BalanceResponsevirou um alias, então o nome antigo continua compilando.Errorganhou uma variante por tipo de falha;Error::Apicontinua existindo para o que não se encaixa.ModelInfopassou a trazer nome, categoria e preço, que a API já enviava e o SDK descartava.content_as_string()continua funcionando, etext()é 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.

