Class: Riffer::Providers::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/riffer/providers/base.rb

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

AmazonBedrock, Anthropic, Gemini, Mock, OpenAI, OpenRouter

Constant Summary collapse

WIRE_SEPARATOR =
"__"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.semconv_provider_nameObject

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) ⇒ Object

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

#generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Object

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
72
73
# File 'lib/riffer/providers/base.rb', line 41

def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
  validate_input!(prompt: prompt, system: system, messages: messages)
  @current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
  @current_model = model
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
  validate_normalized_messages!(messages)
  messages = merge_consecutive_messages(messages)
  params = build_request_params(messages, model, options)

  in_chat_span(model, messages, options) 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 options[:structured_output] && tool_calls.empty?

    Riffer::Tracing.record_usage(span, token_usage)
    record_token_usage_metric(model, token_usage)
    record_cost_metric(model, 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

#stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options) ⇒ Object

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]



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/riffer/providers/base.rb', line 79

def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
  validate_input!(prompt: prompt, system: system, messages: messages)
  @current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
  @current_model = model
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
  validate_normalized_messages!(messages)
  messages = merge_consecutive_messages(messages)
  params = build_request_params(messages, model, options)

  # 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, messages, options) do |span|
        # The recorder feeds both the span and the token-usage metric, so build
        # it whenever either is live — metrics fire even with tracing off.
        observe = span.recording? || Riffer::Metrics.recording?
        sink = observe ? Riffer::Tracing::StreamRecorder.new(yielder) : yielder
        execute_stream(params, sink)
        if sink.is_a?(Riffer::Tracing::StreamRecorder)
          record_stream_outcome(span, sink)
          record_token_usage_metric(model, sink.token_usage)
          record_cost_metric(model, sink.token_usage)
        end
      end
    end
  end
end