Rust SDK
Install the official HiNow crate and use chat, streaming, web search, agents, and semantic search in Rust.
Updated on Aug 09, 2026
The official Rust SDK for the HiNow API. It is asynchronous, runs on reqwest and tokio, and all response types are custom structs — the compiler holds you accountable before the API does.
The API speaks the OpenAI protocol, and the SDK follows the same format. If you have already integrated with OpenAI, the shape of the calls is what you know.
cargo add hinow-ai tokio --features tokio/full[dependencies]
hinow-ai = "2.0"
tokio = { version = "1", features = ["full"] }The crate is hinow-ai, the use statement is hinow_ai
Cargo accepts hyphens in package names, but Rust does not accept hyphens in identifiers — so the hinow-ai crate becomes the hinow_ai module in your code.
Store the key in HINOW_API_KEY and use Hinow::from_env(). Hinow::new("hi_…") also works, but avoid leaving the value in your code.
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(())
}The hinow/ prefix is part of the model name
Passing himax instead of hinow/himax returns 404 model_not_found, and the message does not make it clear that the prefix is missing. This applies to all: hinow/himax, hinow/hinova, hinow/higenesis.
Read the response via text()
content is a serde_json::Value because a message can carry text or a list of parts. text() handles both cases and returns a String.
create_stream returns a ChatStream, and next().await? delivers one chunk at a time. In a chat interface, this changes the perception of speed more than switching models does.
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(())
}It is delta, not message
In the complete response, the text comes in message.text(). In streaming, each chunk brings only the new snippet, in delta.content — putting it all together is up to you.
Responds instantly, without a job to track, at $0.005 per call. The nine types are in search_type, and an invalid type is rejected before the request leaves.
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(())
}| Type | What is populated in each result |
|---|---|
search_type::SEARCH, SCHOLAR, PATENTS | position, title, url, snippet |
search_type::NEWS | the above plus 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, coordinates |
search_type::SHOPPING | price, delivery, rating, source |
search_type::AUTOCOMPLETE | suggestions; results remains empty |
Scans one or more sites for emails, phone numbers, and social profiles, and each finding comes with the exact page where it appeared. Since scanning takes time, this runs as a 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(())
}Running the same crawl twice is not charged again
The job returns with cached set to true when the result came from the cache. If you want to control the cycle yourself, use website_contacts and track it with tools().jobs().retrieve(&job.job_id) — note that the field is job_id, not id.
Validate what comes back before using it
The scan returns what it found in the page HTML, as it is. Truncated or repeated addresses appear — treat the list as raw material and validate before saving to your database.
Assistants, threads, and runs in the same format as the OpenAI API. You declare the functions, and the model decides when to call them: the run pauses, you execute, and return the result.
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 is not an error
It is the run handing control back to you to execute a function. This is why poll returns in this state instead of continuing to spin: read the required_action, call submit_tool_outputs, and resume polling.
Upload files, group them into a vector store, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without error, which is exactly what poll is for.
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(())
}The filter for vector stores is called rag_id
The API ignores vector_store_id in this endpoint: the search simply scans all documents in the account instead of the vector store you intended. This is the kind of detail that makes it seem like the search is returning garbage.
Error is an enum, so match covers every failure type and the compiler warns you if you miss a case. e.status() returns the HTTP code when the failure came from the 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(())
}| Variant | When it occurs |
|---|---|
Error::Authentication | 401 — missing, invalid, or revoked key |
Error::Permission | 403 — the key lacks access to this resource |
Error::NotFound | 404 — non-existent model, file, or assistant |
Error::InvalidRequest | 400 or 422 — missing a field or a value is out of range |
Error::RateLimit | 429 — limit reached or balance exhausted |
Error::Server | 5xx — the failure is on the API side |
Error::Connection | the request did not arrive; nothing was charged |
Error::Timeout | a job or run did not complete within the given time |
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(())
}| Method | Purpose |
|---|---|
chat() | Chat, streaming, function calling, JSON mode |
embeddings() | Vectors for semantic search |
images() · audio() · video() | Generation |
models() | Catalog, pricing, and capabilities of each model |
tools() | Web search and site contacts |
files() | Document upload |
vector_stores() | Searchable knowledge bases |
rag() | Semantic search on your documents |
beta() | Server-executed agents |
get_balance() | Account balance |
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()?;Up to version 1.0.1, ChatCompletionRequest wrapped temperature, max_tokens, and top_p in a HashMap<String, String> called parameters, converting all numbers to strings. The API accepts this format and ignores it, so requesting max_tokens(10) returned the entire response. Now they are typed fields sent at the root level.
Other changes affecting existing code:
get_balancereturnsBalancedirectly, without the{success, data}wrapper.BalanceResponsebecame an alias, so the old name continues to compile.Errorgained a variant per failure type;Error::Apistill exists for cases that don't fit.ModelInfonow includes name, category, and price, which the API already sent but the SDK discarded.content_as_string()continues to work, andtext()is the short name that also concatenates content across parts.
Choosing between HiMax, HiNova, and HiGenesis
What each model does well, its cost, and how to write the prompt for each.

