riffer 0.37.1 → 0.38.1
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 +14 -0
- data/docs/16_TRACING.md +1 -0
- data/lib/riffer/agent/run.rb +7 -3
- data/lib/riffer/providers/amazon_bedrock.rb +9 -4
- data/lib/riffer/providers/anthropic.rb +13 -8
- data/lib/riffer/providers/base.rb +2 -0
- data/lib/riffer/tracing/stream_recorder.rb +9 -2
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/tracing/stream_recorder.rbs +8 -2
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81a97541ff82791dbf24d5bcad216678db7db1262d3f0e18e4dfe18d77169b20
|
|
4
|
+
data.tar.gz: 294d8493c2e6349e6005c190deb15c7b56c1d1cfc49bcdafa2d99ea39fb4b1e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7004eefa26eaa6bd7a02ff31ae04a4596c063f591b0375f7089e8f39037254945b32a5e57ef6595ddc3f0845173b07d5e020b01c553b573df2a9b535f21d15a2
|
|
7
|
+
data.tar.gz: c0444136536b7034241212e9bca19ad634c98080a1491af4cb32e51a2dcecf0566dcc0e5cd798e2fd433a0bebf1aff02fee46f231454500afaa1a8fb7e5675a3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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.38.1](https://github.com/janeapp/riffer/compare/riffer/v0.38.0...riffer/v0.38.1) (2026-07-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance Improvements
|
|
12
|
+
|
|
13
|
+
* avoid O(n^2) text accumulation in streaming providers ([#378](https://github.com/janeapp/riffer/issues/378)) ([d344fa7](https://github.com/janeapp/riffer/commit/d344fa7122492d14ee1c91c0be3a88501170e255))
|
|
14
|
+
|
|
15
|
+
## [0.38.0](https://github.com/janeapp/riffer/compare/riffer/v0.37.1...riffer/v0.38.0) (2026-07-21)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* record time to first chunk on streaming chat spans ([#369](https://github.com/janeapp/riffer/issues/369)) ([133b019](https://github.com/janeapp/riffer/commit/133b0197dd358ae1cd37adbeb7b5df7446b0cbde))
|
|
21
|
+
|
|
8
22
|
## [0.37.1](https://github.com/janeapp/riffer/compare/riffer/v0.37.0...riffer/v0.37.1) (2026-07-13)
|
|
9
23
|
|
|
10
24
|
|
data/docs/16_TRACING.md
CHANGED
|
@@ -147,6 +147,7 @@ Usage on this span is the run total, aggregated across every step. See [Token us
|
|
|
147
147
|
| `riffer.cost` | float | When the call's model was priced |
|
|
148
148
|
| `gen_ai.response.finish_reasons` | string[] | When the provider reported a finish reason |
|
|
149
149
|
| `riffer.finish_reason.raw` | string | When the raw value differs from the normalized one |
|
|
150
|
+
| `gen_ai.response.time_to_first_chunk` | float | Streaming only — seconds from request issuance to the first stream event |
|
|
150
151
|
| `gen_ai.input.messages` | string | When `capture_messages` is on (JSON; see [capture](#message-content-capture)) |
|
|
151
152
|
| `gen_ai.system_instructions` | string | When `capture_messages` is on and a system prompt exists |
|
|
152
153
|
| `gen_ai.output.messages` | string | When `capture_messages` is on (JSON) |
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -121,7 +121,7 @@ module Riffer::Agent::Run
|
|
|
121
121
|
#--
|
|
122
122
|
#: (Riffer::Agent, Enumerator::Yielder, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
123
123
|
def accumulate_streamed_response(agent, stream_yielder, tags = {})
|
|
124
|
-
accumulated_content = ""
|
|
124
|
+
accumulated_content = +""
|
|
125
125
|
accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
126
126
|
accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
|
|
127
127
|
accumulated_finish_reason = nil #: Symbol?
|
|
@@ -131,9 +131,13 @@ module Riffer::Agent::Run
|
|
|
131
131
|
|
|
132
132
|
case event
|
|
133
133
|
when Riffer::StreamEvents::TextDelta
|
|
134
|
-
|
|
134
|
+
# Append in place rather than += (which reallocates and copies the whole
|
|
135
|
+
# buffer per delta, O(n^2) over a stream). accumulated_content stays an
|
|
136
|
+
# owned buffer; replace (not =) on TextDone keeps it that way so a later
|
|
137
|
+
# delta's << can never mutate the string held by a TextDone event.
|
|
138
|
+
accumulated_content << event.content
|
|
135
139
|
when Riffer::StreamEvents::TextDone
|
|
136
|
-
accumulated_content
|
|
140
|
+
accumulated_content.replace(event.content)
|
|
137
141
|
when Riffer::StreamEvents::ToolCallDone
|
|
138
142
|
accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
139
143
|
call_id: event.call_id,
|
|
@@ -267,7 +267,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
267
267
|
state[:tool_call] = {
|
|
268
268
|
id: typed_event.start.tool_use.tool_use_id,
|
|
269
269
|
name: decode_tool_name(typed_event.start.tool_use.name, tools: @current_tools),
|
|
270
|
-
arguments: ""
|
|
270
|
+
arguments: +""
|
|
271
271
|
}
|
|
272
272
|
end
|
|
273
273
|
|
|
@@ -276,8 +276,13 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
276
276
|
def handle_content_block_delta_text_delta(event, state:, yielder:)
|
|
277
277
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
278
278
|
delta_text = typed_event.delta.text
|
|
279
|
-
|
|
280
|
-
state[:text]
|
|
279
|
+
# Mutating append: += would reallocate and copy the whole accumulated
|
|
280
|
+
# buffer on every delta (O(n^2) per content block). state[:text] is handed
|
|
281
|
+
# off to TextDone and then cleared on block stop, so nothing reads the
|
|
282
|
+
# pre-append string, making in-place mutation safe. Seed with an unfrozen
|
|
283
|
+
# String (+"") so << does not raise under frozen_string_literal.
|
|
284
|
+
state[:text] ||= +""
|
|
285
|
+
state[:text] << delta_text
|
|
281
286
|
yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
|
|
282
287
|
end
|
|
283
288
|
|
|
@@ -287,7 +292,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
287
292
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
288
293
|
input_delta = typed_event.delta.tool_use.input
|
|
289
294
|
|
|
290
|
-
state[:tool_call][:arguments]
|
|
295
|
+
state[:tool_call][:arguments] << input_delta
|
|
291
296
|
|
|
292
297
|
yielder << Riffer::StreamEvents::ToolCallDelta.new(
|
|
293
298
|
item_id: state[:tool_call][:id],
|
|
@@ -233,7 +233,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
233
233
|
content_block = event.content_block
|
|
234
234
|
if content_block.type.to_s == "server_tool_use" && content_block.name.to_s == "web_search"
|
|
235
235
|
state[:web_search_index] = event.index
|
|
236
|
-
state[:web_search_json] = ""
|
|
236
|
+
state[:web_search_json] = +""
|
|
237
237
|
end
|
|
238
238
|
end
|
|
239
239
|
|
|
@@ -243,22 +243,27 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
243
243
|
return unless state[:web_search_index] == event.index
|
|
244
244
|
|
|
245
245
|
delta = event.delta
|
|
246
|
-
state[:web_search_json]
|
|
246
|
+
state[:web_search_json] << delta.partial_json if delta.respond_to?(:partial_json)
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
#--
|
|
250
250
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
251
251
|
def handle_text_event(event, state:, yielder:)
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
# Mutating append instead of += to avoid reallocating and copying the whole
|
|
253
|
+
# accumulated buffer on every delta (O(n^2) per content block). The buffer
|
|
254
|
+
# is handed to TextDone and cleared on block stop, so no reader observes the
|
|
255
|
+
# pre-append string. Seed with an unfrozen String so << is legal under
|
|
256
|
+
# frozen_string_literal.
|
|
257
|
+
state[:text] ||= +""
|
|
258
|
+
state[:text] << event.text
|
|
254
259
|
yielder << Riffer::StreamEvents::TextDelta.new(event.text)
|
|
255
260
|
end
|
|
256
261
|
|
|
257
262
|
#--
|
|
258
263
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
259
264
|
def handle_thinking_event(event, state:, yielder:)
|
|
260
|
-
state[:reasoning] ||= ""
|
|
261
|
-
state[:reasoning]
|
|
265
|
+
state[:reasoning] ||= +""
|
|
266
|
+
state[:reasoning] << event.thinking
|
|
262
267
|
yielder << Riffer::StreamEvents::ReasoningDelta.new(event.thinking)
|
|
263
268
|
end
|
|
264
269
|
|
|
@@ -266,9 +271,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
266
271
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
267
272
|
def handle_input_json_event(event, state:, yielder:)
|
|
268
273
|
if state[:tool_call].nil?
|
|
269
|
-
state[:tool_call] = {id: nil, name: nil, arguments: ""}
|
|
274
|
+
state[:tool_call] = {id: nil, name: nil, arguments: +""}
|
|
270
275
|
end
|
|
271
|
-
state[:tool_call][:arguments]
|
|
276
|
+
state[:tool_call][:arguments] << event.partial_json
|
|
272
277
|
yielder << Riffer::StreamEvents::ToolCallDelta.new(
|
|
273
278
|
item_id: state[:tool_call][:id] || "pending",
|
|
274
279
|
name: state[:tool_call][:name],
|
|
@@ -263,6 +263,8 @@ class Riffer::Providers::Base
|
|
|
263
263
|
def record_stream_outcome(span, recorder)
|
|
264
264
|
Riffer::Tracing.record_usage(span, recorder.token_usage)
|
|
265
265
|
record_finish_reason(span, recorder.finish_reason, recorder.raw_finish_reason)
|
|
266
|
+
time_to_first_chunk = recorder.time_to_first_chunk
|
|
267
|
+
span.set_attribute("gen_ai.response.time_to_first_chunk", time_to_first_chunk) if time_to_first_chunk
|
|
266
268
|
capture_output(span, content: recorder.content, tool_calls: recorder.tool_calls, finish_reason: recorder.finish_reason)
|
|
267
269
|
end
|
|
268
270
|
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
# forwarding every event downstream untouched.
|
|
6
6
|
class Riffer::Tracing::StreamRecorder # :nodoc: all
|
|
7
7
|
# @rbs @yielder: Enumerator::Yielder
|
|
8
|
+
# @rbs @clock: ^() -> Float
|
|
9
|
+
# @rbs @started_at: Float
|
|
8
10
|
|
|
9
11
|
attr_reader :token_usage #: Riffer::Providers::TokenUsage?
|
|
10
12
|
|
|
13
|
+
attr_reader :time_to_first_chunk #: Float?
|
|
14
|
+
|
|
11
15
|
attr_reader :finish_reason #: Symbol?
|
|
12
16
|
|
|
13
17
|
attr_reader :raw_finish_reason #: String?
|
|
@@ -17,15 +21,18 @@ class Riffer::Tracing::StreamRecorder # :nodoc: all
|
|
|
17
21
|
attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
18
22
|
|
|
19
23
|
#--
|
|
20
|
-
#: (Enumerator::Yielder) -> void
|
|
21
|
-
def initialize(yielder)
|
|
24
|
+
#: (Enumerator::Yielder, ?clock: ^() -> Float) -> void
|
|
25
|
+
def initialize(yielder, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
22
26
|
@yielder = yielder
|
|
27
|
+
@clock = clock
|
|
28
|
+
@started_at = clock.call
|
|
23
29
|
@tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
#--
|
|
27
33
|
#: (Riffer::StreamEvents::Base) -> self
|
|
28
34
|
def <<(event)
|
|
35
|
+
@time_to_first_chunk ||= @clock.call - @started_at
|
|
29
36
|
record(event)
|
|
30
37
|
@yielder << event
|
|
31
38
|
self
|
data/lib/riffer/version.rb
CHANGED
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
class Riffer::Tracing::StreamRecorder
|
|
6
6
|
@yielder: Enumerator::Yielder
|
|
7
7
|
|
|
8
|
+
@clock: ^() -> Float
|
|
9
|
+
|
|
10
|
+
@started_at: Float
|
|
11
|
+
|
|
8
12
|
attr_reader token_usage: Riffer::Providers::TokenUsage?
|
|
9
13
|
|
|
14
|
+
attr_reader time_to_first_chunk: Float?
|
|
15
|
+
|
|
10
16
|
attr_reader finish_reason: Symbol?
|
|
11
17
|
|
|
12
18
|
attr_reader raw_finish_reason: String?
|
|
@@ -16,8 +22,8 @@ class Riffer::Tracing::StreamRecorder
|
|
|
16
22
|
attr_reader tool_calls: Array[Riffer::Messages::Assistant::ToolCall]
|
|
17
23
|
|
|
18
24
|
# --
|
|
19
|
-
# : (Enumerator::Yielder) -> void
|
|
20
|
-
def initialize: (Enumerator::Yielder) -> void
|
|
25
|
+
# : (Enumerator::Yielder, ?clock: ^() -> Float) -> void
|
|
26
|
+
def initialize: (Enumerator::Yielder, ?clock: ^() -> Float) -> void
|
|
21
27
|
|
|
22
28
|
# --
|
|
23
29
|
# : (Riffer::StreamEvents::Base) -> self
|
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.38.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - "~>"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: 1.
|
|
38
|
+
version: 1.59.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.59.0
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: aws-sdk-bedrockruntime
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,28 +77,28 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - "~>"
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0
|
|
80
|
+
version: '1.0'
|
|
81
81
|
type: :development
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - "~>"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '0
|
|
87
|
+
version: '1.0'
|
|
88
88
|
- !ruby/object:Gem::Dependency
|
|
89
89
|
name: openai
|
|
90
90
|
requirement: !ruby/object:Gem::Requirement
|
|
91
91
|
requirements:
|
|
92
92
|
- - "~>"
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: 0.
|
|
94
|
+
version: 0.72.0
|
|
95
95
|
type: :development
|
|
96
96
|
prerelease: false
|
|
97
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
98
|
requirements:
|
|
99
99
|
- - "~>"
|
|
100
100
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: 0.
|
|
101
|
+
version: 0.72.0
|
|
102
102
|
- !ruby/object:Gem::Dependency
|
|
103
103
|
name: async
|
|
104
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,7 +108,7 @@ dependencies:
|
|
|
108
108
|
version: '2.25'
|
|
109
109
|
- - "<"
|
|
110
110
|
- !ruby/object:Gem::Version
|
|
111
|
-
version: '2.
|
|
111
|
+
version: '2.44'
|
|
112
112
|
type: :development
|
|
113
113
|
prerelease: false
|
|
114
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -118,7 +118,7 @@ dependencies:
|
|
|
118
118
|
version: '2.25'
|
|
119
119
|
- - "<"
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version: '2.
|
|
121
|
+
version: '2.44'
|
|
122
122
|
- !ruby/object:Gem::Dependency
|
|
123
123
|
name: io-event
|
|
124
124
|
requirement: !ruby/object:Gem::Requirement
|