Ruby SDK
This page covers the full Ruby SDK path: trace binding, explicit execute control, tool loops, simulation, and private-agent routing.
If you are migrating an existing app and only need the OpenAI-compatible gateway path, start with Quick Start or the SDK overview.
Full SDK (explicit control)
Use the Ruby SDK when you want explicit trace binding, tool loops, and typed Olyx resource calls.
Olyx.configure do |config|
config.api_key = ENV["OLYX_API_KEY"]
config.fail_open = false
end
client = Olyx.new
Requires Ruby ≥ 3.3.0. No runtime dependencies (net/http only).
Installation
bundle add olyx
# or: gem install olyx
Pure Ruby. No native dependencies. Works on every platform Ruby supports.
Configuration
Configure once at boot — typically in config/initializers/olyx.rb:
Olyx.configure do |config|
config.api_key = ENV["OLYX_API_KEY"]
config.base_url = ENV["OLYX_GATEWAY_URL"] if ENV["OLYX_GATEWAY_URL"]
config.fail_open = false # fail-closed by default — see Safety Valve
end
Olyx.configure and Olyx.new share the same configuration object.
client.execute — the primary call
In Ruby, each execute call must be bound to a trace:
trace = client.traces.create(metadata: { user_id: "u_123", intent: "translation" })
result = client.execute(
trace_id: trace.id,
input: "Translate to French: Hello, world."
)
Response shape
result.output # model output string | nil
result.model # resolved model identifier | nil
result.step_id # step ID in the trace graph
result.reason # present when blocked
result.status # "tool_calls_pending" | nil
result.tool_calls # array of tool call objects
result.bypass? # true when fail_open bypass path was used
result.blocked? # convenience helper
result.tool_calls_pending? # convenience helper
Simulate / dry-run
Before committing to a call, ask Olyx what it would do — no model is invoked, no cost is incurred:
sim = client.simulate.create(
input: "Summarise this quarterly financial report...",
metadata: { user_id: "u_123", intent: "summarization" }
)
puts sim.status # "resolved" | "blocked" | "unconfigured"
puts sim.model # "gpt-4o-mini" | nil
puts sim.estimated_cost # 0.00045 (USD)
puts sim.risk_score # 0.08 (0–1; above 0.7 routes to Secure tier)
puts sim.tier # "medium"
puts sim.fallback_path # ["gpt-4o-mini", "gpt-4o"]
puts sim.reason # nil unless blocked/unconfigured
Policy hooks
Policy is enforced server-side. In Ruby 0.1.x, client.execute does not currently accept a policy hash.
Attach context (user_id, intent, feature, etc.) on traces.create(metadata: ...) for attribution and governance routing context.
Blocked responses
A blocked response is a governance event, not an exception:
trace = client.traces.create(metadata: { user_id: "u_123" })
result = client.execute(trace_id: trace.id, input: "...")
if result.blocked?
log_governance_event(result.reason, step_id: result.step_id)
else
render json: { output: result.output }
end
Tool calls
Pass tool definitions and Olyx manages the execution loop — schema translation per provider is automatic:
tools = [{
type: "function",
function: {
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}
}]
trace = client.traces.create(metadata: { user_id: "u_123", intent: "weather_lookup" })
result = client.execute(
trace_id: trace.id,
input: "What is the weather in London?",
tools: tools
)
while result.tool_calls_pending?
tool_results = result.tool_calls.map do |call|
output = dispatch_tool(call.name, call.arguments)
{ tool_call_id: call.id, name: call.name, content: output.to_json }
end
result = client.execute(
trace_id: trace.id, # bind to the same trace
parent_step_id: result.step_id,
tool_results: tool_results
)
end
MCP tools in service objects
Initialize the Olyx client once in the constructor and declare MCP servers as private configuration. Each service owns its own MCP scope — the client is reused across calls.
class DocumentService
def initialize(user_id:)
@user_id = user_id
@client = Olyx.new
end
def summarise(document_id:)
trace = @client.traces.create(
metadata: { user_id: @user_id, intent: "summarization" }
)
@client.execute(
trace_id: trace.id,
input: "Summarise document #{document_id}",
tools: mcp_tools
)
end
private
def mcp_tools
[{
type: "mcp",
server_label: "documents",
server_url: ENV["DOCS_MCP_URL"],
require_approval: "never"
}]
end
end
Services with different MCP servers compose naturally — each service brings its own tool scope:
class SearchService
def initialize(user_id:)
@user_id = user_id
@client = Olyx.new
end
def query(q:)
trace = @client.traces.create(
metadata: { user_id: @user_id, intent: "search" }
)
@client.execute(
trace_id: trace.id,
input: q,
tools: mcp_tools
)
end
private
def mcp_tools
[{
type: "mcp",
server_label: "search",
server_url: ENV["SEARCH_MCP_URL"],
require_approval: "never"
}]
end
end
class AnalyticsService
def initialize(user_id:)
@user_id = user_id
@client = Olyx.new
end
def insight(metric:, window:)
trace = @client.traces.create(
metadata: { user_id: @user_id, intent: "analytics" }
)
@client.execute(
trace_id: trace.id,
input: "Explain the trend in #{metric} over the last #{window} days.",
tools: mcp_tools
)
end
private
def mcp_tools
[{
type: "mcp",
server_label: "data_warehouse",
server_url: ENV["DW_MCP_URL"],
require_approval: "never",
vpc_only: true
}]
end
end
vpc_only is a routing intent flag. Entitlement and access control are enforced by the Olyx gateway, not by client-side SDK checks.
In Rails, register these as singletons in config/initializers/olyx.rb so the HTTP connection pool is shared:
Rails.application.config.after_initialize do
Rails.application.config.x.document_service = DocumentService
Rails.application.config.x.search_service = SearchService
Rails.application.config.x.analytics_service = AnalyticsService
end
Then call from controllers:
class ReportsController < ApplicationController
def show
service = AnalyticsService.new(user_id: current_user.id)
result = service.insight(metric: params[:metric], window: 30)
render json: { output: result.output, model: result.model, step_id: result.step_id }
end
end
Multi-step workflows (explicit trace control)
For agentic workflows that span multiple execute calls — where you want the full chain visible as a single trace — bind calls to an explicit trace:
# Create a trace once for the full workflow
trace = client.traces.create(
metadata: { user_id: "u_123", task: "research_report" },
revenue: 2.00 # what you charge for this workflow
)
# Each execute call bound to the same trace
step1 = client.execute(trace_id: trace.id, input: "Find papers on transformer efficiency.")
step2 = client.execute(trace_id: trace.id, input: "Summarise: #{step1.output}")
# Complete triggers grading across all steps
client.traces.complete(trace.id)
# All steps are linked under one trace in the dashboard
puts trace.id
Embeddings
Ruby SDK 0.1.x does not currently expose client.embeddings.create.
Use the OpenAI-compatible gateway path from Quick Start for embeddings:
openai = OpenAI::Client.new(
access_token: ENV["OLYX_API_KEY"],
uri_base: "https://olyx.ai/v1"
)
response = openai.embeddings(
parameters: {
model: "text-embedding-3-small",
input: ["Document one.", "Document two."]
}
)
User retention analytics
Attach user and feature metadata on the trace, then execute against that trace:
trace = client.traces.create(
metadata: {
user_id: "u_123",
org_id: "org_abc",
intent: "email_draft",
feature: "sales_assistant"
}
)
result = client.execute(
trace_id: trace.id,
input: "Draft a follow-up email for this deal."
)
With consistent user_id and intent tagging across calls, the Olyx dashboard surfaces:
- Per-user AI cost — identify your highest-value users and price accordingly
- Feature adoption — which AI surfaces are used vs. ignored
- Optimization grades per cohort — are heavy users getting efficient routing?
- Blocked call rate — flag users hitting safety guardrails repeatedly
Use this signal to inform retention decisions: users with high AI engagement and low block rates are your stickiest users.
The Safety Valve: Fail-Closed vs. Fail-Open
Fail-closed (default): if the gateway is unreachable, execute raises Olyx::CircuitBreakerError.
Fail-open: set config.fail_open = true to call fallback_provider_url directly during outages.
# Per-call override
trace = client.traces.create(metadata: { user_id: "u_123" })
result = client.execute(
trace_id: trace.id,
input: "Summarise this internal changelog.",
fail_open: true
)
puts result.bypass? # true — no audit trail for this call
Testing
Use a dedicated test project with a project-scoped API key. All SDK calls in test mode route to the real Olyx backend — traces are created, executions counted, and policy applied exactly as in production. This makes test-environment behaviour verifiable against real governance rules.
# config/environments/test.rb — or via ENV in CI
Olyx.configure do |config|
config.api_key = ENV.fetch("OLYX_TEST_API_KEY")
config.base_url = ENV.fetch("OLYX_BASE_URL", "https://olyx.ai")
end
Set a spend cap on the test project key to bound runaway test costs. Test traces appear in your dashboard and count against your plan quota — use a separate test project to keep production trace history clean.
Controlling test outcomes
Use client.simulate to exercise your policy logic without invoking a model:
result = client.simulate(input: "What is 2+2?")
# => { status: "resolved", model: "gpt-4o-mini", estimated_cost: 0.00018 }
Use client.checks to test your guardrail logic against specific inputs:
check = client.checks(trace_id: trace.id, input: user_input)
unless check.allowed?
render json: { error: "Request blocked" }, status: :forbidden
end
Offline testing (enterprise)
Enterprise plans include an offline testing flag that enables zero-network test execution. The SDK detects this capability automatically at initialisation via GET /api/v1/sdk/config:
# No additional configuration required — the SDK reads the plan capability flag.
# When offline_testing is enabled for your plan, pass offline: true:
Olyx.configure do |config|
config.api_key = ENV.fetch("OLYX_API_KEY")
config.offline = Rails.env.test? # only resolves if your plan permits it
end
Offline mode returns locally-generated stub responses with the same shape as real responses. No HTTP call is made to the backend; no trace is recorded; no quota is consumed. Use offline mode in CI pipelines with strict egress controls or in air-gapped environments.
If offline: true is set on a plan that does not have the offline_testing feature, the SDK raises Olyx::ConfigurationError rather than silently falling back to online mode.
Error reference
All SDK errors inherit from Olyx::Error and carry .status plus optional .code.
| Class | When raised |
|---|---|
Olyx::AuthError | 401 — missing, revoked, or expired API key |
Olyx::NotFoundError | 404 — resource not found or belongs to another account |
Olyx::ValidationError | 400/422 — request failed validation |
Olyx::RateLimitError | 429 — rate limit or spend cap hit |
Olyx::ServerError | 5xx from the gateway |
Olyx::GatewayError | Network timeout, connection refused, or 5xx — internal, triggers fail-open/closed |
Olyx::CircuitBreakerError | Gateway unreachable and fail_open is false — no ungoverned path to the provider |
Olyx::ConfigurationError | Invalid or missing SDK configuration |
begin
trace = client.traces.create(metadata: { user_id: "u_123" })
result = client.execute(trace_id: trace.id, input: "...")
rescue Olyx::CircuitBreakerError
render json: { error: "AI service temporarily unavailable." }, status: :service_unavailable
rescue Olyx::RateLimitError => e
case e.code
when "CIRCUIT_OPEN" then :reset_key_in_dashboard
when "LOOP_DETECTED" then :investigate_loop_then_reset_key
else :wait_for_window_reset
end
rescue Olyx::AuthError
# Rotate key in Settings → API Keys
end
Private Agent Routes
The Olyx Agent is a lightweight, outbound-only container for selected private beta deployments. Your application points the SDK at an internal hostname, and the agent forwards Olyx requests outbound through your normal network controls.
Use the agent when the hosted gateway cannot reach an internal provider endpoint or when your deployment needs an internal egress point. Most beta teams can start with the hosted gateway and add the agent later.
Start the agent
docker run -d \
--name olyx-agent \
-e OLYX_API_KEY="$OLYX_API_KEY" \
-p 4000:4000 \
olyxlabs/olyx-agent:latest
The agent exposes the same API shape as the hosted gateway. It applies your project-level policy before forwarding requests through the configured outbound path.
Point the SDK at the agent
# config/initializers/olyx.rb
Olyx.configure do |config|
config.api_key = ENV["OLYX_API_KEY"]
config.base_url = "http://olyx-agent:4000" # internal hostname
config.fail_open = false
end
SDK behavior is the same from the application perspective — only base_url changes.
Kubernetes sidecar
Run the agent as a sidecar in the same pod as your application:
# deployment.yaml (relevant section)
containers:
- name: app
image: your-app:latest
env:
- name: OLYX_GATEWAY_URL
value: "http://localhost:4000"
- name: olyx-agent
image: olyxlabs/olyx-agent:latest
env:
- name: OLYX_API_KEY
valueFrom:
secretKeyRef:
name: olyx-secrets
key: api-key
ports:
- containerPort: 4000
Operational behavior
| Behavior | Detail |
|---|---|
| Outbound-first | Designed for deployments where your network initiates connections outward. |
| Credential placement | Keep the Olyx API key in the agent or secret manager rather than hardcoding it in app code. |
| Network visibility | Route Olyx-bound model traffic through infrastructure your team already monitors. |
| Policy path | Project-level routing, cost caps, and PII checks still happen before provider execution. |
| Fail-closed default | If the agent is unreachable, the SDK raises Olyx::CircuitBreakerError unless you explicitly opt into fail-open behavior. |
TLS
If your network terminates TLS at an internal boundary, point the agent at your CA bundle:
docker run -d \
--name olyx-agent \
-e OLYX_API_KEY="$OLYX_API_KEY" \
-v /etc/ssl/internal:/etc/ssl/internal:ro \
-e SSL_CERT_FILE="/etc/ssl/internal/ca-bundle.crt" \
-p 4000:4000 \
olyxlabs/olyx-agent:latest
Verifying connectivity
curl -s http://olyx-agent:4000/up
# → {"status":"ok","version":"1.4.2"}
In Rails, add a startup check:
Rails.application.config.after_initialize do
if Rails.env.production?
Olyx.new.ping!
end
end
Gateway migration through the agent
Existing code using the OpenAI Ruby gem can route through the agent by changing the base URL to your internal agent hostname:
require "openai"
client = OpenAI::Client.new(
access_token: ENV["OLYX_API_KEY"],
uri_base: "http://olyx-agent:4000/v1"
)
# All existing code unchanged — PII scrubbing and routing applied by the agent
response = client.chat(parameters: { model: "gpt-4o", messages: [...] })
Regional routing
If you run services in multiple regions, put regional agent instances behind your own internal routing layer and point
the SDK at that stable base_url. Keep the first beta deployment simple; add regional routing only after trace latency
shows that it matters.