Providers · 07

OpenRouter Provider

The OpenRouter provider connects Riffer to OpenRouter — a unified gateway that exposes hundreds of LLMs from many vendors (Anthropic, OpenAI, Meta, Mistral, DeepSeek, Google, Grok, Qwen, and more) behind a single OpenAI-compatible Chat Completions endpoint.

OpenRouter is useful when you want one credential, one model-string format, and access to models Riffer doesn’t have a direct provider for. It also offers built-in routing, fallback, and prompt transforms.

Note: OpenRouter exposes only the OpenAI Chat Completions API, not the Responses API. That’s why this provider does not subclass Riffer::Providers::OpenAI (which uses Responses). It implements the five hook methods independently against Chat Completions while still sharing the openai Ruby gem.

Installation

Add the OpenAI gem to your Gemfile — OpenRouter reuses it:

gem 'openai'

Configuration

Set your API key globally:

Riffer.configure do |config|
  config.openrouter.api_key = ENV['OPENROUTER_API_KEY']
end

The api_key resolves in order: Riffer.config.openrouter.api_key → ENV['OPENROUTER_API_KEY'].

For anything beyond the API key — timeouts, retries, proxies — supply your own OpenAI::Client pinned to the OpenRouter endpoint:

Riffer.configure do |config|
  config.openrouter.client = OpenAI::Client.new(
    api_key: ENV['OPENROUTER_API_KEY'],
    base_url: 'https://openrouter.ai/api/v1',
    timeout: 60
  )
end

The setting accepts a client instance or a no-argument Proc, resolved on every LLM call — see Configuration → Provider Clients.

Supported Models

Use any OpenRouter model in the openrouter/<openrouter-model-id> format. The OpenRouter model ID is everything after the first slash:

model 'openrouter/anthropic/claude-sonnet-4.6'
model 'openrouter/openai/gpt-4o-mini'
model 'openrouter/meta-llama/llama-3.1-70b-instruct'
model 'openrouter/deepseek/deepseek-r1'
model 'openrouter/mistralai/mixtral-8x22b-instruct'

See OpenRouter’s model catalog for the full list.

Model Options

temperature, max_tokens, top_p, etc.

Standard sampling options pass through to the underlying model:

model_options temperature: 0.5, max_tokens: 2048

reasoning

For reasoning models (DeepSeek R1, OpenAI o-series via OpenRouter, etc.):

model_options reasoning: 'high'  # 'low' | 'medium' | 'high'

Pass a hash for finer control:

model_options reasoning: {effort: 'medium', max_tokens: 5000}

Streaming yields Riffer::StreamEvents::ReasoningDelta and ReasoningDone events when the model returns reasoning content.

provider (routing preferences)

Pin which upstream provider OpenRouter should use, set allow/deny lists, or prefer a sort order:

model_options provider: {
  order: ['anthropic', 'openai'],
  allow_fallbacks: false
}

See OpenRouter’s provider routing docs for the full schema.

models (fallback chain)

If the primary model is unavailable, OpenRouter will try the next one in the list:

model_options models: ['openai/gpt-4o', 'anthropic/claude-sonnet-4.6']

transforms

Prompt transforms applied by OpenRouter (e.g. middle-out auto-truncation):

model_options transforms: ['middle-out']

Example

Riffer.configure do |config|
  config.openrouter.api_key = ENV['OPENROUTER_API_KEY']
end

class TranslateAgent < Riffer::Agent
  model 'openrouter/anthropic/claude-sonnet-4.6'
  instructions 'You translate English to French.'
end

puts TranslateAgent.new.generate('Hello, world!')

Streaming

agent.stream('Explain Ruby blocks').each do |event|
  case event
  when Riffer::StreamEvents::TextDelta
    print event.content
  when Riffer::StreamEvents::ReasoningDelta
    print "[thinking] #{event.content}"
  when Riffer::StreamEvents::TokenUsageDone
    puts "\n[tokens: #{event.token_usage.total_tokens}]"
  end
end

The provider opts into stream_options: {include_usage: true} automatically so TokenUsageDone fires reliably.

Tool Calling

