Quickstart: Ruby

This quickstart uses Net::HTTP and JSON from the Ruby standard library. It needs Ruby 3.1 or later.

Write a small client

# syntic.rb
require "json"
require "net/http"
require "uri"
 
module Syntic
  URL = URI("https://app.syntic.ai/v1/chat/completions")
 
  class Error < StandardError
    attr_reader :status, :code, :request_id
    def initialize(status, code, request_id)
      @status, @code, @request_id = status, code, request_id
      super("#{status} #{code} (request #{request_id})")
    end
  end
 
  def self.request(body)
    req = Net::HTTP::Post.new(URL)
    req["Authorization"] = "Bearer #{ENV.fetch('SYNTIC_API_KEY')}"
    req["Content-Type"] = "application/json"
    req.body = JSON.generate(body)
    req
  end
 
  def self.complete(body)
    Net::HTTP.start(URL.host, URL.port, use_ssl: true, read_timeout: 300) do |http|
      res = http.request(request({ stream: false }.merge(body)))
      data = JSON.parse(res.body)
      raise Error.new(res.code.to_i, data["error"], res["x-syntic-request-id"]) unless res.is_a?(Net::HTTPSuccess)
      data
    end
  end
end

Send a request

# hello.rb
require_relative "syntic"
 
reply = Syntic.complete(
  model: "Amara S1.2",
  max_tokens: 300,
  messages: [
    { role: "system", content: "Answer in two sentences." },
    { role: "user", content: "Why do bicycles stay upright when moving?" }
  ]
)
puts reply.dig("choices", 0, "message", "content")
puts "tokens: #{reply.dig('usage', 'total_tokens')}"

Run it

ruby hello.rb

Stream the reply

read_body with a block yields the body in pieces as they arrive:

module Syntic
  def self.stream(body)
    Net::HTTP.start(URL.host, URL.port, use_ssl: true, read_timeout: 300) do |http|
      http.request(request(body.merge(stream: true))) do |res|
        unless res.is_a?(Net::HTTPSuccess)
          data = JSON.parse(res.read_body)
          raise Error.new(res.code.to_i, data["error"], res["x-syntic-request-id"])
        end
        buffer = +""
        res.read_body do |piece|
          buffer << piece
          while (newline = buffer.index("\n"))
            line = buffer.slice!(0..newline).strip
            next unless line.start_with?("data: ")
            payload = line.delete_prefix("data: ")
            return if payload == "[DONE]"
            event = JSON.parse(payload)
            raise "stream failed: #{event['detail']}" if event["error"]
            text = event.dig("choices", 0, "delta", "content")
            yield text if text && !text.empty?
          end
        end
      end
    end
  end
end
 
Syntic.stream(model: "Amara Quick", messages: [{ role: "user", content: "Name four famous lighthouses." }]) do |text|
  print text
  $stdout.flush
end
puts

Breaking out of the block (or returning from it) closes the connection and stops generation.

Call a function

tools = [{
  type: "function",
  function: {
    name: "pool_hours",
    description: "Opening hours of the public swimming pool on a given date (YYYY-MM-DD).",
    parameters: {
      type: "object",
      properties: { date: { type: "string" } },
      required: ["date"]
    }
  }
}]
 
messages = [{ role: "user", content: "Is the pool open on 2026-12-26?" }]
reply = Syntic.complete(model: "Amara S1.2", tools: tools, messages: messages)
msg = reply.dig("choices", 0, "message")
 
if msg["tool_calls"]&.any?
  messages << msg                                          # the assistant turn, unchanged
  msg["tool_calls"].each do |call|
    args = JSON.parse(call.dig("function", "arguments")) rescue {}
    result = { open: "10:00", close: "14:00" }             # replace with a real lookup
    messages << { role: "tool", tool_call_id: call["id"], content: JSON.generate(result) }
  end
  reply = Syntic.complete(model: "Amara S1.2", tools: tools, messages: messages)
end
puts reply.dig("choices", 0, "message", "content")

If you use an OpenAI-compatible Ruby gem, point its base URI at https://app.syntic.ai/ (with the v1 API version) and pass stream: false in the parameters of non-streaming calls.

Handle errors

begin
  Syntic.complete(model: "Amara S1.2", messages: [])
rescue Syntic::Error => e
  case e.status
  when 429, 500..599 then # retry with back-off
  when 402 then           # quota_exhausted: see the Handle 402 recipe
  else raise
  end
end

See Retries with back-off and Error codes.

Next steps

Was this page helpful?