riffer 0.44.0 → 0.45.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 +7 -0
- data/docs/MESSAGES.md +12 -10
- data/docs/TRACING.md +1 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +1 -1
- data/lib/riffer/providers/amazon_bedrock.rb +3 -0
- data/lib/riffer/providers/anthropic.rb +4 -0
- data/lib/riffer/providers/finish_reason.rb +1 -1
- data/lib/riffer/providers/gemini.rb +10 -1
- data/lib/riffer/providers/open_ai.rb +30 -23
- data/lib/riffer/providers/open_router.rb +21 -7
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/providers/open_ai.rbs +8 -3
- data/sig/generated/riffer/providers/open_router.rbs +10 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b166b910624129bd5b64b9443caba3b5c781b691a7453b5640cd132c21cd72c2
|
|
4
|
+
data.tar.gz: d64178e66b49945647393430b57f84a501ec7c0bfbe866b20f6f1b6af44ef570
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36ce62e6539e22a01c5f8523278203b0307a2753c8e3af8e683133b741144417b360721862249d2c64fb118211f134e7b968d681e4c31e20f9ca051768e9aef6
|
|
7
|
+
data.tar.gz: 39ac8834695876cd923e3dffe7065045f7296e418056e18eae9da4811ba04c886b546342a4a4586d92ac529ec456a18e0b272b0598eb70c72371af2dbefebce3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.45.0](https://github.com/janeapp/riffer/compare/riffer/v0.44.0...riffer/v0.45.0) (2026-09-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* expand finish reason vocabulary and close provider gaps ([#419](https://github.com/janeapp/riffer/issues/419)) ([daf91a6](https://github.com/janeapp/riffer/commit/daf91a61956ce8da626a590d30307e11e90d474b))
|
|
14
|
+
|
|
8
15
|
## [0.44.0](https://github.com/janeapp/riffer/compare/riffer/v0.43.0...riffer/v0.44.0) (2026-08-28)
|
|
9
16
|
|
|
10
17
|
|
data/docs/MESSAGES.md
CHANGED
|
@@ -83,16 +83,18 @@ The cache buckets are subsets of `input_tokens`, never additions to it — summi
|
|
|
83
83
|
|
|
84
84
|
`finish_reason` carries the same meaning for every provider — each adapter maps its raw wire value (Anthropic's `end_turn`, OpenAI's response status, Gemini's `STOP`, …) into a normalized vocabulary:
|
|
85
85
|
|
|
86
|
-
| Value
|
|
87
|
-
|
|
|
88
|
-
| `:stop`
|
|
89
|
-
| `:length`
|
|
90
|
-
| `:tool_calls`
|
|
91
|
-
| `:content_filter`
|
|
92
|
-
| `:
|
|
93
|
-
| `:
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
| Value | Meaning |
|
|
87
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| `:stop` | The model finished its turn naturally (or hit a stop sequence). |
|
|
89
|
+
| `:length` | Output was truncated at the max-token limit. |
|
|
90
|
+
| `:tool_calls` | The model stopped to call tools. |
|
|
91
|
+
| `:content_filter` | A provider safety system blocked or cut the response. |
|
|
92
|
+
| `:context_window` | Input plus output hit the model's context window; trim or compact history rather than raising `max_tokens`. |
|
|
93
|
+
| `:malformed_output` | The model emitted output the provider could not parse, such as an invalid tool call; retry or nudge rather than backing off. |
|
|
94
|
+
| `:error` | The provider reported an error finish. |
|
|
95
|
+
| `:other` | A provider-specific value with no normalized equivalent. |
|
|
96
|
+
|
|
97
|
+
`finish_reason` is `nil` when the provider doesn't report one. The provider's raw wire value travels alongside on the `FinishReasonDone` stream event and the `riffer.finish_reason.raw` trace attribute — for OpenRouter that is the upstream model's `native_finish_reason`, and for a failed OpenAI response it is the error code. Use `finish_reason` to detect truncation without parsing provider responses:
|
|
96
98
|
|
|
97
99
|
```ruby
|
|
98
100
|
response = agent.generate("Summarize this document")
|
data/docs/TRACING.md
CHANGED
|
@@ -153,7 +153,7 @@ Usage on this span is the run total, aggregated across every step. See [Token us
|
|
|
153
153
|
| `gen_ai.output.messages` | string | When `capture_messages` is on (JSON) |
|
|
154
154
|
| `error.type` | string | On an unhandled exception |
|
|
155
155
|
|
|
156
|
-
`gen_ai.response.finish_reasons` is an array of exactly one normalized value, from the fixed vocabulary `stop`, `length`, `tool_calls`, `content_filter`, `error`, `other`. When the provider's raw wire value carries more nuance than the normalized one, the raw string is preserved on `riffer.finish_reason.raw`.
|
|
156
|
+
`gen_ai.response.finish_reasons` is an array of exactly one normalized value, from the fixed vocabulary `stop`, `length`, `tool_calls`, `content_filter`, `context_window`, `malformed_output`, `error`, `other`. When the provider's raw wire value carries more nuance than the normalized one, the raw string is preserved on `riffer.finish_reason.raw`.
|
|
157
157
|
|
|
158
158
|
## `execute_tool {tool}` — the tool call span
|
|
159
159
|
|
|
@@ -252,7 +252,7 @@ Riffer::StreamEvents::FinishReasonDone.new(
|
|
|
252
252
|
|
|
253
253
|
## Finish Reasons
|
|
254
254
|
|
|
255
|
-
`Riffer::Providers::FinishReason` is the same kind of normalized contract — map your provider's raw finish/stop value into the vocabulary defined in [Messages — Finish Reasons](../MESSAGES.md#finish-reasons) (`:stop`, `:length`, `:tool_calls`, `:content_filter`, `:error`, `:other`), keeping the raw wire value alongside:
|
|
255
|
+
`Riffer::Providers::FinishReason` is the same kind of normalized contract — map your provider's raw finish/stop value into the vocabulary defined in [Messages — Finish Reasons](../MESSAGES.md#finish-reasons) (`:stop`, `:length`, `:tool_calls`, `:content_filter`, `:context_window`, `:malformed_output`, `:error`, `:other`), keeping the raw wire value alongside:
|
|
256
256
|
|
|
257
257
|
```ruby
|
|
258
258
|
def extract_finish_reason(response)
|
|
@@ -17,6 +17,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
17
17
|
"tool_use" => :tool_calls,
|
|
18
18
|
"guardrail_intervened" => :content_filter,
|
|
19
19
|
"content_filtered" => :content_filter,
|
|
20
|
+
"malformed_model_output" => :malformed_output,
|
|
21
|
+
"malformed_tool_use" => :malformed_output,
|
|
22
|
+
"model_context_window_exceeded" => :context_window,
|
|
20
23
|
}.freeze #: Hash[String, Symbol]
|
|
21
24
|
|
|
22
25
|
# Returns the skill adapter for the Bedrock model — XML for Anthropic models
|
|
@@ -12,6 +12,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
12
12
|
"max_tokens" => :length,
|
|
13
13
|
"tool_use" => :tool_calls,
|
|
14
14
|
"refusal" => :content_filter,
|
|
15
|
+
"model_context_window_exceeded" => :context_window,
|
|
16
|
+
# A paused server-tool turn resumes only by re-sending the response; the
|
|
17
|
+
# agent loop does not do that, so it has no normalized equivalent.
|
|
18
|
+
"pause_turn" => :other,
|
|
15
19
|
}.freeze #: Hash[String, Symbol]
|
|
16
20
|
|
|
17
21
|
# Returns the XML skill adapter for Anthropic/Claude.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# wire value. +reason+ carries the same meaning for every provider.
|
|
6
6
|
class Riffer::Providers::FinishReason
|
|
7
7
|
# The normalized vocabulary every provider maps into.
|
|
8
|
-
VALUES = %i[stop length tool_calls content_filter error other].freeze #: Array[Symbol]
|
|
8
|
+
VALUES = %i[stop length tool_calls content_filter context_window malformed_output error other].freeze #: Array[Symbol]
|
|
9
9
|
|
|
10
10
|
# The normalized reason.
|
|
11
11
|
attr_reader :reason #: Symbol
|
|
@@ -17,7 +17,16 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
17
17
|
"PROHIBITED_CONTENT" => :content_filter,
|
|
18
18
|
"SPII" => :content_filter,
|
|
19
19
|
"IMAGE_SAFETY" => :content_filter,
|
|
20
|
-
"
|
|
20
|
+
"IMAGE_PROHIBITED_CONTENT" => :content_filter,
|
|
21
|
+
"IMAGE_RECITATION" => :content_filter,
|
|
22
|
+
"LANGUAGE" => :content_filter,
|
|
23
|
+
"MALFORMED_FUNCTION_CALL" => :malformed_output,
|
|
24
|
+
"UNEXPECTED_TOOL_CALL" => :malformed_output,
|
|
25
|
+
"NO_IMAGE" => :error,
|
|
26
|
+
"TOO_MANY_TOOL_CALLS" => :other,
|
|
27
|
+
"OTHER" => :other,
|
|
28
|
+
"IMAGE_OTHER" => :other,
|
|
29
|
+
"FINISH_REASON_UNSPECIFIED" => :other,
|
|
21
30
|
}.freeze #: Hash[String, Symbol]
|
|
22
31
|
|
|
23
32
|
# The GenAI semconv well-known provider name.
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
6
6
|
WEB_SEARCH_TOOL_TYPE = "web_search_preview" #: String
|
|
7
7
|
|
|
8
|
+
# The Responses API has no finish_reason field. The response +status+ is
|
|
9
|
+
# the primary signal; an +incomplete+ status is only meaningful together
|
|
10
|
+
# with <tt>incomplete_details.reason</tt>, so that branch nests one level.
|
|
11
|
+
FINISH_REASONS = {
|
|
12
|
+
"completed" => :stop,
|
|
13
|
+
"incomplete" => {
|
|
14
|
+
"max_output_tokens" => :length,
|
|
15
|
+
"content_filter" => :content_filter,
|
|
16
|
+
},
|
|
17
|
+
"failed" => :error,
|
|
18
|
+
"cancelled" => :other,
|
|
19
|
+
"in_progress" => :other,
|
|
20
|
+
"queued" => :other,
|
|
21
|
+
}.freeze #: Hash[String, Symbol | Hash[String, Symbol]]
|
|
22
|
+
|
|
8
23
|
# The GenAI semconv well-known provider name.
|
|
9
24
|
#--
|
|
10
25
|
#: () -> String
|
|
@@ -133,40 +148,32 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
133
148
|
build_finish_reason(response)
|
|
134
149
|
end
|
|
135
150
|
|
|
136
|
-
# The Responses API reports no finish_reason field, so one is derived.
|
|
137
151
|
#--
|
|
138
152
|
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
139
153
|
def build_finish_reason(response)
|
|
140
154
|
typed_response = response #: OpenAI::Models::Responses::Response
|
|
141
|
-
status = typed_response.status
|
|
155
|
+
status = typed_response.status&.to_s
|
|
142
156
|
return nil unless status
|
|
143
157
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Riffer::Providers::FinishReason.new(reason: :error, raw: "failed")
|
|
152
|
-
else
|
|
153
|
-
Riffer::Providers::FinishReason.new(reason: :other, raw: status.to_s)
|
|
154
|
-
end
|
|
158
|
+
detail = finish_detail(typed_response, status)
|
|
159
|
+
mapping = FINISH_REASONS.fetch(status, :other)
|
|
160
|
+
reason = mapping.is_a?(Hash) ? mapping.fetch(detail.to_s, :other) : mapping
|
|
161
|
+
# A completed response signals tool use only through its output items.
|
|
162
|
+
reason = :tool_calls if reason == :stop && !extract_tool_calls(typed_response).empty?
|
|
163
|
+
|
|
164
|
+
Riffer::Providers::FinishReason.new(reason: reason, raw: detail || status)
|
|
155
165
|
end
|
|
156
166
|
|
|
167
|
+
# The nested field that names the cause behind an ambiguous status.
|
|
157
168
|
#--
|
|
158
|
-
#: (untyped) ->
|
|
159
|
-
def
|
|
169
|
+
#: (untyped, String) -> String?
|
|
170
|
+
def finish_detail(response, status)
|
|
160
171
|
typed_response = response #: OpenAI::Models::Responses::Response
|
|
161
|
-
raw = typed_response.incomplete_details&.reason&.to_s
|
|
162
|
-
|
|
163
|
-
reason = case raw
|
|
164
|
-
when "max_output_tokens" then :length
|
|
165
|
-
when "content_filter" then :content_filter
|
|
166
|
-
else :other
|
|
167
|
-
end
|
|
168
172
|
|
|
169
|
-
|
|
173
|
+
case status
|
|
174
|
+
when "incomplete" then typed_response.incomplete_details&.reason&.to_s
|
|
175
|
+
when "failed" then typed_response.error&.code&.to_s
|
|
176
|
+
end
|
|
170
177
|
end
|
|
171
178
|
|
|
172
179
|
#--
|
|
@@ -133,18 +133,30 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
133
133
|
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
134
134
|
def extract_finish_reason(response)
|
|
135
135
|
typed_response = response #: OpenAI::Models::Chat::ChatCompletion
|
|
136
|
-
|
|
136
|
+
choice = typed_response.choices.first
|
|
137
|
+
build_finish_reason(choice&.finish_reason, native: native_finish_reason(choice))
|
|
137
138
|
end
|
|
138
139
|
|
|
140
|
+
# +native+ is the upstream model's own finish reason, which OpenRouter
|
|
141
|
+
# reports alongside its normalized one; it wins as +raw+ when present.
|
|
139
142
|
#--
|
|
140
|
-
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
141
|
-
def build_finish_reason(finish_reason)
|
|
143
|
+
#: (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
|
|
144
|
+
def build_finish_reason(finish_reason, native: nil)
|
|
142
145
|
return nil unless finish_reason
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
return nil if
|
|
147
|
+
normalized = finish_reason.to_s
|
|
148
|
+
return nil if normalized.empty?
|
|
149
|
+
|
|
150
|
+
raw = native.to_s.empty? ? normalized : native.to_s
|
|
151
|
+
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(normalized, :other), raw: raw)
|
|
152
|
+
end
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
# +native_finish_reason+ is outside the OpenAI schema, so it is only
|
|
155
|
+
# reachable through the SDK model's raw data hash.
|
|
156
|
+
#--
|
|
157
|
+
#: (untyped) -> untyped
|
|
158
|
+
def native_finish_reason(choice)
|
|
159
|
+
choice && choice.to_h[:native_finish_reason]
|
|
148
160
|
end
|
|
149
161
|
|
|
150
162
|
#--
|
|
@@ -187,6 +199,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
187
199
|
reasoning: +"",
|
|
188
200
|
tool_calls: {},
|
|
189
201
|
finish_reason: nil,
|
|
202
|
+
native_finish_reason: nil,
|
|
190
203
|
} #: Hash[Symbol, untyped]
|
|
191
204
|
|
|
192
205
|
# Use stream_raw (not stream) — the latter yields a higher-level
|
|
@@ -211,7 +224,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
211
224
|
|
|
212
225
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
213
226
|
yielder << Riffer::StreamEvents::ReasoningDone.new(state[:reasoning]) unless state[:reasoning].empty?
|
|
214
|
-
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason]))
|
|
227
|
+
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
|
|
215
228
|
end
|
|
216
229
|
|
|
217
230
|
#--
|
|
@@ -228,6 +241,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
228
241
|
end
|
|
229
242
|
|
|
230
243
|
state[:finish_reason] = choice.finish_reason if choice&.finish_reason
|
|
244
|
+
state[:native_finish_reason] = native_finish_reason(choice) || state[:native_finish_reason]
|
|
231
245
|
|
|
232
246
|
emit_tool_call_done_events(state: state, yielder: yielder) if choice && finish_reason_is_tool_calls?(choice)
|
|
233
247
|
|
data/lib/riffer/version.rb
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
5
5
|
WEB_SEARCH_TOOL_TYPE: String
|
|
6
6
|
|
|
7
|
+
# The Responses API has no finish_reason field. The response +status+ is
|
|
8
|
+
# the primary signal; an +incomplete+ status is only meaningful together
|
|
9
|
+
# with <tt>incomplete_details.reason</tt>, so that branch nests one level.
|
|
10
|
+
FINISH_REASONS: Hash[String, Symbol | Hash[String, Symbol]]
|
|
11
|
+
|
|
7
12
|
# The GenAI semconv well-known provider name.
|
|
8
13
|
# --
|
|
9
14
|
# : () -> String
|
|
@@ -50,14 +55,14 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
50
55
|
# : (untyped) -> Riffer::Providers::FinishReason?
|
|
51
56
|
def extract_finish_reason: (untyped) -> Riffer::Providers::FinishReason?
|
|
52
57
|
|
|
53
|
-
# The Responses API reports no finish_reason field, so one is derived.
|
|
54
58
|
# --
|
|
55
59
|
# : (untyped) -> Riffer::Providers::FinishReason?
|
|
56
60
|
def build_finish_reason: (untyped) -> Riffer::Providers::FinishReason?
|
|
57
61
|
|
|
62
|
+
# The nested field that names the cause behind an ambiguous status.
|
|
58
63
|
# --
|
|
59
|
-
# : (untyped) ->
|
|
60
|
-
def
|
|
64
|
+
# : (untyped, String) -> String?
|
|
65
|
+
def finish_detail: (untyped, String) -> String?
|
|
61
66
|
|
|
62
67
|
# --
|
|
63
68
|
# : (untyped) -> String
|
|
@@ -57,9 +57,17 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
57
57
|
# : (untyped) -> Riffer::Providers::FinishReason?
|
|
58
58
|
def extract_finish_reason: (untyped) -> Riffer::Providers::FinishReason?
|
|
59
59
|
|
|
60
|
+
# +native+ is the upstream model's own finish reason, which OpenRouter
|
|
61
|
+
# reports alongside its normalized one; it wins as +raw+ when present.
|
|
60
62
|
# --
|
|
61
|
-
# : (untyped) -> Riffer::Providers::FinishReason?
|
|
62
|
-
def build_finish_reason: (untyped) -> Riffer::Providers::FinishReason?
|
|
63
|
+
# : (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
|
|
64
|
+
def build_finish_reason: (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
|
|
65
|
+
|
|
66
|
+
# +native_finish_reason+ is outside the OpenAI schema, so it is only
|
|
67
|
+
# reachable through the SDK model's raw data hash.
|
|
68
|
+
# --
|
|
69
|
+
# : (untyped) -> untyped
|
|
70
|
+
def native_finish_reason: (untyped) -> untyped
|
|
63
71
|
|
|
64
72
|
# --
|
|
65
73
|
# : (untyped) -> String
|