riffer 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d44459cec1b14508aac77178786e7230640ad1d455ced72ebda4ff02166283e4
4
- data.tar.gz: 58a86d78ac5025d17245596e5ab0680d740fc0345cc3d51f2e68a65c5f3b8641
3
+ metadata.gz: f5f2d3838897b17320d17d3cc95b1db37ae8af023d6dcac8c5c9ce0fa5a6e847
4
+ data.tar.gz: e7e75acdd198f147747d06bcaaddeba27f3491bcc12edd2ac0e35f464d47e2f0
5
5
  SHA512:
6
- metadata.gz: 9f8816ae7deb524c786afd74096f55bad54102c32d246f706f7bf15ed1c47cb7cd4f2ccb30c05af833b80e4898049febf5df45c98b8085c7b15c0ed71f425c98
7
- data.tar.gz: 4c2627096ae1181b34d710d744ff339a61ebde623c30c6ea1e000ddac82bf9b374c7eb0d23364f7821856b407466332cc26fedca6288e93b23f9785515d220e7
6
+ metadata.gz: 557d945daf22b6a45cb838532a5df0c811d2ac73377b2a5c445efb0dc06a8a2e9c234d14691333d699c6d0dddda7299a5e3bb5a676e4ae72d32b1f631c9694b4
7
+ data.tar.gz: aa35cbff64ef8c11e72e76c3c931589e8163f751b96c066cfef74a5f7779fabb8891565c4d215b2ea9b29fca45fb991ebd49260462c203aa9b73328f39d913ca
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.10.0"
2
+ ".": "0.11.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.11.0](https://github.com/janeapp/riffer/compare/riffer/v0.10.0...riffer/v0.11.0) (2026-02-04)
9
+
10
+
11
+ ### Features
12
+
13
+ * add class methods for generate and stream to Riffer::Agent ([#97](https://github.com/janeapp/riffer/issues/97)) ([597636a](https://github.com/janeapp/riffer/commit/597636aef7498fe34c975522930e3fd0939a2ea0))
14
+ * Add token usage tracking in Riffer::Agent ([#102](https://github.com/janeapp/riffer/pull/102)) ([6044914](https://github.com/janeapp/riffer/commit/60449148074e42a8b36f0b6977be005b06993d9c))
15
+
8
16
  ## [0.10.0](https://github.com/janeapp/riffer/compare/riffer/v0.9.0...riffer/v0.10.0) (2026-01-30)
9
17
 
10
18
 
data/docs/03_AGENTS.md CHANGED
@@ -107,20 +107,24 @@ end
107
107
  Generates a response synchronously:
108
108
 
109
109
  ```ruby
110
- agent = MyAgent.new
110
+ # Class method (recommended for simple calls)
111
+ response = MyAgent.generate('Hello')
111
112
 
112
- # With a string prompt
113
+ # Instance method (when you need message history or callbacks)
114
+ agent = MyAgent.new
115
+ agent.on_message { |msg| log(msg) }
113
116
  response = agent.generate('Hello')
117
+ agent.messages # Access message history
114
118
 
115
119
  # With message objects/hashes
116
- response = agent.generate([
120
+ response = MyAgent.generate([
117
121
  {role: 'user', content: 'Hello'},
118
122
  {role: 'assistant', content: 'Hi there!'},
119
123
  {role: 'user', content: 'How are you?'}
120
124
  ])
121
125
 
122
126
  # With tool context
123
- response = agent.generate('Look up my orders', tool_context: {user_id: 123})
127
+ response = MyAgent.generate('Look up my orders', tool_context: {user_id: 123})
124
128
  ```
125
129
 
126
130
  ### stream
@@ -128,9 +132,8 @@ response = agent.generate('Look up my orders', tool_context: {user_id: 123})
128
132
  Streams a response as an Enumerator:
129
133
 
130
134
  ```ruby
131
- agent = MyAgent.new
132
-
133
- agent.stream('Tell me a story').each do |event|
135
+ # Class method (recommended for simple calls)
136
+ MyAgent.stream('Tell me a story').each do |event|
134
137
  case event
135
138
  when Riffer::StreamEvents::TextDelta
136
139
  print event.content
@@ -140,6 +143,12 @@ agent.stream('Tell me a story').each do |event|
140
143
  puts "[Tool: #{event.name}]"
141
144
  end
142
145
  end
146
+
147
+ # Instance method (when you need message history or callbacks)
148
+ agent = MyAgent.new
149
+ agent.on_message { |msg| persist_message(msg) }
150
+ agent.stream('Tell me a story').each { |event| handle(event) }
151
+ agent.messages # Access message history
143
152
  ```
144
153
 
145
154
  ### messages
@@ -181,6 +190,23 @@ agent
181
190
 
182
191
  Works with both `generate` and `stream`. Only emits agent-generated messages (Assistant, Tool), not inputs (System, User).
183
192
 
193
+ ### token_usage
194
+
195
+ Access cumulative token usage across all LLM calls:
196
+
197
+ ```ruby
198
+ agent = MyAgent.new
199
+ agent.generate("Hello!")
200
+
201
+ if agent.token_usage
202
+ puts "Total tokens: #{agent.token_usage.total_tokens}"
203
+ puts "Input: #{agent.token_usage.input_tokens}"
204
+ puts "Output: #{agent.token_usage.output_tokens}"
205
+ end
206
+ ```
207
+
208
+ Returns `nil` if the provider doesn't report usage, or a `Riffer::TokenUsage` object with accumulated totals.
209
+
184
210
  ## Class Methods
185
211
 
186
212
  ### find
data/docs/05_MESSAGES.md CHANGED
@@ -30,14 +30,15 @@ msg.to_h # => {role: :user, content: "Hello, how are you?"}
30
30
 
31
31
  ### Assistant
32
32
 
33
- Assistant messages represent LLM responses, potentially including tool calls:
33
+ Assistant messages represent LLM responses, potentially including tool calls and token usage data:
34
34
 
35
35
  ```ruby
36
36
  # Text-only response
37
37
  msg = Riffer::Messages::Assistant.new("I'm doing well, thank you!")
38
- msg.role # => :assistant
39
- msg.content # => "I'm doing well, thank you!"
40
- msg.tool_calls # => []
38
+ msg.role # => :assistant
39
+ msg.content # => "I'm doing well, thank you!"
40
+ msg.tool_calls # => []
41
+ msg.token_usage # => nil or Riffer::TokenUsage
41
42
 
42
43
  # Response with tool calls
43
44
  msg = Riffer::Messages::Assistant.new("", tool_calls: [
@@ -45,6 +46,13 @@ msg = Riffer::Messages::Assistant.new("", tool_calls: [
45
46
  ])
46
47
  msg.tool_calls # => [{id: "call_123", ...}]
47
48
  msg.to_h # => {role: "assistant", content: "", tool_calls: [...]}
49
+
50
+ # Accessing token usage data (when available from provider)
51
+ if msg.token_usage
52
+ puts "Input tokens: #{msg.token_usage.input_tokens}"
53
+ puts "Output tokens: #{msg.token_usage.output_tokens}"
54
+ puts "Total tokens: #{msg.token_usage.total_tokens}"
55
+ end
48
56
  ```
49
57
 
50
58
  ### Tool
@@ -109,6 +109,22 @@ event.role # => "assistant"
109
109
  event.content # => "Let me think about this step by step..."
110
110
  ```
111
111
 
112
+ ### TokenUsageDone
113
+
114
+ Emitted when token usage data is available at the end of a response:
115
+
116
+ ```ruby
117
+ event = Riffer::StreamEvents::TokenUsageDone.new(token_usage: token_usage)
118
+ event.role # => :assistant
119
+ event.token_usage # => Riffer::TokenUsage
120
+ event.token_usage.input_tokens # => 100
121
+ event.token_usage.output_tokens # => 50
122
+ event.token_usage.total_tokens # => 150
123
+ event.to_h # => {role: :assistant, token_usage: {input_tokens: 100, output_tokens: 50}}
124
+ ```
125
+
126
+ Use this to track token consumption in real-time during streaming.
127
+
112
128
  ## Streaming with Tools
113
129
 
114
130
  When an agent uses tools during streaming, the flow is:
data/lib/riffer/agent.rb CHANGED
@@ -101,6 +101,20 @@ class Riffer::Agent
101
101
  def all
102
102
  subclasses
103
103
  end
104
+
105
+ # Generates a response using a new agent instance.
106
+ #
107
+ # See #generate for parameters and return value.
108
+ def generate(...)
109
+ new.generate(...)
110
+ end
111
+
112
+ # Streams a response using a new agent instance.
113
+ #
114
+ # See #stream for parameters and return value.
115
+ def stream(...)
116
+ new.stream(...)
117
+ end
104
118
  end
105
119
 
106
120
  # The message history for the agent.
@@ -108,6 +122,11 @@ class Riffer::Agent
108
122
  # Returns Array of Riffer::Messages::Base.
109
123
  attr_reader :messages
110
124
 
125
+ # Cumulative token usage across all LLM calls.
126
+ #
127
+ # Returns Riffer::TokenUsage or nil.
128
+ attr_reader :token_usage
129
+
111
130
  # Initializes a new agent.
112
131
  #
113
132
  # Raises Riffer::ArgumentError if the configured model string is invalid
@@ -115,6 +134,7 @@ class Riffer::Agent
115
134
  def initialize
116
135
  @messages = []
117
136
  @message_callbacks = []
137
+ @token_usage = nil
118
138
  @model_string = self.class.model
119
139
  @instructions_text = self.class.instructions
120
140
 
@@ -140,6 +160,7 @@ class Riffer::Agent
140
160
  loop do
141
161
  response = call_llm
142
162
  add_message(response)
163
+ track_token_usage(response.token_usage)
143
164
 
144
165
  break unless has_tool_calls?(response)
145
166
 
@@ -164,6 +185,7 @@ class Riffer::Agent
164
185
  loop do
165
186
  accumulated_content = ""
166
187
  accumulated_tool_calls = []
188
+ accumulated_token_usage = nil
167
189
  current_tool_call = nil
168
190
 
169
191
  call_llm_stream.each do |event|
@@ -186,11 +208,18 @@ class Riffer::Agent
186
208
  arguments: event.arguments
187
209
  }
188
210
  current_tool_call = nil
211
+ when Riffer::StreamEvents::TokenUsageDone
212
+ accumulated_token_usage = event.token_usage
189
213
  end
190
214
  end
191
215
 
192
- response = Riffer::Messages::Assistant.new(accumulated_content, tool_calls: accumulated_tool_calls)
216
+ response = Riffer::Messages::Assistant.new(
217
+ accumulated_content,
218
+ tool_calls: accumulated_tool_calls,
219
+ token_usage: accumulated_token_usage
220
+ )
193
221
  add_message(response)
222
+ track_token_usage(accumulated_token_usage)
194
223
 
195
224
  break unless has_tool_calls?(response)
196
225
 
@@ -219,6 +248,12 @@ class Riffer::Agent
219
248
  @message_callbacks.each { |callback| callback.call(message) }
220
249
  end
221
250
 
251
+ def track_token_usage(usage)
252
+ return unless usage
253
+
254
+ @token_usage = @token_usage ? @token_usage + usage : usage
255
+ end
256
+
222
257
  def initialize_messages(prompt_or_messages)
223
258
  @messages = []
224
259
  @messages << Riffer::Messages::System.new(@instructions_text) if @instructions_text
@@ -17,13 +17,20 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
17
17
  # Returns Array of Hash.
18
18
  attr_reader :tool_calls
19
19
 
20
+ # Token usage data for this response.
21
+ #
22
+ # Returns Riffer::TokenUsage or nil.
23
+ attr_reader :token_usage
24
+
20
25
  # Creates a new assistant message.
21
26
  #
22
27
  # content:: String - the message content
23
28
  # tool_calls:: Array of Hash - optional tool calls
24
- def initialize(content, tool_calls: [])
29
+ # token_usage:: Riffer::TokenUsage or nil - optional token usage data
30
+ def initialize(content, tool_calls: [], token_usage: nil)
25
31
  super(content)
26
32
  @tool_calls = tool_calls
33
+ @token_usage = token_usage
27
34
  end
28
35
 
29
36
  # Returns :assistant.
@@ -33,10 +40,11 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
33
40
 
34
41
  # Converts the message to a hash.
35
42
  #
36
- # Returns Hash with +:role+, +:content+, and optionally +:tool_calls+.
43
+ # Returns Hash with +:role+, +:content+, and optionally +:tool_calls+ and +:token_usage+.
37
44
  def to_h
38
45
  hash = {role: role, content: content}
39
46
  hash[:tool_calls] = tool_calls unless tool_calls.empty?
47
+ hash[:token_usage] = token_usage.to_h if token_usage
40
48
  hash
41
49
  end
42
50
  end
@@ -51,7 +51,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
51
51
  end
52
52
 
53
53
  response = @client.converse(**params)
54
- extract_assistant_message(response)
54
+ extract_assistant_message(response, extract_token_usage(response))
55
55
  end
56
56
 
57
57
  def perform_stream_text(messages, model:, **options)
@@ -120,6 +120,20 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
120
120
  stream.on_message_stop_event do |_event|
121
121
  yielder << Riffer::StreamEvents::TextDone.new(accumulated_text)
122
122
  end
123
+
124
+ stream.on_metadata_event do |event|
125
+ if event.usage
126
+ usage = event.usage
127
+ yielder << Riffer::StreamEvents::TokenUsageDone.new(
128
+ token_usage: Riffer::TokenUsage.new(
129
+ input_tokens: usage.input_tokens,
130
+ output_tokens: usage.output_tokens,
131
+ cache_creation_tokens: usage.cache_write_input_tokens,
132
+ cache_read_tokens: usage.cache_read_input_tokens
133
+ )
134
+ )
135
+ end
136
+ end
123
137
  end
124
138
  end
125
139
  end
@@ -185,7 +199,19 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
185
199
  arguments.is_a?(String) ? JSON.parse(arguments) : arguments
186
200
  end
187
201
 
188
- def extract_assistant_message(response)
202
+ def extract_token_usage(response)
203
+ usage = response.usage
204
+ return nil unless usage
205
+
206
+ Riffer::TokenUsage.new(
207
+ input_tokens: usage.input_tokens,
208
+ output_tokens: usage.output_tokens,
209
+ cache_creation_tokens: usage.cache_write_input_tokens,
210
+ cache_read_tokens: usage.cache_read_input_tokens
211
+ )
212
+ end
213
+
214
+ def extract_assistant_message(response, token_usage = nil)
189
215
  output = response.output
190
216
  raise Riffer::Error, "No output returned from Bedrock API" if output.nil? || output.message.nil?
191
217
 
@@ -212,7 +238,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
212
238
  raise Riffer::Error, "No content returned from Bedrock API"
213
239
  end
214
240
 
215
- Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls)
241
+ Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls, token_usage: token_usage)
216
242
  end
217
243
 
218
244
  def convert_tool_to_bedrock_format(tool)
@@ -42,7 +42,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
42
42
  end
43
43
 
44
44
  response = @client.messages.create(**params)
45
- extract_assistant_message(response)
45
+ extract_assistant_message(response, extract_token_usage(response))
46
46
  end
47
47
 
48
48
  def perform_stream_text(messages, model:, **options)
@@ -114,6 +114,18 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
114
114
 
115
115
  when Anthropic::Streaming::MessageStopEvent
116
116
  yielder << Riffer::StreamEvents::TextDone.new(accumulated_text)
117
+ # Get final usage from accumulated message
118
+ final_message = stream.accumulated_message
119
+ if final_message&.usage
120
+ usage = final_message.usage
121
+ stream_token_usage = Riffer::TokenUsage.new(
122
+ input_tokens: usage.input_tokens,
123
+ output_tokens: usage.output_tokens,
124
+ cache_creation_tokens: usage.cache_creation_input_tokens,
125
+ cache_read_tokens: usage.cache_read_input_tokens
126
+ )
127
+ yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: stream_token_usage)
128
+ end
117
129
  end
118
130
  end
119
131
  end
@@ -170,7 +182,19 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
170
182
  arguments.is_a?(String) ? JSON.parse(arguments) : arguments
171
183
  end
172
184
 
173
- def extract_assistant_message(response)
185
+ def extract_token_usage(response)
186
+ usage = response.usage
187
+ return nil unless usage
188
+
189
+ Riffer::TokenUsage.new(
190
+ input_tokens: usage.input_tokens,
191
+ output_tokens: usage.output_tokens,
192
+ cache_creation_tokens: usage.cache_creation_input_tokens,
193
+ cache_read_tokens: usage.cache_read_input_tokens
194
+ )
195
+ end
196
+
197
+ def extract_assistant_message(response, token_usage = nil)
174
198
  content_blocks = response.content
175
199
  raise Riffer::Error, "No content returned from Anthropic API" if content_blocks.nil? || content_blocks.empty?
176
200
 
@@ -196,7 +220,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
196
220
  raise Riffer::Error, "No content returned from Anthropic API"
197
221
  end
198
222
 
199
- Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls)
223
+ Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls, token_usage: token_usage)
200
224
  end
201
225
 
202
226
  def convert_tool_to_anthropic_format(tool)
@@ -22,7 +22,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
22
22
  params = build_request_params(messages, model, options)
23
23
  response = @client.responses.create(params)
24
24
 
25
- extract_assistant_message(response.output)
25
+ extract_assistant_message(response.output, extract_token_usage(response))
26
26
  end
27
27
 
28
28
  def perform_stream_text(messages, model:, **options)
@@ -93,7 +93,17 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
93
93
  end
94
94
  end
95
95
 
96
- def extract_assistant_message(output_items)
96
+ def extract_token_usage(response)
97
+ usage = response.usage
98
+ return nil unless usage
99
+
100
+ Riffer::TokenUsage.new(
101
+ input_tokens: usage.input_tokens,
102
+ output_tokens: usage.output_tokens
103
+ )
104
+ end
105
+
106
+ def extract_assistant_message(output_items, token_usage = nil)
97
107
  text_content = ""
98
108
  tool_calls = []
99
109
 
@@ -116,7 +126,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
116
126
  raise Riffer::Error, "No output returned from OpenAI API"
117
127
  end
118
128
 
119
- Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls)
129
+ Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls, token_usage: token_usage)
120
130
  end
121
131
 
122
132
  def process_stream_events(stream, yielder)
@@ -167,6 +177,16 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
167
177
  name: tracked[:name],
168
178
  arguments: event.arguments
169
179
  )
180
+ when :"response.completed"
181
+ usage = event.response&.usage
182
+ if usage
183
+ Riffer::StreamEvents::TokenUsageDone.new(
184
+ token_usage: Riffer::TokenUsage.new(
185
+ input_tokens: usage.input_tokens,
186
+ output_tokens: usage.output_tokens
187
+ )
188
+ )
189
+ end
170
190
  end
171
191
  end
172
192
 
@@ -27,14 +27,15 @@ class Riffer::Providers::Test < Riffer::Providers::Base
27
27
  #
28
28
  # content:: String - the response content
29
29
  # tool_calls:: Array of Hash - optional tool calls to include
30
+ # token_usage:: Riffer::TokenUsage or nil - optional token usage data to include
30
31
  #
31
32
  # Returns void.
32
33
  #
33
34
  # provider.stub_response("Hello")
34
35
  # provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
35
- # provider.stub_response("Final response")
36
+ # provider.stub_response("Final response", token_usage: Riffer::TokenUsage.new(input_tokens: 10, output_tokens: 5))
36
37
  #
37
- def stub_response(content, tool_calls: [])
38
+ def stub_response(content, tool_calls: [], token_usage: nil)
38
39
  formatted_tool_calls = tool_calls.map.with_index do |tc, idx|
39
40
  {
40
41
  id: tc[:id] || "test_id_#{idx}",
@@ -43,7 +44,7 @@ class Riffer::Providers::Test < Riffer::Providers::Base
43
44
  arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
44
45
  }
45
46
  end
46
- @stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls}
47
+ @stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls, token_usage: token_usage}
47
48
  end
48
49
 
49
50
  # Clears all stubbed responses.
@@ -72,7 +73,11 @@ class Riffer::Providers::Test < Riffer::Providers::Base
72
73
  response = next_response
73
74
 
74
75
  if response.is_a?(Hash)
75
- Riffer::Messages::Assistant.new(response[:content], tool_calls: response[:tool_calls] || [])
76
+ Riffer::Messages::Assistant.new(
77
+ response[:content],
78
+ tool_calls: response[:tool_calls] || [],
79
+ token_usage: response[:token_usage]
80
+ )
76
81
  else
77
82
  response
78
83
  end
@@ -84,6 +89,7 @@ class Riffer::Providers::Test < Riffer::Providers::Base
84
89
  Enumerator.new do |yielder|
85
90
  full_content = response[:content] || ""
86
91
  tool_calls = response[:tool_calls] || []
92
+ token_usage = response[:token_usage]
87
93
 
88
94
  unless full_content.empty?
89
95
  content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
@@ -107,6 +113,7 @@ class Riffer::Providers::Test < Riffer::Providers::Base
107
113
  end
108
114
 
109
115
  yielder << Riffer::StreamEvents::TextDone.new(full_content)
116
+ yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: token_usage) if token_usage
110
117
  end
111
118
  end
112
119
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents completion of token usage tracking during streaming.
4
+ #
5
+ # Emitted when the LLM has finished and token usage data is available.
6
+ #
7
+ # event.token_usage.input_tokens # => 100
8
+ # event.token_usage.output_tokens # => 50
9
+ # event.token_usage.total_tokens # => 150
10
+ #
11
+ class Riffer::StreamEvents::TokenUsageDone < Riffer::StreamEvents::Base
12
+ # The token usage data for this response.
13
+ #
14
+ # Returns Riffer::TokenUsage.
15
+ attr_reader :token_usage
16
+
17
+ # Creates a new token usage done event.
18
+ #
19
+ # token_usage:: Riffer::TokenUsage - the token usage data
20
+ # role:: Symbol - the message role (defaults to :assistant)
21
+ def initialize(token_usage:, role: :assistant)
22
+ super(role: role)
23
+ @token_usage = token_usage
24
+ end
25
+
26
+ # Converts the event to a hash.
27
+ #
28
+ # Returns Hash with +:role+ and +:token_usage+ keys.
29
+ def to_h
30
+ {role: @role, token_usage: @token_usage.to_h}
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents token usage data from an LLM API call.
4
+ #
5
+ # Tracks input tokens, output tokens, and optional cache statistics.
6
+ #
7
+ # token_usage = Riffer::TokenUsage.new(input_tokens: 100, output_tokens: 50)
8
+ # token_usage.total_tokens # => 150
9
+ #
10
+ # combined = token_usage1 + token_usage2 # Combine multiple token usage objects
11
+ #
12
+ class Riffer::TokenUsage
13
+ # Number of tokens in the input/prompt.
14
+ #
15
+ # Returns Integer.
16
+ attr_reader :input_tokens
17
+
18
+ # Number of tokens in the output/response.
19
+ #
20
+ # Returns Integer.
21
+ attr_reader :output_tokens
22
+
23
+ # Number of tokens written to cache (Anthropic-specific).
24
+ #
25
+ # Returns Integer or nil.
26
+ attr_reader :cache_creation_tokens
27
+
28
+ # Number of tokens read from cache (Anthropic-specific).
29
+ #
30
+ # Returns Integer or nil.
31
+ attr_reader :cache_read_tokens
32
+
33
+ # Creates a new TokenUsage instance.
34
+ #
35
+ # input_tokens:: Integer - number of input tokens
36
+ # output_tokens:: Integer - number of output tokens
37
+ # cache_creation_tokens:: Integer or nil - tokens written to cache
38
+ # cache_read_tokens:: Integer or nil - tokens read from cache
39
+ def initialize(input_tokens:, output_tokens:, cache_creation_tokens: nil, cache_read_tokens: nil)
40
+ @input_tokens = input_tokens
41
+ @output_tokens = output_tokens
42
+ @cache_creation_tokens = cache_creation_tokens
43
+ @cache_read_tokens = cache_read_tokens
44
+ end
45
+
46
+ # Returns the total number of tokens (input + output).
47
+ #
48
+ # Returns Integer.
49
+ def total_tokens
50
+ input_tokens + output_tokens
51
+ end
52
+
53
+ # Combines two TokenUsage objects for cumulative tracking.
54
+ #
55
+ # other:: Riffer::TokenUsage - another token usage object to combine with
56
+ #
57
+ # Returns Riffer::TokenUsage - a new TokenUsage with summed values.
58
+ def +(other)
59
+ Riffer::TokenUsage.new(
60
+ input_tokens: input_tokens + other.input_tokens,
61
+ output_tokens: output_tokens + other.output_tokens,
62
+ cache_creation_tokens: add_nullable(cache_creation_tokens, other.cache_creation_tokens),
63
+ cache_read_tokens: add_nullable(cache_read_tokens, other.cache_read_tokens)
64
+ )
65
+ end
66
+
67
+ # Converts the token usage to a hash representation.
68
+ #
69
+ # Cache tokens are omitted if nil.
70
+ #
71
+ # Returns Hash.
72
+ def to_h
73
+ hash = {input_tokens: input_tokens, output_tokens: output_tokens}
74
+ hash[:cache_creation_tokens] = cache_creation_tokens if cache_creation_tokens
75
+ hash[:cache_read_tokens] = cache_read_tokens if cache_read_tokens
76
+ hash
77
+ end
78
+
79
+ private
80
+
81
+ def add_nullable(a, b)
82
+ return nil if a.nil? && b.nil?
83
+ (a || 0) + (b || 0)
84
+ end
85
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riffer
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -35,28 +35,28 @@ dependencies:
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: 1.16.3
38
+ version: 1.17.0
39
39
  type: :development
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
- version: 1.16.3
45
+ version: 1.17.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: aws-sdk-bedrockruntime
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  requirements:
50
50
  - - "~>"
51
51
  - !ruby/object:Gem::Version
52
- version: '1.0'
52
+ version: '1.42'
53
53
  type: :development
54
54
  prerelease: false
55
55
  version_requirements: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '1.0'
59
+ version: '1.42'
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: openai
62
62
  requirement: !ruby/object:Gem::Requirement
@@ -208,8 +208,10 @@ files:
208
208
  - lib/riffer/stream_events/reasoning_done.rb
209
209
  - lib/riffer/stream_events/text_delta.rb
210
210
  - lib/riffer/stream_events/text_done.rb
211
+ - lib/riffer/stream_events/token_usage_done.rb
211
212
  - lib/riffer/stream_events/tool_call_delta.rb
212
213
  - lib/riffer/stream_events/tool_call_done.rb
214
+ - lib/riffer/token_usage.rb
213
215
  - lib/riffer/tool.rb
214
216
  - lib/riffer/tools.rb
215
217
  - lib/riffer/tools/param.rb