riffer 0.32.1 → 0.34.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/.ruby-version +1 -1
- data/CHANGELOG.md +39 -0
- data/README.md +13 -11
- data/docs/01_OVERVIEW.md +2 -0
- data/docs/04_AGENT_LIFECYCLE.md +15 -13
- data/docs/08_MESSAGES.md +39 -5
- data/docs/09_STREAM_EVENTS.md +14 -0
- data/docs/10_CONFIGURATION.md +73 -4
- data/docs/13_SKILLS.md +62 -4
- data/docs/14_MCP.md +2 -1
- data/docs/16_TRACING.md +274 -0
- data/docs/17_METRICS.md +153 -0
- data/docs/providers/07_CUSTOM_PROVIDERS.md +44 -0
- data/lib/riffer/agent/response.rb +11 -2
- data/lib/riffer/agent/run.rb +136 -35
- data/lib/riffer/agent.rb +1 -1
- data/lib/riffer/config.rb +230 -15
- data/lib/riffer/guardrail.rb +8 -0
- data/lib/riffer/guardrails/runner.rb +58 -0
- data/lib/riffer/helpers/boolean.rb +22 -0
- data/lib/riffer/mcp/authenticated_tool.rb +14 -20
- data/lib/riffer/mcp/registration.rb +4 -4
- data/lib/riffer/mcp/tool.rb +23 -0
- data/lib/riffer/mcp/tool_factory.rb +14 -22
- data/lib/riffer/messages/assistant.rb +15 -3
- data/lib/riffer/messages/base.rb +2 -1
- data/lib/riffer/metrics/instruments.rb +31 -0
- data/lib/riffer/metrics/no_op.rb +14 -0
- data/lib/riffer/metrics/otel.rb +80 -0
- data/lib/riffer/metrics.rb +94 -0
- data/lib/riffer/providers/amazon_bedrock.rb +57 -21
- data/lib/riffer/providers/anthropic.rb +59 -24
- data/lib/riffer/providers/azure_open_ai.rb +7 -0
- data/lib/riffer/providers/base.rb +247 -15
- data/lib/riffer/providers/finish_reason.rb +27 -0
- data/lib/riffer/providers/gemini.rb +59 -11
- data/lib/riffer/providers/mock.rb +30 -9
- data/lib/riffer/providers/open_ai.rb +78 -24
- data/lib/riffer/providers/open_router.rb +56 -16
- data/lib/riffer/providers/repository.rb +9 -0
- data/lib/riffer/providers/token_usage.rb +27 -11
- data/lib/riffer/skills/activate_tool.rb +11 -2
- data/lib/riffer/skills/adapter.rb +15 -0
- data/lib/riffer/skills/context.rb +63 -11
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/finish_reason_done.rb +34 -0
- data/lib/riffer/tools/runtime.rb +99 -3
- data/lib/riffer/tracing/capture.rb +92 -0
- data/lib/riffer/tracing/no_op.rb +61 -0
- data/lib/riffer/tracing/otel.rb +132 -0
- data/lib/riffer/tracing/stream_recorder.rb +51 -0
- data/lib/riffer/tracing.rb +78 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/opentelemetry.rbs +22 -0
- data/sig/generated/riffer/agent/response.rbs +9 -2
- data/sig/generated/riffer/agent/run.rbs +28 -8
- data/sig/generated/riffer/config.rbs +162 -16
- data/sig/generated/riffer/guardrail.rbs +6 -0
- data/sig/generated/riffer/guardrails/runner.rbs +22 -0
- data/sig/generated/riffer/helpers/boolean.rbs +11 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +6 -8
- data/sig/generated/riffer/mcp/registration.rbs +4 -4
- data/sig/generated/riffer/mcp/tool.rbs +19 -0
- data/sig/generated/riffer/mcp/tool_factory.rbs +8 -7
- data/sig/generated/riffer/messages/assistant.rbs +10 -4
- data/sig/generated/riffer/metrics/instruments.rbs +15 -0
- data/sig/generated/riffer/metrics/no_op.rbs +10 -0
- data/sig/generated/riffer/metrics/otel.rbs +48 -0
- data/sig/generated/riffer/metrics.rbs +72 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +35 -14
- data/sig/generated/riffer/providers/anthropic.rbs +41 -20
- data/sig/generated/riffer/providers/azure_open_ai.rbs +5 -0
- data/sig/generated/riffer/providers/base.rbs +78 -2
- data/sig/generated/riffer/providers/finish_reason.rbs +19 -0
- data/sig/generated/riffer/providers/gemini.rbs +25 -2
- data/sig/generated/riffer/providers/mock.rbs +16 -5
- data/sig/generated/riffer/providers/open_ai.rbs +44 -22
- data/sig/generated/riffer/providers/open_router.rbs +31 -12
- data/sig/generated/riffer/providers/repository.rbs +7 -0
- data/sig/generated/riffer/providers/token_usage.rbs +20 -10
- data/sig/generated/riffer/skills/activate_tool.rbs +7 -1
- data/sig/generated/riffer/skills/adapter.rbs +10 -0
- data/sig/generated/riffer/skills/context.rbs +42 -4
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +21 -0
- data/sig/generated/riffer/tools/runtime.rbs +35 -0
- data/sig/generated/riffer/tracing/capture.rbs +46 -0
- data/sig/generated/riffer/tracing/no_op.rbs +46 -0
- data/sig/generated/riffer/tracing/otel.rbs +84 -0
- data/sig/generated/riffer/tracing/stream_recorder.rbs +31 -0
- data/sig/generated/riffer/tracing.rbs +52 -0
- data/sig/manual/riffer/helpers/boolean.rbs +5 -0
- data/sig/manual/riffer/metrics/no_op.rbs +5 -0
- data/sig/manual/riffer/metrics.rbs +5 -0
- data/sig/manual/riffer/providers.rbs +9 -0
- data/sig/manual/riffer/tracing/capture.rbs +5 -0
- data/sig/manual/riffer/tracing/no_op.rbs +5 -0
- data/sig/manual/riffer/tracing.rbs +5 -0
- metadata +42 -6
|
@@ -16,6 +16,13 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
16
16
|
Riffer::Skills::MarkdownAdapter
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# The GenAI semconv well-known provider name.
|
|
20
|
+
#--
|
|
21
|
+
#: () -> String
|
|
22
|
+
def self.semconv_provider_name
|
|
23
|
+
"mock"
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
# Array of recorded method calls for assertions.
|
|
20
27
|
attr_reader :calls #: Array[Hash[Symbol, untyped]]
|
|
21
28
|
|
|
@@ -36,16 +43,18 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
36
43
|
@stubbed_responses = []
|
|
37
44
|
end
|
|
38
45
|
|
|
39
|
-
# Stubs the next response; call repeatedly to queue several.
|
|
46
|
+
# Stubs the next response; call repeatedly to queue several. +finish_reason+
|
|
47
|
+
# defaults to +:tool_calls+ when tool calls are present, else +:stop+.
|
|
40
48
|
#
|
|
41
49
|
# provider.stub_response("Hello")
|
|
42
50
|
# provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
|
|
43
51
|
# provider.stub_response("Final response", token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
52
|
+
# provider.stub_response("Truncated...", finish_reason: :length)
|
|
44
53
|
#
|
|
45
54
|
#--
|
|
46
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?) -> void
|
|
47
|
-
def stub_response(content, tool_calls: [], token_usage: nil)
|
|
48
|
-
@stubbed_responses << normalize_response(content: content, tool_calls: tool_calls, token_usage: token_usage)
|
|
55
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?) -> void
|
|
56
|
+
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil)
|
|
57
|
+
@stubbed_responses << normalize_response(content: content, tool_calls: tool_calls, token_usage: token_usage, finish_reason: finish_reason)
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
# Clears all stubbed responses.
|
|
@@ -73,7 +82,8 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
73
82
|
role: response[:role] || "assistant",
|
|
74
83
|
content: response[:content] || "",
|
|
75
84
|
tool_calls: formatted_tool_calls,
|
|
76
|
-
token_usage: response[:token_usage]
|
|
85
|
+
token_usage: response[:token_usage],
|
|
86
|
+
finish_reason: response[:finish_reason] || (formatted_tool_calls.empty? ? :stop : :tool_calls)
|
|
77
87
|
}
|
|
78
88
|
end
|
|
79
89
|
|
|
@@ -96,7 +106,17 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
96
106
|
#--
|
|
97
107
|
#: (untyped) -> Riffer::Providers::TokenUsage?
|
|
98
108
|
def extract_token_usage(response)
|
|
99
|
-
response[:token_usage]
|
|
109
|
+
usage = response[:token_usage]
|
|
110
|
+
usage && apply_pricing(usage)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#--
|
|
114
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
115
|
+
def extract_finish_reason(response)
|
|
116
|
+
return nil unless response.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
reason = response[:finish_reason]
|
|
119
|
+
reason ? Riffer::Providers::FinishReason.new(reason: reason) : nil
|
|
100
120
|
end
|
|
101
121
|
|
|
102
122
|
#--
|
|
@@ -112,7 +132,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
#--
|
|
115
|
-
#: (Hash[Symbol, untyped],
|
|
135
|
+
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
116
136
|
def execute_stream(params, yielder)
|
|
117
137
|
response = params[:response]
|
|
118
138
|
full_content = response[:content] || ""
|
|
@@ -150,7 +170,8 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
150
170
|
end
|
|
151
171
|
|
|
152
172
|
yielder << Riffer::StreamEvents::TextDone.new(full_content)
|
|
153
|
-
yielder
|
|
173
|
+
yield_finish_reason(yielder, extract_finish_reason(response))
|
|
174
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: apply_pricing(token_usage)) if token_usage
|
|
154
175
|
end
|
|
155
176
|
|
|
156
177
|
#--
|
|
@@ -163,7 +184,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
163
184
|
@current_index += 1
|
|
164
185
|
response
|
|
165
186
|
else
|
|
166
|
-
|
|
187
|
+
normalize_response(content: "Mock response")
|
|
167
188
|
end
|
|
168
189
|
end
|
|
169
190
|
end
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
6
6
|
WEB_SEARCH_TOOL_TYPE = "web_search_preview" #: String
|
|
7
7
|
|
|
8
|
+
# The GenAI semconv well-known provider name.
|
|
9
|
+
#--
|
|
10
|
+
#: () -> String
|
|
11
|
+
def self.semconv_provider_name
|
|
12
|
+
"openai"
|
|
13
|
+
end
|
|
14
|
+
|
|
8
15
|
#--
|
|
9
16
|
#: (**untyped) -> void
|
|
10
17
|
def initialize(**options)
|
|
@@ -74,11 +81,59 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
74
81
|
usage = typed_response.usage
|
|
75
82
|
return nil unless usage
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
build_token_usage(usage)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#--
|
|
88
|
+
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
89
|
+
def build_token_usage(usage)
|
|
90
|
+
apply_pricing(Riffer::Providers::TokenUsage.new(
|
|
78
91
|
input_tokens: usage.input_tokens,
|
|
79
92
|
output_tokens: usage.output_tokens,
|
|
80
93
|
cache_read_tokens: usage.input_tokens_details&.cached_tokens
|
|
81
|
-
)
|
|
94
|
+
))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#--
|
|
98
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
99
|
+
def extract_finish_reason(response)
|
|
100
|
+
build_finish_reason(response)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The Responses API reports no finish_reason field, so one is derived.
|
|
104
|
+
#--
|
|
105
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
106
|
+
def build_finish_reason(response)
|
|
107
|
+
typed_response = response #: OpenAI::Models::Responses::Response
|
|
108
|
+
status = typed_response.status
|
|
109
|
+
return nil unless status
|
|
110
|
+
|
|
111
|
+
case status.to_sym
|
|
112
|
+
when :completed
|
|
113
|
+
reason = extract_tool_calls(typed_response).empty? ? :stop : :tool_calls
|
|
114
|
+
Riffer::Providers::FinishReason.new(reason: reason, raw: "completed")
|
|
115
|
+
when :incomplete
|
|
116
|
+
incomplete_finish_reason(typed_response)
|
|
117
|
+
when :failed
|
|
118
|
+
Riffer::Providers::FinishReason.new(reason: :error, raw: "failed")
|
|
119
|
+
else
|
|
120
|
+
Riffer::Providers::FinishReason.new(reason: :other, raw: status.to_s)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#--
|
|
125
|
+
#: (untyped) -> Riffer::Providers::FinishReason
|
|
126
|
+
def incomplete_finish_reason(response)
|
|
127
|
+
typed_response = response #: OpenAI::Models::Responses::Response
|
|
128
|
+
raw = typed_response.incomplete_details&.reason&.to_s
|
|
129
|
+
|
|
130
|
+
reason = case raw
|
|
131
|
+
when "max_output_tokens" then :length
|
|
132
|
+
when "content_filter" then :content_filter
|
|
133
|
+
else :other
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Riffer::Providers::FinishReason.new(reason: reason, raw: raw || "incomplete")
|
|
82
137
|
end
|
|
83
138
|
|
|
84
139
|
#--
|
|
@@ -117,7 +172,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
117
172
|
end
|
|
118
173
|
|
|
119
174
|
#--
|
|
120
|
-
#: (Hash[Symbol, untyped],
|
|
175
|
+
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
121
176
|
def execute_stream(params, yielder)
|
|
122
177
|
current_state = {
|
|
123
178
|
tool_info: {}
|
|
@@ -149,8 +204,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
149
204
|
handle_web_search_status(event, status: "completed", yielder: yielder)
|
|
150
205
|
when :"response.output_item.done"
|
|
151
206
|
handle_output_item_done_web_search(event, yielder: yielder) if event.item&.type == :web_search_call
|
|
152
|
-
when :"response.completed"
|
|
153
|
-
|
|
207
|
+
when :"response.completed", :"response.incomplete", :"response.failed"
|
|
208
|
+
handle_response_finished(event, state: current_state, yielder: yielder)
|
|
154
209
|
end
|
|
155
210
|
end
|
|
156
211
|
ensure
|
|
@@ -162,7 +217,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
162
217
|
end
|
|
163
218
|
|
|
164
219
|
#--
|
|
165
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
220
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
166
221
|
def handle_output_item_added_function_call(event, state:, yielder:)
|
|
167
222
|
state[:tool_info][event.item.id] = {
|
|
168
223
|
name: decode_tool_name(event.item.name, tools: @current_tools),
|
|
@@ -171,31 +226,31 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
171
226
|
end
|
|
172
227
|
|
|
173
228
|
#--
|
|
174
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
229
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
175
230
|
def handle_output_text_delta(event, state:, yielder:)
|
|
176
231
|
yielder << Riffer::StreamEvents::TextDelta.new(event.delta)
|
|
177
232
|
end
|
|
178
233
|
|
|
179
234
|
#--
|
|
180
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
235
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
181
236
|
def handle_output_text_done(event, state:, yielder:)
|
|
182
237
|
yielder << Riffer::StreamEvents::TextDone.new(event.text)
|
|
183
238
|
end
|
|
184
239
|
|
|
185
240
|
#--
|
|
186
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
241
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
187
242
|
def handle_reasoning_summary_text_delta(event, state:, yielder:)
|
|
188
243
|
yielder << Riffer::StreamEvents::ReasoningDelta.new(event.delta)
|
|
189
244
|
end
|
|
190
245
|
|
|
191
246
|
#--
|
|
192
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
247
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
193
248
|
def handle_reasoning_summary_text_done(event, state:, yielder:)
|
|
194
249
|
yielder << Riffer::StreamEvents::ReasoningDone.new(event.text)
|
|
195
250
|
end
|
|
196
251
|
|
|
197
252
|
#--
|
|
198
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
253
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
199
254
|
def handle_function_call_arguments_delta(event, state:, yielder:)
|
|
200
255
|
tracked = state[:tool_info][event.item_id] || {}
|
|
201
256
|
yielder << Riffer::StreamEvents::ToolCallDelta.new(
|
|
@@ -206,7 +261,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
206
261
|
end
|
|
207
262
|
|
|
208
263
|
#--
|
|
209
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
264
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
210
265
|
def handle_function_call_arguments_done(event, state:, yielder:)
|
|
211
266
|
tracked = state[:tool_info][event.item_id] || {}
|
|
212
267
|
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
@@ -218,28 +273,27 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
218
273
|
end
|
|
219
274
|
|
|
220
275
|
#--
|
|
221
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
222
|
-
def
|
|
223
|
-
|
|
276
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
277
|
+
def handle_response_finished(event, state:, yielder:)
|
|
278
|
+
response = event.response
|
|
279
|
+
return unless response
|
|
280
|
+
|
|
281
|
+
yield_finish_reason(yielder, build_finish_reason(response))
|
|
282
|
+
|
|
283
|
+
usage = response.usage
|
|
224
284
|
return unless usage
|
|
225
285
|
|
|
226
|
-
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
227
|
-
token_usage: Riffer::Providers::TokenUsage.new(
|
|
228
|
-
input_tokens: usage.input_tokens,
|
|
229
|
-
output_tokens: usage.output_tokens,
|
|
230
|
-
cache_read_tokens: usage.input_tokens_details&.cached_tokens
|
|
231
|
-
)
|
|
232
|
-
)
|
|
286
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: build_token_usage(usage))
|
|
233
287
|
end
|
|
234
288
|
|
|
235
289
|
#--
|
|
236
|
-
#: (untyped, status: String, yielder:
|
|
290
|
+
#: (untyped, status: String, yielder: Riffer::Providers::_EventSink) -> void
|
|
237
291
|
def handle_web_search_status(_event, status:, yielder:)
|
|
238
292
|
yielder << Riffer::StreamEvents::WebSearchStatus.new(status)
|
|
239
293
|
end
|
|
240
294
|
|
|
241
295
|
#--
|
|
242
|
-
#: (untyped, yielder:
|
|
296
|
+
#: (untyped, yielder: Riffer::Providers::_EventSink) -> void
|
|
243
297
|
def handle_output_item_done_web_search(event, yielder:)
|
|
244
298
|
action = event.item.action
|
|
245
299
|
case action
|
|
@@ -10,6 +10,22 @@ require "json"
|
|
|
10
10
|
class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
11
11
|
BASE_URL = "https://openrouter.ai/api/v1" #: String
|
|
12
12
|
|
|
13
|
+
FINISH_REASONS = {
|
|
14
|
+
"stop" => :stop,
|
|
15
|
+
"length" => :length,
|
|
16
|
+
"tool_calls" => :tool_calls,
|
|
17
|
+
"function_call" => :tool_calls,
|
|
18
|
+
"content_filter" => :content_filter,
|
|
19
|
+
"error" => :error
|
|
20
|
+
}.freeze #: Hash[String, Symbol]
|
|
21
|
+
|
|
22
|
+
# The GenAI semconv well-known provider name.
|
|
23
|
+
#--
|
|
24
|
+
#: () -> String
|
|
25
|
+
def self.semconv_provider_name
|
|
26
|
+
"openrouter"
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
#--
|
|
14
30
|
#: (?api_key: String?, **untyped) -> void
|
|
15
31
|
def initialize(api_key: nil, **options)
|
|
@@ -70,10 +86,35 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
70
86
|
usage = typed_response.usage
|
|
71
87
|
return nil unless usage
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
build_token_usage(usage)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#--
|
|
93
|
+
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
94
|
+
def build_token_usage(usage)
|
|
95
|
+
apply_pricing(Riffer::Providers::TokenUsage.new(
|
|
74
96
|
input_tokens: usage.prompt_tokens,
|
|
75
|
-
output_tokens: usage.completion_tokens
|
|
76
|
-
|
|
97
|
+
output_tokens: usage.completion_tokens,
|
|
98
|
+
cache_read_tokens: usage.prompt_tokens_details&.cached_tokens
|
|
99
|
+
))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#--
|
|
103
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
104
|
+
def extract_finish_reason(response)
|
|
105
|
+
typed_response = response #: OpenAI::Models::Chat::ChatCompletion
|
|
106
|
+
build_finish_reason(typed_response.choices.first&.finish_reason)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#--
|
|
110
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
111
|
+
def build_finish_reason(finish_reason)
|
|
112
|
+
return nil unless finish_reason
|
|
113
|
+
|
|
114
|
+
raw = finish_reason.to_s
|
|
115
|
+
return nil if raw.empty?
|
|
116
|
+
|
|
117
|
+
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(raw, :other), raw: raw)
|
|
77
118
|
end
|
|
78
119
|
|
|
79
120
|
#--
|
|
@@ -105,7 +146,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
105
146
|
end
|
|
106
147
|
|
|
107
148
|
#--
|
|
108
|
-
#: (Hash[Symbol, untyped],
|
|
149
|
+
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
109
150
|
def execute_stream(params, yielder)
|
|
110
151
|
# OpenRouter omits usage from streams unless explicitly opted in.
|
|
111
152
|
stream_options = (params[:stream_options] || {}).merge(include_usage: true)
|
|
@@ -114,7 +155,8 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
114
155
|
state = {
|
|
115
156
|
text: +"",
|
|
116
157
|
reasoning: +"",
|
|
117
|
-
tool_calls: {}
|
|
158
|
+
tool_calls: {},
|
|
159
|
+
finish_reason: nil
|
|
118
160
|
} #: Hash[Symbol, untyped]
|
|
119
161
|
|
|
120
162
|
# Use stream_raw (not stream) — the latter yields a higher-level
|
|
@@ -139,10 +181,11 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
139
181
|
|
|
140
182
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
141
183
|
yielder << Riffer::StreamEvents::ReasoningDone.new(state[:reasoning]) unless state[:reasoning].empty?
|
|
184
|
+
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason]))
|
|
142
185
|
end
|
|
143
186
|
|
|
144
187
|
#--
|
|
145
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
188
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
146
189
|
def handle_stream_chunk(chunk, state:, yielder:)
|
|
147
190
|
typed_chunk = chunk #: OpenAI::Models::Chat::ChatCompletionChunk
|
|
148
191
|
choice = typed_chunk.choices&.first
|
|
@@ -154,22 +197,19 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
154
197
|
handle_tool_call_deltas(delta, state: state, yielder: yielder)
|
|
155
198
|
end
|
|
156
199
|
|
|
200
|
+
state[:finish_reason] = choice.finish_reason if choice&.finish_reason
|
|
201
|
+
|
|
157
202
|
if choice && finish_reason_is_tool_calls?(choice)
|
|
158
203
|
emit_tool_call_done_events(state: state, yielder: yielder)
|
|
159
204
|
end
|
|
160
205
|
|
|
161
206
|
return unless typed_chunk.usage
|
|
162
207
|
|
|
163
|
-
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
164
|
-
token_usage: Riffer::Providers::TokenUsage.new(
|
|
165
|
-
input_tokens: typed_chunk.usage.prompt_tokens,
|
|
166
|
-
output_tokens: typed_chunk.usage.completion_tokens
|
|
167
|
-
)
|
|
168
|
-
)
|
|
208
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: build_token_usage(typed_chunk.usage))
|
|
169
209
|
end
|
|
170
210
|
|
|
171
211
|
#--
|
|
172
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
212
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
173
213
|
def handle_text_delta(delta, state:, yielder:)
|
|
174
214
|
typed_delta = delta #: OpenAI::Models::Chat::ChatCompletionChunk::Choice::Delta
|
|
175
215
|
content = typed_delta.content
|
|
@@ -180,7 +220,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
180
220
|
end
|
|
181
221
|
|
|
182
222
|
#--
|
|
183
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
223
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
184
224
|
def handle_reasoning_delta(delta, state:, yielder:)
|
|
185
225
|
# The openai gem's typed Delta model strips fields not in OpenAI's spec
|
|
186
226
|
# (so +delta.reasoning+ raises NoMethodError), but the underlying data
|
|
@@ -193,7 +233,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
193
233
|
end
|
|
194
234
|
|
|
195
235
|
#--
|
|
196
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
236
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
197
237
|
def handle_tool_call_deltas(delta, state:, yielder:)
|
|
198
238
|
typed_delta = delta #: OpenAI::Models::Chat::ChatCompletionChunk::Choice::Delta
|
|
199
239
|
tool_calls = typed_delta.tool_calls
|
|
@@ -221,7 +261,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
221
261
|
end
|
|
222
262
|
|
|
223
263
|
#--
|
|
224
|
-
#: (state: Hash[Symbol, untyped], yielder:
|
|
264
|
+
#: (state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
225
265
|
def emit_tool_call_done_events(state:, yielder:)
|
|
226
266
|
state[:tool_calls].each do |index, entry|
|
|
227
267
|
fallback = "tool_#{index}"
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
module Riffer::Providers::Repository
|
|
6
6
|
extend self
|
|
7
7
|
|
|
8
|
+
# @rbs @key_for: Hash[singleton(Riffer::Providers::Base), Symbol]?
|
|
9
|
+
|
|
8
10
|
REPO = {
|
|
9
11
|
amazon_bedrock: -> { Riffer::Providers::AmazonBedrock },
|
|
10
12
|
anthropic: -> { Riffer::Providers::Anthropic },
|
|
@@ -22,4 +24,11 @@ module Riffer::Providers::Repository
|
|
|
22
24
|
def find(identifier)
|
|
23
25
|
REPO.fetch(identifier.to_sym, nil)&.call
|
|
24
26
|
end
|
|
27
|
+
|
|
28
|
+
# Returns the registry identifier for a provider class, or nil when unregistered.
|
|
29
|
+
#--
|
|
30
|
+
#: (singleton(Riffer::Providers::Base)) -> Symbol?
|
|
31
|
+
def key_for(provider_class)
|
|
32
|
+
(@key_for ||= REPO.to_h { |key, factory| [factory.call, key] })[provider_class]
|
|
33
|
+
end
|
|
25
34
|
end
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# Normalized token usage for an LLM API call. Buckets carry the same
|
|
5
|
+
# meaning for every provider.
|
|
5
6
|
class Riffer::Providers::TokenUsage
|
|
6
|
-
# Number of tokens
|
|
7
|
+
# Number of tokens entering the context window, including cache reads and writes.
|
|
7
8
|
attr_reader :input_tokens #: Integer
|
|
8
9
|
|
|
9
|
-
# Number of tokens
|
|
10
|
+
# Number of tokens generated by the model, including reasoning/thinking tokens.
|
|
10
11
|
attr_reader :output_tokens #: Integer
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
+
# Subset of +input_tokens+ written to the provider's prompt cache, when the provider reports it.
|
|
13
14
|
attr_reader :cache_write_tokens #: Integer?
|
|
14
15
|
|
|
15
|
-
#
|
|
16
|
+
# Subset of +input_tokens+ read from the provider's prompt cache, when the provider reports it.
|
|
16
17
|
attr_reader :cache_read_tokens #: Integer?
|
|
17
18
|
|
|
19
|
+
# Cost of the call, set when the model is priced. For observability, not billing.
|
|
20
|
+
attr_reader :cost #: Float?
|
|
21
|
+
|
|
18
22
|
#--
|
|
19
|
-
#: (input_tokens: Integer, output_tokens: Integer, ?cache_write_tokens: Integer?, ?cache_read_tokens: Integer?) -> void
|
|
20
|
-
def initialize(input_tokens:, output_tokens:, cache_write_tokens: nil, cache_read_tokens: nil)
|
|
23
|
+
#: (input_tokens: Integer, output_tokens: Integer, ?cache_write_tokens: Integer?, ?cache_read_tokens: Integer?, ?cost: Float?) -> void
|
|
24
|
+
def initialize(input_tokens:, output_tokens:, cache_write_tokens: nil, cache_read_tokens: nil, cost: nil)
|
|
21
25
|
@input_tokens = input_tokens
|
|
22
26
|
@output_tokens = output_tokens
|
|
23
27
|
@cache_write_tokens = cache_write_tokens
|
|
24
28
|
@cache_read_tokens = cache_read_tokens
|
|
29
|
+
@cost = cost
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
# Returns the total number of tokens (input + output).
|
|
@@ -41,17 +46,19 @@ class Riffer::Providers::TokenUsage
|
|
|
41
46
|
input_tokens: input_tokens + other.input_tokens,
|
|
42
47
|
output_tokens: output_tokens + other.output_tokens,
|
|
43
48
|
cache_write_tokens: add_nullable(cache_write_tokens, other.cache_write_tokens),
|
|
44
|
-
cache_read_tokens: add_nullable(cache_read_tokens, other.cache_read_tokens)
|
|
49
|
+
cache_read_tokens: add_nullable(cache_read_tokens, other.cache_read_tokens),
|
|
50
|
+
cost: add_cost(cost, other.cost)
|
|
45
51
|
)
|
|
46
52
|
end
|
|
47
53
|
|
|
48
|
-
# Converts the token usage to a hash; cache tokens are omitted when nil.
|
|
54
|
+
# Converts the token usage to a hash; cache tokens and cost are omitted when nil.
|
|
49
55
|
#--
|
|
50
|
-
#: () -> Hash[Symbol, Integer]
|
|
56
|
+
#: () -> Hash[Symbol, (Integer | Float)]
|
|
51
57
|
def to_h
|
|
52
|
-
hash = {input_tokens: input_tokens, output_tokens: output_tokens}
|
|
58
|
+
hash = {input_tokens: input_tokens, output_tokens: output_tokens} #: Hash[Symbol, (Integer | Float)]
|
|
53
59
|
hash[:cache_write_tokens] = cache_write_tokens if cache_write_tokens
|
|
54
60
|
hash[:cache_read_tokens] = cache_read_tokens if cache_read_tokens
|
|
61
|
+
hash[:cost] = cost if cost
|
|
55
62
|
hash
|
|
56
63
|
end
|
|
57
64
|
|
|
@@ -63,4 +70,13 @@ class Riffer::Providers::TokenUsage
|
|
|
63
70
|
return nil if a.nil? && b.nil?
|
|
64
71
|
(a || 0) + (b || 0)
|
|
65
72
|
end
|
|
73
|
+
|
|
74
|
+
# nil is absorbing, not zero: one unpriced call makes the run total nil
|
|
75
|
+
# rather than silently under-reporting.
|
|
76
|
+
#--
|
|
77
|
+
#: (Float?, Float?) -> Float?
|
|
78
|
+
def add_cost(a, b)
|
|
79
|
+
return nil if a.nil? || b.nil?
|
|
80
|
+
a + b
|
|
81
|
+
end
|
|
66
82
|
end
|
|
@@ -13,16 +13,25 @@ class Riffer::Skills::ActivateTool < Riffer::Tool
|
|
|
13
13
|
required :name, String, description: "The skill name to activate"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# Activates a skill by name and returns its body.
|
|
16
|
+
# Activates a skill by name and returns its wrapped body, or a short pointer when the skill is already active.
|
|
17
17
|
#--
|
|
18
18
|
#: (context: Riffer::Agent::Context?, name: String) -> Riffer::Tools::Response
|
|
19
19
|
def call(context:, name:)
|
|
20
20
|
skills_context = context&.skills
|
|
21
21
|
return error("Skills not configured") unless skills_context
|
|
22
22
|
return error("Unknown skill: '#{name}'") unless skills_context.model_invocable?(name)
|
|
23
|
+
return text(already_active_message(name)) if skills_context.activated?(name)
|
|
23
24
|
|
|
24
|
-
text(skills_context.
|
|
25
|
+
text(skills_context.activation_prompt(name))
|
|
25
26
|
rescue Riffer::ArgumentError => e
|
|
26
27
|
error(e.message)
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
#--
|
|
33
|
+
#: (String) -> String
|
|
34
|
+
def already_active_message(name)
|
|
35
|
+
"Skill '#{name}' is already active. Its instructions are already available in your context."
|
|
36
|
+
end
|
|
28
37
|
end
|
|
@@ -21,4 +21,19 @@ class Riffer::Skills::Adapter
|
|
|
21
21
|
def render_catalog(skills)
|
|
22
22
|
raise NotImplementedError, "#{self.class} must implement #render_catalog"
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Renders an activated skill body wrapped in identifying tags.
|
|
26
|
+
#--
|
|
27
|
+
#: (Riffer::Skills::Frontmatter, String) -> String
|
|
28
|
+
def render_activation(skill, body)
|
|
29
|
+
%(<skill_content name="#{skill.name}">\n#{body}\n</skill_content>)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The behavioral instructions rendered alongside the catalog.
|
|
33
|
+
#--
|
|
34
|
+
#: () -> String
|
|
35
|
+
def catalog_instructions
|
|
36
|
+
"When a user's request matches a skill description below, call the `#{skill_activate_tool.name}` tool with the skill name. After activation, follow the skill's instructions. " \
|
|
37
|
+
"If a skill's instructions already appear in your context (inside <skill_content> tags), follow them instead of activating the skill again."
|
|
38
|
+
end
|
|
24
39
|
end
|