Tools are converted to OpenAI Chat Completions function format. The provider handles tool name encoding/decoding (slashes in tool names are wire-encoded with __) just like the OpenAI and Anthropic providers.

class CalculatorTool < Riffer::Tool
  description 'Performs basic math'
  params do
    required :operation, String, enum: ['add', 'subtract', 'multiply', 'divide']
    required :a, Float
    required :b, Float
  end

  def call(context:, operation:, a:, b:)
    result = case operation
    when 'add' then a + b
    when 'subtract' then a - b
    when 'multiply' then a * b
    when 'divide' then a / b
    end
    text(result.to_s)
  end
end

class MathAgent < Riffer::Agent
  model 'openrouter/openai/gpt-4o-mini'
  uses_tools [CalculatorTool]
end

Reasoning Models

Reasoning models surface their thought process via OpenRouter’s normalised reasoning_details field. Enable it with the reasoning option:

class ThinkAgent < Riffer::Agent
  model 'openrouter/deepseek/deepseek-r1'
  model_options reasoning: 'medium'
end

ThinkAgent.new.stream('What is 2+2? Think step by step.').each do |event|
  case event
  when Riffer::StreamEvents::ReasoningDelta
    print "[reasoning] #{event.content}"
  when Riffer::StreamEvents::TextDelta
    print event.content
  end
end

Reasoning Replay

Each entry in OpenRouter’s reasoning_details becomes a ReasoningPart on the assistant message, on both generate_text and stream_text, with nothing dropped:

reasoning_details field ReasoningPart field
type type: reasoning.text → :text, reasoning.summary → :summary, reasoning.encrypted → :encrypted
text / summary text
data data
signature signature
id id
format format (e.g. "anthropic-claude-v1", "openai-responses-v1", "unknown")

When streaming, OpenRouter splits one block into many fragments that share an index. The provider concatenates their text, summary, and data and keeps the signature, id, and format that arrive along the way, so each block ends as one ReasoningDone part. Each non-empty text or summary fragment is also yielded as a ReasoningDelta. Detail types riffer doesn’t know are skipped.

On the next request, the assistant message’s parts go back as reasoning_details in their original order and unchanged. This is what lets Anthropic and Gemini models continue signed thinking across a tool-call loop, and lets OpenAI models reuse their encrypted reasoning. Following the replay contract, only parts whose format is one OpenRouter documents are sent: unknown, openai-responses-v1, azure-openai-responses-v1, bedrock-openai-responses-v1, bedrock-xai-responses-v1, xai-responses-v1, meta-responses-v1, anthropic-claude-v1, and google-gemini-v1 (listed in Riffer::Providers::OpenRouter::REASONING_FORMATS). Parts with no format, or one produced by another adapter such as mock-v1, are skipped. The index is not stored, since the parts’ order already carries it.

Routing & Fallbacks

Survive an upstream outage by chaining models:

class ResilientAgent < Riffer::Agent
  model 'openrouter/openai/gpt-4o-mini'
  model_options models: [
    'openai/gpt-4o-mini',
    'anthropic/claude-haiku-4.5',
    'google/gemini-flash-1.5'
  ]
end

Pin to a specific upstream when consistency matters:

model_options provider: {order: ['anthropic'], allow_fallbacks: false}

Message Format

Riffer messages convert to Chat Completions roles:

Riffer Message Chat Completions Role
System system
User user
Assistant assistant
Tool tool

User messages with files become multi-part content (image_url for images, file for documents). Assistant tool calls go into a nested tool_calls array on the assistant message.

Limitations (v1)

  • No unified web search. OpenRouter doesn’t expose a cross-vendor web-search tool — capability varies per upstream model.
  • Audio and image generation models are not supported.
  • Responses API features (e.g. OpenAI’s response.id for continuation) are unavailable — OpenRouter implements only Chat Completions.

Direct Provider Usage

provider = Riffer::Providers::OpenRouter.new

response = provider.generate_text(
  prompt: 'Hello!',
  model: 'anthropic/claude-sonnet-4.6',
  temperature: 0.7
)

puts response.content
puts response.token_usage.total_tokens