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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +8 -0
- data/docs/03_AGENTS.md +33 -7
- data/docs/05_MESSAGES.md +12 -4
- data/docs/06_STREAM_EVENTS.md +16 -0
- data/lib/riffer/agent.rb +36 -1
- data/lib/riffer/messages/assistant.rb +10 -2
- data/lib/riffer/providers/amazon_bedrock.rb +29 -3
- data/lib/riffer/providers/anthropic.rb +27 -3
- data/lib/riffer/providers/open_ai.rb +23 -3
- data/lib/riffer/providers/test.rb +11 -4
- data/lib/riffer/stream_events/token_usage_done.rb +32 -0
- data/lib/riffer/token_usage.rb +85 -0
- data/lib/riffer/version.rb +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5f2d3838897b17320d17d3cc95b1db37ae8af023d6dcac8c5c9ce0fa5a6e847
|
|
4
|
+
data.tar.gz: e7e75acdd198f147747d06bcaaddeba27f3491bcc12edd2ac0e35f464d47e2f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 557d945daf22b6a45cb838532a5df0c811d2ac73377b2a5c445efb0dc06a8a2e9c234d14691333d699c6d0dddda7299a5e3bb5a676e4ae72d32b1f631c9694b4
|
|
7
|
+
data.tar.gz: aa35cbff64ef8c11e72e76c3c931589e8163f751b96c066cfef74a5f7779fabb8891565c4d215b2ea9b29fca45fb991ebd49260462c203aa9b73328f39d913ca
|
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
|
-
|
|
110
|
+
# Class method (recommended for simple calls)
|
|
111
|
+
response = MyAgent.generate('Hello')
|
|
111
112
|
|
|
112
|
-
#
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
39
|
-
msg.content
|
|
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
|
data/docs/06_STREAM_EVENTS.md
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
data/lib/riffer/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|