Class: Riffer::Providers::Base
- Inherits:
-
Object
- Object
- Riffer::Providers::Base
- Defined in:
- lib/riffer/providers/base.rb,
sig/generated/riffer/providers/base.rbs
Overview
Base class for all LLM providers. A template-method flow: subclasses implement
the hooks (+build_request_params+, execute_generate, execute_stream,
extract_token_usage, extract_content, extract_tool_calls) and the base
class orchestrates them.
Direct Known Subclasses
Constant Summary collapse
- WIRE_SEPARATOR =
"__"- REQUEST_PARAM_ATTRIBUTES =
A deliberate whitelist — caller options outside it stay off spans.
{ temperature: "gen_ai.request.temperature", max_tokens: "gen_ai.request.max_tokens", max_output_tokens: "gen_ai.request.max_tokens", top_p: "gen_ai.request.top_p", top_k: "gen_ai.request.top_k", frequency_penalty: "gen_ai.request.frequency_penalty", presence_penalty: "gen_ai.request.presence_penalty", seed: "gen_ai.request.seed", stop_sequences: "gen_ai.request.stop_sequences", }.freeze
Class Method Summary collapse
-
.semconv_provider_name ⇒ String
Returns the provider name stamped as gen_ai.provider.name on trace spans, ideally a GenAI semconv well-known value.
-
.skills_adapter(_model = nil) ⇒ singleton(Riffer::Skills::Adapter)
Returns the preferred skill adapter for this provider; override in subclasses (optionally introspecting
model) for provider-specific formats.
Instance Method Summary collapse
-
#apply_pricing(usage) ⇒ Riffer::Providers::TokenUsage
: (Riffer::Providers::TokenUsage) -> Riffer::Providers::TokenUsage.
-
#build_request_params(messages, model, options) ⇒ Hash[Symbol, untyped]
-- : (Array, String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped].
-
#capture_input(span, messages) ⇒ void
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Array) -> void.
-
#capture_messages?(span) ⇒ Boolean
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> bool.
-
#capture_output(span, content:, tool_calls:, finish_reason:) ⇒ void
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), content: String?, tool_calls: Array, finish_reason: Symbol?) -> void.
-
#chat_span_attributes(model, options) ⇒ Hash[String, untyped]
-- : (String?, Hash[Symbol, untyped]) -> Hash[String, untyped].
-
#decode_tool_name(wire_name, tools:) ⇒ String
-- : (String, tools: Array) -> String.
-
#depends_on(gem_name) ⇒ true
: (String) -> true.
-
#encode_tool_name(name) ⇒ String
-- : (String) -> String.
-
#execute_generate(params) ⇒ Object
-- : (Hash[Symbol, untyped]) -> untyped.
-
#execute_stream(params, yielder) ⇒ void
-- : (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void.
-
#extract_content(response) ⇒ String
-- : (untyped) -> String.
-
#extract_finish_reason(_response) ⇒ Riffer::Providers::FinishReason?
Defaults to nil rather than raising — finish reasons are optional, so providers that don't report one stay valid.
-
#extract_token_usage(response) ⇒ Riffer::Providers::TokenUsage?
-- : (untyped) -> Riffer::Providers::TokenUsage?.
-
#extract_tool_calls(response) ⇒ Array[Riffer::Messages::Assistant::ToolCall]
-- : (untyped) -> Array.
-
#generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Riffer::Messages::Assistant
Generates text using the provider.
-
#in_chat_span(model, messages, options) ⇒ void
-- : [R] (String?, Array, Hash[Symbol, untyped]) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R.
- #merge_consecutive_messages(messages) ⇒ Array[Riffer::Messages::Base]
-
#normalize_messages(prompt:, system:, messages:, files: nil) ⇒ Array[Riffer::Messages::Base]
-- : (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array.
-
#parse_structured_output(content) ⇒ Hash[Symbol, untyped]?
-- : (String) -> Hash[Symbol, untyped]?.
-
#parse_tool_arguments(arguments) ⇒ Hash[String, untyped]
-- : ((String | Hash[String, untyped])?) -> Hash[String, untyped].
-
#pricing_rates ⇒ Riffer::Config::Pricing::Rates?
-- : () -> Riffer::Config::Pricing::Rates?.
-
#record_finish_reason(span, reason, raw) ⇒ void
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Symbol?, String?) -> void.
-
#record_stream_outcome(span, recorder) ⇒ void
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tracing::StreamRecorder) -> void.
-
#stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Enumerator[Riffer::StreamEvents::Base, void]
Streams text from the provider.
-
#tag_attributes(tags) ⇒ Hash[String, String]
Maps normalized tags to their namespaced span attribute form.
-
#validate_input!(prompt:, system:, messages:) ⇒ void
-- : (prompt: String?, system: String?, messages: Array[Hash[Symbol | String, untyped] | Riffer::Messages::Base]?) -> void.
-
#validate_normalized_messages!(messages) ⇒ void
-- : (Array) -> void.
-
#yield_finish_reason(yielder, finish_reason) ⇒ void
-- : (Riffer::Providers::_EventSink, Riffer::Providers::FinishReason?) -> void.
Class Method Details
.semconv_provider_name ⇒ String
Returns the provider name stamped as gen_ai.provider.name on trace spans, ideally a GenAI semconv well-known value. Defaults to the snake_cased class name rather than raising like the abstract provider methods, so enabling tracing never breaks an otherwise-working custom provider.
: () -> String
30 31 32 33 34 35 |
# File 'lib/riffer/providers/base.rb', line 30 def self.semconv_provider_name class_name = name return "unknown" unless class_name Riffer::Helpers::ClassNameConverter.convert(class_name.split("::").last.to_s) end |
.skills_adapter(_model = nil) ⇒ singleton(Riffer::Skills::Adapter)
Returns the preferred skill adapter for this provider; override in
subclasses (optionally introspecting model) for provider-specific formats.
: (?String?) -> singleton(Riffer::Skills::Adapter)
20 21 22 |
# File 'lib/riffer/providers/base.rb', line 20 def self.skills_adapter(_model = nil) Riffer::Skills::MarkdownAdapter end |
Instance Method Details
#apply_pricing(usage) ⇒ Riffer::Providers::TokenUsage
: (Riffer::Providers::TokenUsage) -> Riffer::Providers::TokenUsage
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/riffer/providers/base.rb', line 146 def apply_pricing(usage) rates = pricing_rates return usage unless rates cost = rates.cost_for( input_tokens: usage.input_tokens, output_tokens: usage.output_tokens, cache_read_tokens: usage.cache_read_tokens, cache_write_tokens: usage.cache_write_tokens, ) Riffer::Providers::TokenUsage.new( input_tokens: usage.input_tokens, output_tokens: usage.output_tokens, cache_write_tokens: usage.cache_write_tokens, cache_read_tokens: usage.cache_read_tokens, cost: cost, ) end |
#build_request_params(messages, model, options) ⇒ Hash[Symbol, untyped]
-- : (Array, String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
123 124 125 |
# File 'lib/riffer/providers/base.rb', line 123 def build_request_params(, model, ) raise NotImplementedError, "Subclasses must implement #build_request_params" end |
#capture_input(span, messages) ⇒ void
This method returns an undefined value.
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Array) -> void
283 284 285 286 287 288 289 |
# File 'lib/riffer/providers/base.rb', line 283 def capture_input(span, ) return unless (span) span.set_attribute("gen_ai.input.messages", Riffer::Tracing::Capture.()) system_instructions = Riffer::Tracing::Capture.system_instructions() span.set_attribute("gen_ai.system_instructions", system_instructions) if system_instructions end |
#capture_messages?(span) ⇒ Boolean
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> bool
308 309 310 |
# File 'lib/riffer/providers/base.rb', line 308 def (span) Riffer.config.tracing. && span.recording? end |
#capture_output(span, content:, tool_calls:, finish_reason:) ⇒ void
This method returns an undefined value.
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), content: String?, tool_calls: Array, finish_reason: Symbol?) -> void
293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/riffer/providers/base.rb', line 293 def capture_output(span, content:, tool_calls:, finish_reason:) return unless (span) span.set_attribute( "gen_ai.output.messages", Riffer::Tracing::Capture.( content: content, tool_calls: tool_calls, finish_reason: finish_reason, ), ) end |
#chat_span_attributes(model, options) ⇒ Hash[String, untyped]
-- : (String?, Hash[Symbol, untyped]) -> Hash[String, untyped]
234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/riffer/providers/base.rb', line 234 def chat_span_attributes(model, ) attributes = { "gen_ai.operation.name" => "chat", "gen_ai.provider.name" => self.class.semconv_provider_name, } #: Hash[String, untyped] attributes["gen_ai.request.model"] = model if model REQUEST_PARAM_ATTRIBUTES.each do |key, attribute| value = [key] attributes[attribute] = value if value end attributes.merge(tag_attributes([:tags] || {})) end |
#decode_tool_name(wire_name, tools:) ⇒ String
-- : (String, tools: Array) -> String
116 117 118 119 |
# File 'lib/riffer/providers/base.rb', line 116 def decode_tool_name(wire_name, tools:) tool = tools.find { |t| encode_tool_name(t.name) == wire_name } tool ? tool.name : wire_name end |
#depends_on(gem_name) ⇒ true
: (String) -> true
104 105 106 |
# File 'lib/riffer/providers/base.rb', line 104 def depends_on(gem_name) Riffer::Helpers::Dependencies.depends_on(gem_name) end |
#encode_tool_name(name) ⇒ String
-- : (String) -> String
110 111 112 |
# File 'lib/riffer/providers/base.rb', line 110 def encode_tool_name(name) name.gsub("/", WIRE_SEPARATOR) end |
#execute_generate(params) ⇒ Object
-- : (Hash[Symbol, untyped]) -> untyped
129 130 131 |
# File 'lib/riffer/providers/base.rb', line 129 def execute_generate(params) raise NotImplementedError, "Subclasses must implement #execute_generate" end |
#execute_stream(params, yielder) ⇒ void
This method returns an undefined value.
-- : (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
135 136 137 |
# File 'lib/riffer/providers/base.rb', line 135 def execute_stream(params, yielder) raise NotImplementedError, "Subclasses must implement #execute_stream" end |
#extract_content(response) ⇒ String
-- : (untyped) -> String
190 191 192 |
# File 'lib/riffer/providers/base.rb', line 190 def extract_content(response) raise NotImplementedError, "Subclasses must implement #extract_content" end |
#extract_finish_reason(_response) ⇒ Riffer::Providers::FinishReason?
Defaults to nil rather than raising — finish reasons are optional, so providers that don't report one stay valid.
: (untyped) -> Riffer::Providers::FinishReason?
184 185 186 |
# File 'lib/riffer/providers/base.rb', line 184 def extract_finish_reason(_response) nil end |
#extract_token_usage(response) ⇒ Riffer::Providers::TokenUsage?
-- : (untyped) -> Riffer::Providers::TokenUsage?
141 142 143 |
# File 'lib/riffer/providers/base.rb', line 141 def extract_token_usage(response) raise NotImplementedError, "Subclasses must implement #extract_token_usage" end |
#extract_tool_calls(response) ⇒ Array[Riffer::Messages::Assistant::ToolCall]
-- : (untyped) -> Array
196 197 198 |
# File 'lib/riffer/providers/base.rb', line 196 def extract_tool_calls(response) raise NotImplementedError, "Subclasses must implement #extract_tool_calls" end |
#generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Riffer::Messages::Assistant
Generates text using the provider.
-- : (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/riffer/providers/base.rb', line 41 def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **) validate_input!(prompt: prompt, system: system, messages: ) @current_tools = [:tools] || [] #: Array[singleton(Riffer::Tool)] @current_model = model = (prompt: prompt, system: system, messages: , files: files) () = () params = build_request_params(, model, ) in_chat_span(model, , ) do |span| response = execute_generate(params) content = extract_content(response) tool_calls = extract_tool_calls(response) token_usage = extract_token_usage(response) finish_reason = extract_finish_reason(response) structured_output = parse_structured_output(content) if [:structured_output] && tool_calls.empty? Riffer::Tracing.record_usage(span, token_usage) record_finish_reason(span, finish_reason&.reason, finish_reason&.raw) capture_output(span, content: content, tool_calls: tool_calls, finish_reason: finish_reason&.reason) Riffer::Messages::Assistant.new( content, tool_calls: tool_calls, token_usage: token_usage, structured_output: structured_output, finish_reason: finish_reason&.reason, ) end end |
#in_chat_span(model, messages, options) ⇒ void
This method returns an undefined value.
-- : [R] (String?, Array, Hash[Symbol, untyped]) { (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span) -> R } -> R
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/riffer/providers/base.rb', line 216 def in_chat_span(model, , ) Riffer::Tracing.in_span( model ? "chat #{model}" : "chat", attributes: chat_span_attributes(model, ), kind: :client, ) do |span| capture_input(span, ) yield span rescue StandardError => e # The backend records the exception and error status on the re-raise; # error.type is the one semconv attribute it doesn't set. span.set_attribute("error.type", e.class.name) raise end end |
#merge_consecutive_messages(messages) ⇒ Array[Riffer::Messages::Base]
341 342 343 344 345 346 347 |
# File 'lib/riffer/providers/base.rb', line 341 def () .chunk(&:role).flat_map do |role, group| next group if role == :tool || group.size == 1 group.inject { |merged, msg| merged + msg } end end |
#normalize_messages(prompt:, system:, messages:, files: nil) ⇒ Array[Riffer::Messages::Base]
-- : (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array
362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/riffer/providers/base.rb', line 362 def (prompt:, system:, messages:, files: nil) if && files && !files.empty? raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead" end return .map { |msg| Riffer::Messages::Base.from_hash(msg) } if result = [] #: Array[Riffer::Messages::Base] result << Riffer::Messages::System.new(system) if system file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) } prompt_text = prompt #: String result << Riffer::Messages::User.new(prompt_text, files: file_parts) result end |
#parse_structured_output(content) ⇒ Hash[Symbol, untyped]?
-- : (String) -> Hash[Symbol, untyped]?
325 326 327 328 329 |
# File 'lib/riffer/providers/base.rb', line 325 def parse_structured_output(content) JSON.parse(content, symbolize_names: true) rescue JSON::ParserError nil end |
#parse_tool_arguments(arguments) ⇒ Hash[String, untyped]
-- : ((String | Hash[String, untyped])?) -> Hash[String, untyped]
333 334 335 336 337 |
# File 'lib/riffer/providers/base.rb', line 333 def parse_tool_arguments(arguments) return {} if arguments.nil? || arguments.empty? arguments.is_a?(String) ? JSON.parse(arguments) : arguments end |
#pricing_rates ⇒ Riffer::Config::Pricing::Rates?
-- : () -> Riffer::Config::Pricing::Rates?
167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/riffer/providers/base.rb', line 167 def pricing_rates model = @current_model return nil unless model pricing = Riffer.config.pricing return nil if pricing.empty? key = Riffer::Providers::Repository.key_for(self.class) return nil unless key pricing.rates_for("#{key}/#{model}") end |
#record_finish_reason(span, reason, raw) ⇒ void
This method returns an undefined value.
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Symbol?, String?) -> void
259 260 261 262 263 264 |
# File 'lib/riffer/providers/base.rb', line 259 def record_finish_reason(span, reason, raw) return unless reason span.set_attribute("gen_ai.response.finish_reasons", [reason.to_s]) span.set_attribute("riffer.finish_reason.raw", raw) if raw && raw != reason.to_s end |
#record_stream_outcome(span, recorder) ⇒ void
This method returns an undefined value.
-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tracing::StreamRecorder) -> void
268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/riffer/providers/base.rb', line 268 def record_stream_outcome(span, recorder) Riffer::Tracing.record_usage(span, recorder.token_usage) record_finish_reason(span, recorder.finish_reason, recorder.raw_finish_reason) time_to_first_chunk = recorder.time_to_first_chunk span.set_attribute("gen_ai.response.time_to_first_chunk", time_to_first_chunk) if time_to_first_chunk capture_output( span, content: recorder.content, tool_calls: recorder.tool_calls, finish_reason: recorder.finish_reason, ) end |
#stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Enumerator[Riffer::StreamEvents::Base, void]
Streams text from the provider.
-- : (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/riffer/providers/base.rb', line 77 def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **) validate_input!(prompt: prompt, system: system, messages: ) @current_tools = [:tools] || [] #: Array[singleton(Riffer::Tool)] @current_model = model = (prompt: prompt, system: system, messages: , files: files) () = () params = build_request_params(, model, ) # The enumerator body runs in its own fiber, where the fiber-local OTEL # context is empty — capture here so the chat span parents to the caller's # trace. trace_context = Riffer::Tracing.current_context Enumerator.new do |yielder| Riffer::Tracing.with_context(trace_context) do in_chat_span(model, , ) do |span| sink = span.recording? ? Riffer::Tracing::StreamRecorder.new(yielder) : yielder execute_stream(params, sink) record_stream_outcome(span, sink) if sink.is_a?(Riffer::Tracing::StreamRecorder) end end end end |
#tag_attributes(tags) ⇒ Hash[String, String]
Maps normalized tags to their namespaced span attribute form. An empty map yields an empty hash, so merging it is a no-op.
: (Hash[String, String]) -> Hash[String, String]
253 254 255 |
# File 'lib/riffer/providers/base.rb', line 253 def tag_attributes() .transform_keys { |key| "riffer.tag.#{key}" } end |
#validate_input!(prompt:, system:, messages:) ⇒ void
This method returns an undefined value.
-- : (prompt: String?, system: String?, messages: Array[Hash[Symbol | String, untyped] | Riffer::Messages::Base]?) -> void
351 352 353 354 355 356 357 358 |
# File 'lib/riffer/providers/base.rb', line 351 def validate_input!(prompt:, system:, messages:) if .nil? raise Riffer::ArgumentError, "prompt is required when messages is not provided" if prompt.nil? else raise Riffer::ArgumentError, "cannot provide both prompt and messages" if prompt raise Riffer::ArgumentError, "cannot provide both system and messages" if system end end |
#validate_normalized_messages!(messages) ⇒ void
This method returns an undefined value.
-- : (Array) -> void
379 380 381 382 |
# File 'lib/riffer/providers/base.rb', line 379 def () has_user = .any?(Riffer::Messages::User) raise Riffer::ArgumentError, "messages must include at least one user message" unless has_user end |
#yield_finish_reason(yielder, finish_reason) ⇒ void
This method returns an undefined value.
-- : (Riffer::Providers::_EventSink, Riffer::Providers::FinishReason?) -> void
314 315 316 317 318 319 320 321 |
# File 'lib/riffer/providers/base.rb', line 314 def yield_finish_reason(yielder, finish_reason) return unless finish_reason yielder << Riffer::StreamEvents::FinishReasonDone.new( finish_reason: finish_reason.reason, raw_finish_reason: finish_reason.raw, ) end |