TypeScript SDK
Install the official TypeScript SDK and use chat, web search, agents, and semantic search with HiNow.
Updated on Aug 09, 2026
The official TypeScript SDK for the HiNow API. No dependencies, built for Node 18 or newer, and also runs on Deno and Bun.
The API speaks the OpenAI protocol, and the SDK follows the same format. If you've integrated with OpenAI before, the call structure will look familiar.
npm install hinow-aipnpm add hinow-aiyarn add hinow-aiKeep your key in HINOW_API_KEY and the SDK will find it automatically. Passing apiKey in the constructor also works, but avoid leaving the value in your code.
export HINOW_API_KEY="hi_sua_chave_aqui"import { Hinow } from 'hinow-ai';
// The key comes from the HINOW_API_KEY environment variable when you pass nothing.
const client = new Hinow({ apiKey: process.env.HINOW_API_KEY });
const response = await client.chat.completions.create({
model: 'hinow/higenesis',
messages: [{ role: 'user', content: 'Explain what an embedding is in one sentence.' }],
max_tokens: 120,
temperature: 0,
});
console.log(response.choices[0].message.content);
console.log('tokens:', response.usage.total_tokens);The hinow/ prefix is part of the model name
Passing himax instead of hinow/himax returns 404 model_not_found, and the message doesn't clearly indicate that the prefix is missing. This applies to all models: hinow/himax, hinow/hinova, hinow/higenesis.
With stream: true, the return value becomes an async iterable. In a chat interface, this changes the perception of speed more than switching models does.
import { Hinow } from 'hinow-ai';
const client = new Hinow();
const stream = await client.chat.completions.create({
model: 'hinow/hinova',
messages: [{ role: 'user', content: 'List three uses of an LLM, one per line.' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}Responds instantly, without a job to track, at $0.005 per call. The return type follows the type, so the compiler only lets you access the fields that specific type actually returns.
import { Hinow } from 'hinow-ai';
const client = new Hinow();
// The return type follows `type`: `news` gives you date and source,
// em `places` tem telefone e coordenadas, em `autocomplete` tem suggestions.
const web = await client.tools.search({
query: 'plataforma de IA brasileira',
country: 'br',
lang: 'pt-br',
});
console.log(`${web.total_results} results · US$ ${web.cost}`);
for (const r of web.results.slice(0, 3)) {
console.log(`${r.position}. ${r.title}`);
console.log(` ${r.url}`);
}
const news = await client.tools.search({ query: 'artificial intelligence', type: 'news' });
console.log(`\n${news.results[0].source} — ${news.results[0].date}`);type | What comes in each result |
|---|---|
search, scholar, patents | position, title, url, snippet |
news | the above plus source, date, image_url |
images | link, image_url, thumbnail_url, width, height |
videos | channel, duration, date, thumbnail_url |
places | address, category, phone, website, rating, coordinates |
shopping | price, delivery, rating, source |
autocomplete | suggestions, an array of strings — no results field |
Scans one or more sites for emails, phone numbers, and social profiles, returning the page where each contact was found. This runs as a job because scanning takes time.
import { Hinow } from 'hinow-ai';
const client = new Hinow();
// The crawl runs as a job. This method waits and throws if the job fails,
// so `result` always exists when it returns.
const job = await client.tools.websiteContactsAndWait(
{ websites: ['https://teclia.com'], maxDepth: 1, maxLinksPerPage: 5 },
{ onPoll: (j) => console.log('…', j.status) },
);
console.log(`cost US$ ${job.cost} · cached: ${job.cached}`);
for (const c of job.result!.items) {
console.log(`${c.type}: ${c.value} (${c.sourceUrl})`);
}Repeating an identical execution doesn't charge again
The job returns with cached: true when the result came from the cache. If you want to control the lifecycle yourself, use tools.websiteContacts() and track it with tools.jobs.retrieve(job.job_id) — note that the field is job_id, not id.
Assistants, threads, and runs in the same format as the OpenAI API. The model decides when to call your functions; the run pauses, you execute, and return the result.
import { Hinow } from 'hinow-ai';
const client = new Hinow();
const assistant = await client.beta.assistants.create({
model: 'hinow/hinova',
name: 'Order support',
instructions: 'You look up order status. Always use the tool. Answer in one sentence.',
tools: [{
type: 'function',
function: {
name: 'get_order',
description: 'Lookup um order pelo identificador.',
parameters: {
type: 'object',
properties: { order_id: { type: 'string', description: 'Identificador, ex.: "A-1001".' } },
required: ['order_id'],
},
},
}],
});
const thread = await client.beta.threads.create();
await client.beta.threads.messages.create(thread.id, {
role: 'user',
content: 'What is the status of order A-1001?',
});
let run = await client.beta.threads.runs.createAndPoll(thread.id, { assistant_id: assistant.id });
// `requires_action` is not an error: it is the run handing control back to you.
if (run.status === 'requires_action') {
const calls = run.required_action!.submit_tool_outputs.tool_calls;
run = await client.beta.threads.runs.submitToolOutputs(thread.id, run.id, {
tool_outputs: calls.map((call) => ({
tool_call_id: call.id,
output: JSON.stringify({ id: 'A-1001', status: 'delivered', total: 349.9 }),
})),
});
run = await client.beta.threads.runs.poll(thread.id, run.id);
}
const messages = await client.beta.threads.messages.list(thread.id, { order: 'desc', limit: 1 });
console.log(messages.data[0].content[0].text?.value);
await client.beta.assistants.del(assistant.id);
await client.beta.threads.del(thread.id);requires_action is not an error
It is the run handing control back to you to execute a function. That's why poll returns in this state instead of continuing to spin: read the required_action, call submitToolOutputs, and resume tracking.
Upload files, bundle them into a vector store, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without error.
import { Hinow } from 'hinow-ai';
const client = new Hinow();
const text = 'Free shipping on orders over $200. Standard delivery takes 5 business days.';
const file = await client.files.create({
file: new Blob([text], { type: 'text/plain' }),
filename: 'shipping-policy.txt',
purpose: 'assistants',
});
const store = await client.vectorStores.create({ name: 'Políticas' });
let anexo = await client.vectorStores.files.create(store.id, { file_id: file.id });
// Indexing is asynchronous: the attachment comes back as `in_progress`. Searching before
// it finishes returns nothing, with no error at all.
while (anexo.status === 'in_progress') {
await new Promise((r) => setTimeout(r, 1000));
anexo = await client.vectorStores.files.retrieve(store.id, file.id);
}
// The parameter that narrows the search to one store is `rag_id`. Passing
// `vector_store_id` raises no error: the search sweeps the whole account.
const hits = await client.rag.search({
query: 'qual o prazo de entrega?',
rag_id: store.id,
top_k: 2,
});
for (const h of hits.results) {
console.log(`${h.score.toFixed(2)} ${h.source}: ${h.text.slice(0, 60)}`);
}
await client.vectorStores.del(store.id);
await client.files.del(file.id);The base filter is called rag_id
Passing vector_store_id does not throw an error: the search simply scans all documents in the account instead of the intended base. This is the kind of detail that makes it seem like RAG is returning garbage.
Each failure becomes its own class, so you can handle them by type instead of comparing message strings.
import { Hinow, AuthenticationError, RateLimitError, InsufficientBalanceError } from 'hinow-ai';
const client = new Hinow({ apiKey: 'hi_invalid_key' });
try {
await client.getBalance();
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('invalid or revoked key:', error.message);
} else if (error instanceof RateLimitError) {
console.log('rate limited; wait and try again');
} else if (error instanceof InsufficientBalanceError) {
console.log('out of credit');
} else {
throw error;
}
}| Resource | Purpose |
|---|---|
chat.completions | Chat, streaming, function calling, JSON mode |
embeddings | Vectors for semantic search |
images · audio · video | Generation |
models | Catalog, pricing, and features for each model |
tools | Web search and website contacts |
files | Document upload |
vectorStores | Searchable knowledge bases |
rag | Semantic search on your documents |
beta.assistants · beta.threads | Server-executed agents |
getBalance() | Account balance |
const client = new Hinow({
apiKey: process.env.HINOW_API_KEY, // ou deixe em branco e use a variável
baseURL: 'https://api.hinow.ai', // ou HINOW_BASE_URL
timeout: 120_000, // milissegundos
maxRetries: 3,
});Up to version 1.0.7, the SDK wrapped temperature, max_tokens, top_p, and response_format inside a parameters object before sending. The API accepts this format and ignores it, so these options never took effect: requesting max_tokens: 10 returned the full response.
Starting from 2.0, everything goes at the root level, as the API expects. Your code doesn't change — but calls that silently ignored a limit will now respect it, so review prompts that relied on the old behavior.
Choosing between HiMax, HiNova, and HiGenesis
What each model does well, how much it costs, and how to write the prompt for each one.

