Providers · 04
OpenAI Provider
The OpenAI provider connects to OpenAI’s API for GPT models.
Installation
Add the OpenAI gem to your Gemfile:
gem 'openai'
Configuration
Set your API key globally:
Riffer.configure do |config|
config.openai.api_key = ENV['OPENAI_API_KEY']
end
Both api_key and base_url resolve in order: Riffer.config.openai.* → the OpenAI SDK’s own OPENAI_API_KEY / OPENAI_BASE_URL lookup. Leaving one unset in riffer means the SDK resolves it, so an OPENAI_BASE_URL gateway is honored without any riffer configuration.
For anything beyond the API key — timeouts, retries, proxies — supply your own OpenAI::Client:
Riffer.configure do |config|
config.openai.client = OpenAI::Client.new(
api_key: ENV['OPENAI_API_KEY'],
timeout: 30,
max_retries: 4
)
end
The setting accepts a client instance or a no-argument Proc, resolved on every LLM call — see Configuration → Provider Clients.
For OpenAI-compatible servers (LiteLLM, vLLM, corporate gateways), configure a base_url:
Riffer.configure do |config|
config.openai.api_key = ENV['GATEWAY_KEY']
config.openai.base_url = 'http://localhost:4000/v1'
end
Supported Models
Use any OpenAI model in the openai/model format:
model 'openai/gpt-5.4'
model 'openai/gpt-5-mini'
Model Options
temperature
Controls randomness (0.0-2.0):
model_options temperature: 0.7
max_tokens
Maximum tokens in response:
model_options max_tokens: 4096
reasoning
Enables extended thinking (for supported models):
model_options reasoning: 'medium' # 'low', 'medium', or 'high'
When reasoning is enabled, you’ll receive ReasoningDelta and ReasoningDone events during streaming.
Reasoning Replay
Each reasoning item in a Responses API output becomes reasoning parts on the assistant message, on both generate_text and stream_text, all tagged format: "openai-v1" and carrying the item’s id:
| Reasoning item field | ReasoningPart |
|---|---|
each summary entry |
a :summary part with its text |
each content entry |
a :text part with its text |
encrypted_content |
one :encrypted part with it as data |
The :encrypted part is always present, even when the response carried no encrypted_content, so the item’s id is kept. When streaming, the parts are yielded as ReasoningDone events once the item completes; summary text still arrives as ReasoningDelta events before that.
On the next request, parts sharing an id go back as one reasoning item, ahead of the assistant’s text and function calls, in their original order and unchanged. This lets reasoning models carry their reasoning across turns and tool-call loops. You don’t have to do anything as long as the assistant messages stay in the history; if you persist sessions, keep the reasoning key when you store them.
With store left on (the API default), OpenAI can resolve a replayed item from its id alone. With model_options store: false, the item can only be replayed from its encrypted payload, so request it explicitly:
model_options reasoning: 'medium', store: false, include: ['reasoning.encrypted_content']
Only parts tagged openai-v1 are sent. Reasoning from other providers stays on the messages but is left out, including OpenRouter’s openai-responses-v1 parts, which are encrypted under OpenRouter’s organization rather than yours.
web_search
Enable server-side web search using OpenAI’s web_search_preview tool. Pass true to use defaults or a hash to merge with the tool definition:
# Enable with defaults
model_options web_search: true
# With custom configuration
model_options web_search: {search_context_size: "medium"}
Example
Riffer.configure do |config|
config.openai.api_key = ENV['OPENAI_API_KEY']
end
class CodeReviewAgent < Riffer::Agent
model 'openai/gpt-5-mini'
instructions 'You are a code reviewer. Provide constructive feedback.'
model_options temperature: 0.3, reasoning: 'medium'
end
agent = CodeReviewAgent.new
puts agent.generate("Review this code: def add(a,b); a+b; end")
Streaming
agent.stream("Explain Ruby blocks").each do |event|
case event
when Riffer::StreamEvents::TextDelta
print event.content
when Riffer::StreamEvents::ReasoningDelta
# Extended thinking content
print "[thinking] #{event.content}"
when Riffer::StreamEvents::ReasoningDone
puts "\n[reasoning complete]"
end
end
Tool Calling
OpenAI provider converts tools to function calling format with strict mode:
class CalculatorTool < Riffer::Tool
description "Performs basic math operations"
params do
required :operation, String, enum: ["add", "subtract", "multiply", "divide"]
required :a, Float, description: "First number"
required :b, Float, description: "Second number"
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 'openai/gpt-5-mini'
uses_tools [CalculatorTool]
end
Web Search
Web search allows the model to search the web for up-to-date information. When enabled, the provider injects the web_search_preview tool into the request.
class SearchAgent < Riffer::Agent
model 'openai/gpt-5-mini'
model_options web_search: true
end
agent = SearchAgent.new
agent.stream("What's the latest Ruby release?").each do |event|
case event
when Riffer::StreamEvents::WebSearchStatus
# OpenAI emits a full status sequence:
# "in_progress" → "searching" → "open_page" → "completed"
puts "[search: #{event.status}]"
puts " query: #{event.query}" if event.query
puts " url: #{event.url}" if event.url
when Riffer::StreamEvents::WebSearchDone
puts "[search complete: #{event.query}]"
when Riffer::StreamEvents::TextDelta
print event.content
end
end
Note: OpenAI sources include
urlbut nottitle. Each source in thesourcesarray will havetitle: nil.
Message Format
The provider converts Riffer messages to OpenAI format:
| Riffer Message | OpenAI Role |
|---|---|
System |
developer |
User |
user |
Assistant |
assistant |
Tool |
function_call_output |
Direct Provider Usage
provider = Riffer::Providers::OpenAI.new
response = provider.generate_text(
prompt: "Hello!",
model: "gpt-5-mini",
temperature: 0.7
)
puts response.content