Ruby SDK
Install the official HiNow gem and use chat, streaming, web search, agents, and semantic search in Ruby and Rails.
Updated on Aug 09, 2026
The official Ruby SDK for the HiNow API. Runs on Ruby 3.0 or newer and uses Faraday, so it fits into any Rails app without introducing a different HTTP stack.
The API speaks the OpenAI protocol, and the SDK follows the same format. If you've already integrated with OpenAI, the call structure is what you're used to.
gem install hinow-aigem "hinow-ai"bundle add hinow-aiThe gem is hinow-ai, and the require is hinow
They are different names by design: gem install hinow-ai installs the package, and require "hinow" loads it. Running gem install hinow installs something else.
Store the key in HINOW_API_KEY and the SDK will find it automatically. Passing api_key: in the constructor also works, but avoid leaving the value in your code.
export HINOW_API_KEY="hi_sua_chave_aqui"require "hinow"
# With no arguments, the client reads the key from HINOW_API_KEY.
client = Hinow::Client.new
answer = client.chat.completions.create(
model: "hinow/himax",
messages: [
{ role: "system", content: "You answer in English." },
{ role: "user", content: "What is an API? Answer in one paragraph." }
]
)
puts answer["choices"][0]["message"]["content"]
# What it cost, in tokens.
uso = answer["usage"]
puts "\ninput: #{uso['prompt_tokens']} · output: #{uso['completion_tokens']}"The hinow/ prefix is part of the model name
Sending 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.
Responses are hashes with string keys, so dig works from start to finish: resposta.dig("choices", 0, "message", "content").
To avoid creating a client for every request, configure it once in an initializer and use Hinow.client wherever needed.
require "hinow"
Hinow.configure do |config|
config.api_key = ENV.fetch("HINOW_API_KEY")
config.timeout = 120
endclass Atendimento
def responder(pergunta)
resposta = Hinow.client.chat.completions.create(
model: "hinow/himax",
messages: [{ role: "user", content: pergunta }]
)
resposta.dig("choices", 0, "message", "content")
end
endcreate_stream delivers each chunk to the block as soon as it arrives. In a chat interface, this changes the perception of speed more than switching models. Without a block, it returns an Enumerator.
require "hinow"
client = Hinow::Client.new
client.chat.completions.create_stream(
model: "hinow/hinova",
messages: [{ role: "user", content: "Write three lines about the sea at dawn." }]
) do |chunk|
# Note the "delta": each chunk carries the new fragment, not the whole answer.
print chunk.dig("choices", 0, "delta", "content")
$stdout.flush
end
putsIt's delta, not message
In the complete response, the text comes in message.content. In streaming, each chunk brings only the new piece, in delta.content — joining them all is up to you.
Responds instantly, without a job to track, at $0.005 per call. country: and lang: guide the results.
require "hinow"
client = Hinow::Client.new
# Answers on the spot: there is no job to follow.
search = client.tools.search("best beaches in northeast Brazil", country: "br", lang: "pt-br")
search["results"].each do |item|
puts "#{item['position']}. #{item['title']}"
puts " #{item['url']}"
end
puts "\n#{search['results'].size} results · US$ #{search['cost']}"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 — there is no results |
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.
require "hinow"
client = Hinow::Client.new
# The crawl takes a while, so it runs as a job. This method waits for you.
job = client.tools.website_contacts_and_wait(
["https://www.teclia.com"],
max_depth: 2,
max_links_per_page: 10
)
puts "status: #{job['status']} · US$ #{job['cost']}#{job['cached'] ? ' (from cache)' : ''}"
puts
# Each item carries the type, the value and the exact page it was found on.
job["result"]["items"].each do |item|
case item["type"]
when "email", "phone"
puts "#{item['type'].ljust(9)}#{item['value']}"
puts "#{' '.ljust(9)}em #{item['sourceUrl']}"
else
puts "#{item['platform'].to_s.ljust(9)}#{item['url']}"
end
endRunning the same crawl twice is not charged again
The job returns cached as 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 before using it
The scan returns what it found in the page HTML, as it is. Truncated or repeated addresses may 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.
require "hinow"
require "json"
client = Hinow::Client.new
# Your real function: a hash here, just for the example.
ORDERS = {
"A-1001" => { status: "delivered", delivered: "2026-08-02" },
"A-1002" => { status: "in transit", estimated: "2026-08-12" }
}.freeze
def get_order(code)
ORDERS[code] || { error: "order not found" }
end
# 1. The assistant: model, instructions and the functions it may call.
assistant = client.beta.assistants.create(
model: "hinow/himax",
name: "Support",
instructions: "You answer questions about orders. Check the tool before stating any status.",
tools: [{
type: "function",
function: {
name: "get_order",
description: "Look up an order by its code.",
parameters: {
type: "object",
properties: {
code: { type: "string", description: "Order code, e.g. A-1001" }
},
required: ["code"]
}
}
}]
)
# 2. The conversation and the question.
thread = client.beta.threads.create
client.beta.threads.messages.create(thread["id"], "Has order A-1002 arrived?")
# 3. Run it and wait.
run = client.beta.threads.runs.create_and_poll(thread["id"], assistant_id: assistant["id"])
# 4. While the model asks for functions, run them and hand the result back.
while run["status"] == "requires_action"
outputs = run["required_action"]["submit_tool_outputs"]["tool_calls"].map do |call|
args = JSON.parse(call["function"]["arguments"])
puts "-> the model called #{call['function']['name']}(#{args['code']})"
{
tool_call_id: call["id"],
output: get_order(args["code"]).to_json
}
end
client.beta.threads.runs.submit_tool_outputs(thread["id"], run["id"], outputs)
run = client.beta.threads.runs.poll(thread["id"], run["id"])
end
# 5. The final answer is the last message on the thread.
messages = client.beta.threads.messages.list(thread["id"], limit: 1, order: "desc")
puts "\n#{messages['data'][0]['content'][0]['text']['value']}"
client.beta.assistants.delete(assistant["id"])requires_action is not an error
It is the run handing control back to you to execute a function. That is why poll returns in this state instead of continuing to spin: read the required_action, call submit_tool_outputs, and resume tracking.
Upload files, group them into a base, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without error, and that is what poll is for.
require "hinow"
client = Hinow::Client.new
# 1. Upload the document.
file = client.files.create("returns-policy.txt")
puts "file: #{file['id']} (#{file['bytes']} bytes)"
# 2. Create the store and attach the file to it.
base = client.vector_stores.create(name: "Support base")
client.vector_stores.files.create(base["id"], file["id"])
puts "store: #{base['id']}"
# 3. Indexing is asynchronous. Searching before it finishes returns nothing
# at all, with no error, so poll waits for it.
state = client.vector_stores.files.poll(base["id"], file["id"])
puts "indexing: #{state['status']}"
# 4. Search by meaning, not by exact word.
hits = client.rag.search(
"How many days do I have to return an item?",
rag_id: base["id"],
top_k: 3
)
puts
hits["results"].each do |achado|
puts "#{achado['score'].round(2)} #{achado['source']}"
puts " #{achado['text'].tr("\n", ' ')[0, 110]}…"
endThe filter by base is called rag_id
Passing vector_store_id does not generate an error: the search simply scans all documents in the account instead of the base you intended. It is the kind of detail that makes it seem like the search is returning garbage.
Each failure becomes its own class, so you can handle by type instead of comparing message strings. All inherit from Hinow::Error, and those from the API carry status_code, error_type, and the response body.
require "hinow"
client = Hinow::Client.new
begin
client.chat.completions.create(
model: "himax", # the hinow/ prefix is missing
messages: [{ role: "user", content: "Olá" }]
)
rescue Hinow::AuthenticationError
puts "Invalid or expired key. Check HINOW_API_KEY."
rescue Hinow::NotFoundError => e
puts "Not found: #{e.message}"
rescue Hinow::InvalidRequestError => e
puts "Invalid request: #{e.message}"
rescue Hinow::RateLimitError
puts "Rate limited. The SDK already retried; wait a moment."
rescue Hinow::ConnectionError => e
# Nothing reached the API, so nothing was charged.
puts "Sem answer da API: #{e.message}"
rescue Hinow::Error => e
# Safety net: any other error the API reported.
puts "Error #{e.status_code}: #{e.message}"
end| Class | When it happens |
|---|---|
Hinow::AuthenticationError | 401 — missing, invalid, or revoked key |
Hinow::PermissionError | 403 — the key does not have access to this resource |
Hinow::NotFoundError | 404 — non-existent model, file, or assistant |
Hinow::InvalidRequestError | 400 or 422 — a field is missing or a value is out of range |
Hinow::RateLimitError | 429 — limit reached or balance exhausted |
Hinow::ServerError | 5xx — the failure is on the API side |
Hinow::ConnectionError | the request did not arrive; nothing was charged |
Hinow::TimeoutError | a job or run did not finish within the given time |
require "hinow"
client = Hinow::Client.new
# Account credit, in US dollars.
balance = client.get_balance
puts "balance: US$ #{format('%.2f', balance['balance'])}"
puts
# One specific model. The id is namespaced: hinow/himax, not himax.
model = client.models.retrieve("hinow/himax")
puts "#{model['name']} (#{model['id']})"
puts "categories: #{model['category'].join(', ')}"| Method | For what |
|---|---|
chat.completions | Chat, streaming, function calling, JSON mode |
embeddings | Vectors for semantic search |
images · audio · video | Generation |
models | Catalog and features of each model |
tools | Web search and site contacts |
files | Document upload |
vector_stores | Searchable knowledge bases |
rag | Semantic search on your documents |
beta.assistants · beta.threads | Server-side agents |
get_balance | Account balance |
client = Hinow::Client.new(
api_key: ENV["HINOW_API_KEY"], # ou deixe em branco e use a variável
base_url: "https://api.hinow.ai", # ou HINOW_BASE_URL
timeout: 120, # segundos
max_retries: 2 # repete 429 e 5xx
)Up to version 1.0.1, the SDK wrapped temperature, max_tokens, top_p, and repetition_penalty inside a parameters object before sending, with the numbers converted to strings. The API accepts this format and ignores it, so these options never took effect: requesting max_tokens: 10 returned the entire response. Starting from 2.0, everything goes at the root level, as the API expects.
Two other changes: errors are now typed, keeping Hinow::Error as the base class so existing rescue blocks continue to work; and images, video, and audio return in the OpenAI format, response["data"][0]["url"] instead of response["data"]["urls"][0].
Choosing between HiMax, HiNova, and HiGenesis
What each model does well, how much it costs, and how to write the prompt for each one.

