Skip to content

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.

Installation

terminalbash
gem install hinow-ai

The 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.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

The first call

primeira_chamada.rbruby
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").

In a Rails app

To avoid creating a client for every request, configure it once in an initializer and use Hinow.client wherever needed.

config/initializers/hinow.rbruby
require "hinow"

Hinow.configure do |config|
  config.api_key = ENV.fetch("HINOW_API_KEY")
  config.timeout = 120
end

Showing the response as it arrives

create_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.

streaming.rbruby
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

puts

It'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.

busca_web.rbruby
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, patentsposition, title, url, snippet
newsthe above plus source, date, image_url
imageslink, image_url, thumbnail_url, width, height
videoschannel, duration, date, thumbnail_url
placesaddress, category, phone, website, rating, coordinates
shoppingprice, delivery, rating, source
autocompletesuggestions, an array of strings — there is no results

Website contacts

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.

contatos_site.rbruby
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
end

Running 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.

Agents

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.

agente.rbruby
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.

conhecimento.rbruby
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]}…"
end

The 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.

Errors

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.

erros.rbruby
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
ClassWhen it happens
Hinow::AuthenticationError401 — missing, invalid, or revoked key
Hinow::PermissionError403 — the key does not have access to this resource
Hinow::NotFoundError404 — non-existent model, file, or assistant
Hinow::InvalidRequestError400 or 422 — a field is missing or a value is out of range
Hinow::RateLimitError429 — limit reached or balance exhausted
Hinow::ServerError5xx — the failure is on the API side
Hinow::ConnectionErrorthe request did not arrive; nothing was charged
Hinow::TimeoutErrora job or run did not finish within the given time

Balance and catalog

modelos.rbruby
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(', ')}"

Everything the client exposes

MethodFor what
chat.completionsChat, streaming, function calling, JSON mode
embeddingsVectors for semantic search
images · audio · videoGeneration
modelsCatalog and features of each model
toolsWeb search and site contacts
filesDocument upload
vector_storesSearchable knowledge bases
ragSemantic search on your documents
beta.assistants · beta.threadsServer-side agents
get_balanceAccount balance

Configuration

cliente.rbruby
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
)

Coming from version 1.x

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.

Was this page helpful?