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.

flowchart LR APP[RUBY APP] GATEWAY[OPENAI-COMPATIBLE GATEWAY] SDK[OLYX SDK] TRACE[TRACE] EXEC[CLIENT EXECUTE] GATE[OLYX GATEWAY OR AGENT] MODEL[MODEL PROVIDER] APP --> GATEWAY --> GATE --> MODEL APP --> SDK --> TRACE --> EXEC --> GATE --> MODEL

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.

ClassWhen raised
Olyx::AuthError401 — missing, revoked, or expired API key
Olyx::NotFoundError404 — resource not found or belongs to another account
Olyx::ValidationError400/422 — request failed validation
Olyx::RateLimitError429 — rate limit or spend cap hit
Olyx::ServerError5xx from the gateway
Olyx::GatewayErrorNetwork timeout, connection refused, or 5xx — internal, triggers fail-open/closed
Olyx::CircuitBreakerErrorGateway unreachable and fail_open is false — no ungoverned path to the provider
Olyx::ConfigurationErrorInvalid 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

BehaviorDetail
Outbound-firstDesigned for deployments where your network initiates connections outward.
Credential placementKeep the Olyx API key in the agent or secret manager rather than hardcoding it in app code.
Network visibilityRoute Olyx-bound model traffic through infrastructure your team already monitors.
Policy pathProject-level routing, cost caps, and PII checks still happen before provider execution.
Fail-closed defaultIf 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.

Was this page helpful?