Zuno

Standalone Ruby SDK for:

  • provider/model abstraction
  • embeddings via OpenAI-compatible providers
  • single-shot generation
  • iterative tool loops
  • streaming via SSE (OpenRouter)

Install (local development)

bundle install
bundle exec rspec

Breaking change: generate vs loop

  • Zuno.generate is now single-shot.
  • Zuno.loop contains the previous iterative tool-loop behavior.

If you previously relied on iterative tool calls in generate, move that code to loop.

Providers

OpenRouter

require "zuno"

openrouter = Zuno.openrouter(
  api_key: "your-openrouter-key", # required
  app_url: "https://example.com",
  title: "my-app"
)

Replicate

replicate = Zuno.replicate(api_key: "your-replicate-key") # required

Single-shot generation (generate)

OpenRouter

result = Zuno.generate(
  model: openrouter.model("openai/gpt-5-mini"),
  prompt: "Say hello"
)

puts result[:text]

Embeddings (embed)

embed supports OpenAI-compatible embedding providers such as OpenRouter and AI Gateway.

result = Zuno.embed(
  model: openrouter.model("openai/text-embedding-3-small"),
  content: "Sunny day at the beach"
)

pp result[:embedding]

generate supports tool definitions and executes returned tool calls once, without a follow-up LLM request.

Replicate

generate with Replicate requires input: and waits for completion using:

  • Prefer: wait=60 on create
  • polling every 1 second
  • hard timeout at 10 minutes
result = Zuno.generate(
  model: replicate.model("owner/model"),
  input: { prompt: "A watercolor fox" }
)

puts result[:status]
pp result[:output]

Replicate reference types:

replicate.version("version-id")
replicate.model("owner/model")
replicate.deployment("owner/deployment")

Webhooks are not supported. Passing webhook or webhook_events_filter raises an error.

Iterative tool execution (loop)

loop is OpenRouter-only and preserves the previous iterative behavior.

ping = Zuno.tool(
  name: "ping",
  description: "Ping tool",
  input_schema: { type: "object", properties: {} }
) { { ok: true } }

result = Zuno.loop(
  model: openrouter.model("openai/gpt-5-mini"),
  prompt: "Run tools until done",
  tools: { ping: ping },
  max_iterations: 24
)

loop supports:

  • before_generation
  • after_generation
  • before_iteration
  • after_iteration
  • before_tool_execution
  • after_tool_execution
  • max_iterations (Integer, :infinite, or Float::INFINITY)
  • stop_when: { tool_called: ... }

Loop callbacks can accept:

  • a second argument (control) and call control.stop!(reason: "...")
  • a third argument (context) to mutate the next LLM request state

context supports:

  • context.system = "..." or context.clear_system!
  • context.messages = [...], context.add_message(...), context.add_messages(...)
  • context.tools = {...}, context.add_tool(:name, tool), context.add_tools(...), context.remove_tool(:name)

Example:

before_iteration: ->(_payload, _control, context) {
  context.system = "Be concise"
  context.add_message(role: "user", content: "Answer in bullet points")
}

Tool choice

generate and loop support AI SDK-style tool choice when tools are present:

  • "auto" (default)
  • "required"
  • "none"
  • { type: "tool", toolName: "my_tool" }

Streaming (stream)

stream is OpenRouter-only.

Zuno.stream(
  model: openrouter.model("openai/gpt-5-mini"),
  prompt: "Stream hello"
) do |event|
  p event
end

Automated releases

This repo includes .github/workflows/release.yml to automate versioning and gem publication:

  • release-please inspects Conventional Commits on main, opens/updates a release PR, and bumps lib/zuno/version.rb when the release PR is merged.
  • When a new GitHub release/tag is created, the workflow builds the gem and publishes it to RubyGems.

One-time setup

Add this GitHub repository secret:

  • RUBYGEMS_API_KEY

Commit format for version bumping

  • fix: ... -> patch
  • feat: ... -> minor
  • feat!: ... or any commit with BREAKING CHANGE: -> major