Class: Terret::OpenRouter::Accumulator

Inherits:
Object
  • Object
show all
Defined in:
lib/terret/openrouter/accumulator.rb

Overview

Accumulates parsed streaming chunks into vocabulary StreamEvents and the final assistant Message. Tool-call argument fragments are gathered per index; the calls close on finalize. Pure, no I/O.

Constant Summary collapse

STOP_REASONS =
{
  "stop" => :end_turn, "tool_calls" => :tool_use,
  "length" => :length, "error" => :error
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Accumulator

Returns a new instance of Accumulator.



18
19
20
21
22
23
# File 'lib/terret/openrouter/accumulator.rb', line 18

def initialize
  @text = +""
  @calls = {} # index => { id:, name:, args: +"" }
  @stop = nil
  @error = nil
end

Instance Attribute Details

#error ⇒ Object (readonly)

Returns the value of attribute error.



16
17
18
# File 'lib/terret/openrouter/accumulator.rb', line 16

def error
  @error
end

Instance Method Details

#feed(chunk) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/terret/openrouter/accumulator.rb', line 25

def feed(chunk)
  if (err = chunk[:error])
    @error = LLM::StreamError.new(message: err[:message], code: err[:code])
    yield @error
  end
  if (usage = chunk[:usage])
    yield LLM::Usage.new(prompt_tokens: usage[:prompt_tokens],
                         completion_tokens: usage[:completion_tokens],
                         cost: usage[:cost])
  end
  choice = chunk[:choices]&.first or return
  @stop = choice[:finish_reason] if choice[:finish_reason]
  delta = choice[:delta] or return
  if (text = delta[:content]) && !text.empty?
    @text << text
    yield LLM::TextDelta.new(text: text)
  end
  Array(delta[:tool_calls]).each { |tc| accumulate_call(tc) }
  nil
end

#finalize {|LLM::MessageStop.new(stop_reason: STOP_REASONS.fetch(@stop, :end_turn))| ... } ⇒ Object

Yields:

  • (LLM::MessageStop.new(stop_reason: STOP_REASONS.fetch(@stop, :end_turn)))


46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/terret/openrouter/accumulator.rb', line 46

def finalize
  parts = []
  parts << LLM::Text.new(text: @text) unless @text.empty?
  @calls.keys.sort.each do |index|
    slot = @calls[index]
    args = slot[:args].empty? ? {} : JSON.parse(slot[:args], symbolize_names: true)
    call = LLM::ToolCall.new(id: slot[:id], name: slot[:name], args: args)
    yield LLM::ToolCallEnd.new(tool_call: call)
    parts << call
  end
  yield LLM::MessageStop.new(stop_reason: STOP_REASONS.fetch(@stop, :end_turn))
  LLM::Message.new(role: :assistant, parts: parts)
end