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
271 272 273 274 275 276 277 |
# File 'lib/riffer/providers/base.rb', line 271 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
289 290 291 |
# File 'lib/riffer/providers/base.rb', line 289 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
281 282 283 284 285 |
# File 'lib/riffer/providers/base.rb', line 281 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]
229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/riffer/providers/base.rb', line 229 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 unless value.nil? 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
215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/riffer/providers/base.rb', line 215 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 => error # 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", error.class.name) raise end end |
#merge_consecutive_messages(messages) ⇒ Array[Riffer::Messages::Base]
318 319 320 321 322 323 324 |
# File 'lib/riffer/providers/base.rb', line 318 def () .chunk { |msg| msg.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
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/riffer/providers/base.rb', line 339 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 if return .map { |msg| Riffer::Messages::Base.from_hash(msg) } end 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]?
303 304 305 306 307 |
# File 'lib/riffer/providers/base.rb', line 303 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]
311 312 313 314 |
# File 'lib/riffer/providers/base.rb', line 311 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
254 255 256 257 258 259 |
# File 'lib/riffer/providers/base.rb', line 254 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
263 264 265 266 267 |
# File 'lib/riffer/providers/base.rb', line 263 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) 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]
248 249 250 |
# File 'lib/riffer/providers/base.rb', line 248 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
328 329 330 331 332 333 334 335 |
# File 'lib/riffer/providers/base.rb', line 328 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" unless prompt.nil? raise Riffer::ArgumentError, "cannot provide both system and messages" unless system.nil? end end |
#validate_normalized_messages!(messages) ⇒ void
This method returns an undefined value.
-- : (Array) -> void
358 359 360 361 |
# File 'lib/riffer/providers/base.rb', line 358 def () has_user = .any? { |msg| msg.is_a?(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
295 296 297 298 299 |
# File 'lib/riffer/providers/base.rb', line 295 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 |