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.generateis now single-shot.Zuno.loopcontains 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.(
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=60on 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_generationafter_generationbefore_iterationafter_iterationbefore_tool_executionafter_tool_executionmax_iterations(Integer,:infinite, orFloat::INFINITY)stop_when: { tool_called: ... }
Loop callbacks can accept:
- a second argument (
control) and callcontrol.stop!(reason: "...") - a third argument (
context) to mutate the next LLM request state
context supports:
context.system = "..."orcontext.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-pleaseinspects Conventional Commits onmain, opens/updates a release PR, and bumpslib/zuno/version.rbwhen 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: ...-> patchfeat: ...-> minorfeat!: ...or any commit withBREAKING CHANGE:-> major