Creating Custom Providers
You can create custom providers to connect Riffer to other LLM services.
Basic Structure
Extend Riffer::Providers::Base and implement the five required hook methods:
class Riffer::Providers::MyProvider < Riffer::Providers::Base def initialize(**options) # Initialize your client @api_key = options[:api_key] || ENV['MY_PROVIDER_API_KEY'] @client = MyProviderClient.new(api_key: @api_key) end private # Hook methods (matching base.rb order) def build_request_params(messages, model, options) tools = options[:tools] params = { model: model, messages: convert_messages(messages), **options.except(:tools) } if tools && !tools.empty? params[:tools] = tools.map { |t| convert_tool(t) } end params end def execute_generate(params) @client.generate(**params) end def execute_stream(params, yielder) @client.stream(**params) do |chunk| case chunk.type when :text yielder << Riffer::StreamEvents::TextDelta.new(chunk.content) when :text_done yielder << Riffer::StreamEvents::TextDone.new(chunk.content) when :tool_call yielder << Riffer::StreamEvents::ToolCallDone.new( item_id: chunk.id, call_id: chunk.id, name: chunk.name, arguments: chunk.arguments ) end end end def extract_token_usage(response) usage = response.usage return nil unless usage Riffer::TokenUsage.new( input_tokens: usage.input_tokens, output_tokens: usage.output_tokens ) end def extract_assistant_message(response, token_usage = nil) text = response.text tool_calls = extract_tool_calls(response) Riffer::Messages::Assistant.new( text, tool_calls: tool_calls, token_usage: token_usage ) end # Helper methods (provider-specific) def convert_messages(messages) messages.map do |msg| case msg when Riffer::Messages::System {role: "system", content: msg.content} when Riffer::Messages::User {role: "user", content: msg.content} when Riffer::Messages::Assistant convert_assistant(msg) when Riffer::Messages::Tool {role: "tool", tool_call_id: msg.tool_call_id, content: msg.content} end end end def convert_assistant(msg) {role: "assistant", content: msg.content, tool_calls: msg.tool_calls} end def convert_tool(tool) { name: tool.name, description: tool.description, parameters: tool.parameters_schema } end def extract_tool_calls(response) return [] unless response.tool_calls response.tool_calls.map do |tc| Riffer::Messages::Assistant::ToolCall.new( id: tc.id, call_id: tc.id, name: tc.name, arguments: tc.arguments ) end end end
Using depends_on
For lazy loading of external gems:
class Riffer::Providers::MyProvider < Riffer::Providers::Base def initialize(**options) depends_on "my_provider_gem" # Only loaded when provider is used @client = ::MyProviderGem::Client.new(**options) end end
Registering Your Provider
Add your provider to the repository:
# In lib/riffer/providers/repository.rb or your own code Riffer::Providers::Repository::REPO[:my_provider] = -> { Riffer::Providers::MyProvider }
Or create a custom repository:
module MyApp module Providers def self.find(identifier) case identifier.to_sym when :my_provider Riffer::Providers::MyProvider else Riffer::Providers::Repository.find(identifier) end end end end
Using Your Provider
class MyAgent < Riffer::Agent model 'my_provider/model-name' end
Tool Support
Tools are converted in build_request_params and passed through to both execute_generate and execute_stream:
def build_request_params(messages, model, options) tools = options[:tools] params = { model: model, messages: convert_messages(messages) } if tools && !tools.empty? params[:tools] = tools.map { |t| convert_tool(t) } end params end def convert_tool(tool) { name: tool.name, description: tool.description, parameters: tool.parameters_schema } end
Stream Events
Use the appropriate stream event classes in execute_stream:
# Text streaming Riffer::StreamEvents::TextDelta.new("chunk of text") Riffer::StreamEvents::TextDone.new("complete text") # Tool calls Riffer::StreamEvents::ToolCallDelta.new( item_id: "id", name: "tool_name", arguments_delta: '{"partial":' ) Riffer::StreamEvents::ToolCallDone.new( item_id: "id", call_id: "call_id", name: "tool_name", arguments: '{"complete":"args"}' ) # Reasoning (if supported) Riffer::StreamEvents::ReasoningDelta.new("thinking...") Riffer::StreamEvents::ReasoningDone.new("complete reasoning") # Web search (if supported) Riffer::StreamEvents::WebSearchStatus.new("searching", query: "search query") Riffer::StreamEvents::WebSearchDone.new( "search query", sources: [{title: "Result", url: "https://example.com"}] ) # Token usage (emit at end of stream) Riffer::StreamEvents::TokenUsageDone.new( token_usage: Riffer::TokenUsage.new( input_tokens: 100, output_tokens: 50 ) )
Error Handling
Raise appropriate Riffer errors:
def extract_assistant_message(response, token_usage = nil) content = response.content raise Riffer::Error, "No content returned from provider" if content.nil? || content.empty? Riffer::Messages::Assistant.new(content, token_usage: token_usage) rescue MyProviderGem::AuthError => e raise Riffer::ArgumentError, "Authentication failed: #{e.message}" end
Complete Example
# lib/riffer/providers/my_provider.rb class Riffer::Providers::MyProvider < Riffer::Providers::Base def initialize(**options) depends_on "my_provider_gem" api_key = options[:api_key] || ENV["MY_PROVIDER_API_KEY"] @client = ::MyProviderGem::Client.new(api_key: api_key) end private # Hook methods def build_request_params(messages, model, options) system_message = extract_system(messages) conversation = messages.reject { |m| m.is_a?(Riffer::Messages::System) } tools = options[:tools] params = { model: model, messages: convert_messages(conversation), system: system_message, max_tokens: options[:max_tokens] || 4096, **options.except(:tools, :max_tokens) } if tools && !tools.empty? params[:tools] = tools.map { |t| convert_tool(t) } end params end def execute_generate(params) @client.create(**params) end def execute_stream(params, yielder) accumulated_text = "" @client.stream(**params) do |event| case event.type when :text_delta accumulated_text += event.text yielder << Riffer::StreamEvents::TextDelta.new(event.text) when :message_stop yielder << Riffer::StreamEvents::TextDone.new(accumulated_text) when :usage yielder << Riffer::StreamEvents::TokenUsageDone.new( token_usage: Riffer::TokenUsage.new( input_tokens: event.usage.input_tokens, output_tokens: event.usage.output_tokens ) ) end end end def extract_token_usage(response) usage = response.usage return nil unless usage Riffer::TokenUsage.new( input_tokens: usage.input_tokens, output_tokens: usage.output_tokens ) end def extract_assistant_message(response, token_usage = nil) text = "" tool_calls = [] response.content.each do |block| case block.type when "text" text = block.text when "tool_use" tool_calls << Riffer::Messages::Assistant::ToolCall.new( id: block.id, call_id: block.id, name: block.name, arguments: block.input.to_json ) end end raise Riffer::Error, "No content returned from provider" if text.empty? && tool_calls.empty? Riffer::Messages::Assistant.new(text, tool_calls: tool_calls, token_usage: token_usage) end # Helper methods def extract_system(messages) system_msg = messages.find { |m| m.is_a?(Riffer::Messages::System) } system_msg&.content end def convert_messages(messages) messages.map do |msg| case msg when Riffer::Messages::User {role: "user", content: msg.content} when Riffer::Messages::Assistant {role: "assistant", content: msg.content} when Riffer::Messages::Tool {role: "user", content: [{type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content}]} end end end def convert_tool(tool) { name: tool.name, description: tool.description, input_schema: tool.parameters_schema } end end