riffer 0.47.2 → 0.49.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/.claude/rules/comments.md +2 -4
- data/.claude/rules/rbs-inline.md +2 -12
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +42 -0
- data/docs/AGENTS.md +39 -7
- data/docs/AGENT_LIFECYCLE.md +14 -16
- data/docs/CONFIGURATION.md +23 -34
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +8 -4
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +2 -2
- data/docs/providers/AMAZON_BEDROCK.md +32 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +56 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/docs/providers/OPENROUTER.md +18 -1
- data/docs-site/build.rb +0 -6
- data/docs-site/check.rb +1 -5
- data/lib/riffer/agent/config.rb +27 -44
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +4 -35
- data/lib/riffer/agent/run.rb +32 -48
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +26 -38
- data/lib/riffer/agent/structured_output/result.rb +0 -8
- data/lib/riffer/agent/structured_output.rb +0 -7
- data/lib/riffer/agent.rb +31 -127
- data/lib/riffer/config/amazon_bedrock.rb +30 -0
- data/lib/riffer/config/anthropic.rb +21 -0
- data/lib/riffer/config/azure_open_ai.rb +30 -0
- data/lib/riffer/config/evals.rb +18 -0
- data/lib/riffer/config/files.rb +61 -0
- data/lib/riffer/config/gemini.rb +21 -0
- data/lib/riffer/config/mcp.rb +31 -0
- data/lib/riffer/config/open_ai.rb +30 -0
- data/lib/riffer/config/open_router.rb +21 -0
- data/lib/riffer/config/pricing/rates.rb +36 -0
- data/lib/riffer/config/pricing.rb +64 -0
- data/lib/riffer/config/skills.rb +38 -0
- data/lib/riffer/config/tracing.rb +43 -0
- data/lib/riffer/config.rb +3 -362
- data/lib/riffer/evals/evaluator.rb +22 -23
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +9 -11
- data/lib/riffer/evals/result.rb +1 -11
- data/lib/riffer/evals/run_result.rb +0 -12
- data/lib/riffer/evals/scenario_result.rb +0 -19
- data/lib/riffer/files/downloader.rb +2 -3
- data/lib/riffer/files/resolver.rb +5 -10
- data/lib/riffer/guardrail.rb +2 -22
- data/lib/riffer/guardrails/modification.rb +0 -8
- data/lib/riffer/guardrails/result.rb +0 -17
- data/lib/riffer/guardrails/runner.rb +2 -11
- data/lib/riffer/guardrails/tripwire.rb +0 -8
- data/lib/riffer/guardrails.rb +0 -2
- data/lib/riffer/helpers/boolean.rb +0 -4
- data/lib/riffer/helpers/call_or_value.rb +0 -3
- data/lib/riffer/helpers/deep_dup.rb +37 -0
- data/lib/riffer/helpers/dependencies.rb +0 -4
- data/lib/riffer/helpers/identifier.rb +3 -12
- data/lib/riffer/helpers/validate.rb +42 -0
- data/lib/riffer/mcp/authenticated_tool.rb +4 -12
- data/lib/riffer/mcp/client.rb +0 -7
- data/lib/riffer/mcp/manifest.rb +3 -7
- data/lib/riffer/mcp/registration.rb +0 -10
- data/lib/riffer/mcp/registry.rb +0 -9
- data/lib/riffer/mcp/search_tool.rb +0 -4
- data/lib/riffer/mcp/tool.rb +1 -4
- data/lib/riffer/mcp/tool_factory.rb +3 -6
- data/lib/riffer/mcp.rb +2 -23
- data/lib/riffer/messages/assistant/reasoning_part.rb +73 -0
- data/lib/riffer/messages/assistant/tool_call.rb +55 -0
- data/lib/riffer/messages/assistant.rb +43 -15
- data/lib/riffer/messages/base.rb +5 -33
- data/lib/riffer/messages/system.rb +8 -1
- data/lib/riffer/messages/tool.rb +15 -13
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +16 -40
- data/lib/riffer/messages/user.rb +11 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +15 -31
- data/lib/riffer/params.rb +15 -39
- data/lib/riffer/providers/amazon_bedrock.rb +104 -44
- data/lib/riffer/providers/anthropic.rb +11 -26
- data/lib/riffer/providers/azure_open_ai.rb +3 -8
- data/lib/riffer/providers/base.rb +30 -32
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +6 -12
- data/lib/riffer/providers/mock.rb +18 -27
- data/lib/riffer/providers/open_ai.rb +12 -21
- data/lib/riffer/providers/open_router.rb +109 -33
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +19 -12
- data/lib/riffer/registrable.rb +11 -45
- data/lib/riffer/runner/fibers.rb +1 -6
- data/lib/riffer/runner/sequential.rb +0 -1
- data/lib/riffer/runner/threaded.rb +0 -3
- data/lib/riffer/runner.rb +0 -3
- data/lib/riffer/skills/activate_tool.rb +0 -3
- data/lib/riffer/skills/adapter.rb +0 -8
- data/lib/riffer/skills/backend.rb +2 -7
- data/lib/riffer/skills/config.rb +13 -17
- data/lib/riffer/skills/context.rb +0 -29
- data/lib/riffer/skills/filesystem_backend.rb +0 -7
- data/lib/riffer/skills/frontmatter.rb +2 -16
- data/lib/riffer/skills/markdown_adapter.rb +3 -6
- data/lib/riffer/skills/xml_adapter.rb +0 -3
- data/lib/riffer/stream_events/base.rb +0 -3
- data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
- data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
- data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
- data/lib/riffer/stream_events/interrupt.rb +2 -12
- data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
- data/lib/riffer/stream_events/reasoning_done.rb +6 -8
- data/lib/riffer/stream_events/skill_activation.rb +0 -3
- data/lib/riffer/stream_events/text_delta.rb +0 -2
- data/lib/riffer/stream_events/text_done.rb +0 -2
- data/lib/riffer/stream_events/token_usage_done.rb +0 -2
- data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
- data/lib/riffer/stream_events/tool_call_done.rb +0 -5
- data/lib/riffer/stream_events/web_search_done.rb +0 -3
- data/lib/riffer/stream_events/web_search_status.rb +1 -5
- data/lib/riffer/testing/minitest.rb +4 -5
- data/lib/riffer/testing.rb +5 -38
- data/lib/riffer/tool.rb +2 -28
- data/lib/riffer/tools/response.rb +3 -32
- data/lib/riffer/tools/runtime/fibers.rb +0 -6
- data/lib/riffer/tools/runtime/inline.rb +0 -1
- data/lib/riffer/tools/runtime/threaded.rb +0 -6
- data/lib/riffer/tools/runtime.rb +8 -30
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +5 -9
- data/lib/riffer/tracing/no_op.rb +0 -7
- data/lib/riffer/tracing/otel.rb +7 -16
- data/lib/riffer/tracing/stream_recorder.rb +0 -7
- data/lib/riffer/tracing.rb +4 -27
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -30
- data/sig/generated/riffer/agent/config.rbs +24 -46
- data/sig/generated/riffer/agent/context.rbs +2 -26
- data/sig/generated/riffer/agent/outcome.rbs +0 -20
- data/sig/generated/riffer/agent/response.rbs +4 -27
- data/sig/generated/riffer/agent/run.rbs +12 -31
- data/sig/generated/riffer/agent/serializer.rbs +2 -27
- data/sig/generated/riffer/agent/session/repair.rbs +2 -10
- data/sig/generated/riffer/agent/session.rbs +10 -36
- data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
- data/sig/generated/riffer/agent/structured_output.rbs +0 -7
- data/sig/generated/riffer/agent.rbs +25 -125
- data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
- data/sig/generated/riffer/config/anthropic.rbs +15 -0
- data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
- data/sig/generated/riffer/config/evals.rbs +13 -0
- data/sig/generated/riffer/config/files.rbs +43 -0
- data/sig/generated/riffer/config/gemini.rbs +15 -0
- data/sig/generated/riffer/config/mcp.rbs +19 -0
- data/sig/generated/riffer/config/open_ai.rbs +21 -0
- data/sig/generated/riffer/config/open_router.rbs +15 -0
- data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
- data/sig/generated/riffer/config/pricing.rbs +31 -0
- data/sig/generated/riffer/config/skills.rbs +19 -0
- data/sig/generated/riffer/config/tracing.rbs +25 -0
- data/sig/generated/riffer/config.rbs +2 -307
- data/sig/generated/riffer/evals/evaluator.rbs +12 -21
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +4 -8
- data/sig/generated/riffer/evals/result.rbs +1 -11
- data/sig/generated/riffer/evals/run_result.rbs +0 -12
- data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
- data/sig/generated/riffer/files/resolver.rbs +6 -13
- data/sig/generated/riffer/guardrail.rbs +2 -22
- data/sig/generated/riffer/guardrails/modification.rbs +0 -6
- data/sig/generated/riffer/guardrails/result.rbs +0 -15
- data/sig/generated/riffer/guardrails/runner.rbs +0 -11
- data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
- data/sig/generated/riffer/guardrails.rbs +0 -2
- data/sig/generated/riffer/helpers/boolean.rbs +0 -4
- data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
- data/sig/generated/riffer/helpers/deep_dup.rbs +13 -0
- data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
- data/sig/generated/riffer/helpers/identifier.rbs +0 -12
- data/sig/generated/riffer/helpers/validate.rbs +21 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
- data/sig/generated/riffer/mcp/client.rbs +0 -7
- data/sig/generated/riffer/mcp/manifest.rbs +3 -7
- data/sig/generated/riffer/mcp/registration.rbs +0 -10
- data/sig/generated/riffer/mcp/registry.rbs +0 -9
- data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
- data/sig/generated/riffer/mcp.rbs +2 -22
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +45 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +34 -0
- data/sig/generated/riffer/messages/assistant.rbs +25 -31
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +4 -1
- data/sig/generated/riffer/messages/tool.rbs +4 -10
- data/sig/generated/riffer/messages/user/file_part.rbs +76 -0
- data/sig/generated/riffer/messages/user.rbs +7 -5
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +7 -31
- data/sig/generated/riffer/params.rbs +8 -38
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +33 -29
- data/sig/generated/riffer/providers/anthropic.rbs +2 -12
- data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
- data/sig/generated/riffer/providers/base.rbs +18 -34
- data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
- data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
- data/sig/generated/riffer/providers/gemini.rbs +4 -10
- data/sig/generated/riffer/providers/mock.rbs +6 -27
- data/sig/generated/riffer/providers/open_ai.rbs +6 -13
- data/sig/generated/riffer/providers/open_router.rbs +37 -18
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +9 -12
- data/sig/generated/riffer/registrable.rbs +0 -45
- data/sig/generated/riffer/runner/fibers.rbs +0 -6
- data/sig/generated/riffer/runner/sequential.rbs +0 -1
- data/sig/generated/riffer/runner/threaded.rbs +0 -3
- data/sig/generated/riffer/runner.rbs +0 -3
- data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
- data/sig/generated/riffer/skills/adapter.rbs +0 -8
- data/sig/generated/riffer/skills/backend.rbs +2 -7
- data/sig/generated/riffer/skills/config.rbs +6 -17
- data/sig/generated/riffer/skills/context.rbs +0 -29
- data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
- data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
- data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
- data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
- data/sig/generated/riffer/stream_events/base.rbs +0 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
- data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
- data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
- data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
- data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +4 -6
- data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
- data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
- data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
- data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
- data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
- data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
- data/sig/generated/riffer/testing.rbs +0 -38
- data/sig/generated/riffer/tool.rbs +0 -27
- data/sig/generated/riffer/tools/response.rbs +3 -29
- data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
- data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
- data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
- data/sig/generated/riffer/tools/runtime.rbs +4 -26
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +6 -9
- data/sig/generated/riffer/tracing/no_op.rbs +0 -7
- data/sig/generated/riffer/tracing/otel.rbs +7 -16
- data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
- data/sig/generated/riffer/tracing.rbs +4 -23
- data/sig/generated/riffer.rbs +2 -29
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- data/sig/manual/riffer/helpers/validate.rbs +5 -0
- metadata +39 -3
- data/sig/generated/riffer/messages/file_part.rbs +0 -101
|
@@ -5,18 +5,8 @@ require "json"
|
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "uri"
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# and assign it to <tt>Riffer.config.gemini.client</tt>. Any object
|
|
11
|
-
# implementing +post+ and +post_stream+ with these contracts works there —
|
|
12
|
-
# the class is a default implementation, not a required base.
|
|
13
|
-
#
|
|
14
|
-
# Riffer.configure do |config|
|
|
15
|
-
# config.gemini.client = Riffer::Providers::Gemini::Client.new(
|
|
16
|
-
# api_key: ENV["GEMINI_API_KEY"],
|
|
17
|
-
# read_timeout: 120
|
|
18
|
-
# )
|
|
19
|
-
# end
|
|
8
|
+
# <tt>Riffer.config.gemini.client</tt> accepts any object implementing +post+ and +post_stream+
|
|
9
|
+
# with these contracts; this class is the default, not a required base.
|
|
20
10
|
class Riffer::Providers::Gemini::Client
|
|
21
11
|
# @rbs @api_key: String?
|
|
22
12
|
# @rbs @base_url: String
|
|
@@ -43,8 +33,7 @@ class Riffer::Providers::Gemini::Client
|
|
|
43
33
|
@proxy_port = proxy_port
|
|
44
34
|
end
|
|
45
35
|
|
|
46
|
-
#
|
|
47
|
-
# Raises Riffer::Error when the API responds with a non-success status.
|
|
36
|
+
# Raises Riffer::Error on a non-success status.
|
|
48
37
|
#--
|
|
49
38
|
#: (String, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
50
39
|
def post(path, body)
|
|
@@ -54,9 +43,7 @@ class Riffer::Providers::Gemini::Client
|
|
|
54
43
|
JSON.parse(response.body, symbolize_names: true)
|
|
55
44
|
end
|
|
56
45
|
|
|
57
|
-
#
|
|
58
|
-
# they arrive. Raises Riffer::Error when the API responds with a
|
|
59
|
-
# non-success status.
|
|
46
|
+
# Raises Riffer::Error on a non-success status.
|
|
60
47
|
#--
|
|
61
48
|
#: (String, Hash[Symbol, untyped]) { (String) -> void } -> void
|
|
62
49
|
def post_stream(path, body, &block)
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "securerandom"
|
|
6
6
|
|
|
7
|
-
# Google Gemini provider for Gemini models via the Gemini REST API.
|
|
8
7
|
class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
9
8
|
VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
|
|
10
9
|
|
|
@@ -29,7 +28,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
29
28
|
"FINISH_REASON_UNSPECIFIED" => :other,
|
|
30
29
|
}.freeze #: Hash[String, Symbol]
|
|
31
30
|
|
|
32
|
-
# The GenAI semconv well-known provider name.
|
|
33
31
|
#--
|
|
34
32
|
#: () -> String
|
|
35
33
|
def self.semconv_provider_name
|
|
@@ -37,7 +35,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
37
35
|
end
|
|
38
36
|
|
|
39
37
|
#--
|
|
40
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
38
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
41
39
|
def file_delivery(_file)
|
|
42
40
|
:base64
|
|
43
41
|
end
|
|
@@ -76,10 +74,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
76
74
|
}]
|
|
77
75
|
end
|
|
78
76
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# stripped here rather than mapped. Native labels would arrive with a Vertex
|
|
82
|
-
# adapter. See docs/CONFIGURATION.md.
|
|
77
|
+
# The Gemini Developer API has no request labels field and rejects unknown
|
|
78
|
+
# body fields, so :tags reach observability only.
|
|
83
79
|
generation_config = options.except(:tools, :structured_output, :tags)
|
|
84
80
|
|
|
85
81
|
if structured_output
|
|
@@ -144,8 +140,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
144
140
|
build_finish_reason(response.dig(:candidates, 0, :finishReason), tool_calls: has_function_call)
|
|
145
141
|
end
|
|
146
142
|
|
|
147
|
-
# Gemini reports STOP even when the candidate carries functionCall parts,
|
|
148
|
-
# so tool-call presence overrides the raw value.
|
|
149
143
|
#--
|
|
150
144
|
#: (String?, tool_calls: bool) -> Riffer::Providers::FinishReason?
|
|
151
145
|
def build_finish_reason(raw_reason, tool_calls:)
|
|
@@ -153,18 +147,18 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
153
147
|
|
|
154
148
|
raw = raw_reason.to_s
|
|
155
149
|
reason = FINISH_REASONS.fetch(raw, :other)
|
|
150
|
+
# Gemini reports STOP even when the candidate carries functionCall parts.
|
|
156
151
|
reason = :tool_calls if reason == :stop && tool_calls
|
|
157
152
|
Riffer::Providers::FinishReason.new(reason: reason, raw: raw)
|
|
158
153
|
end
|
|
159
154
|
|
|
160
|
-
# Gemini reports thinking tokens outside +candidatesTokenCount+;
|
|
161
|
-
# TokenUsage's output includes them.
|
|
162
155
|
#--
|
|
163
156
|
#: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage
|
|
164
157
|
def build_token_usage(usage)
|
|
165
158
|
apply_pricing(
|
|
166
159
|
Riffer::Providers::TokenUsage.new(
|
|
167
160
|
input_tokens: usage[:promptTokenCount] || 0,
|
|
161
|
+
# Gemini reports thinking tokens outside candidatesTokenCount.
|
|
168
162
|
output_tokens: (usage[:candidatesTokenCount] || 0) + (usage[:thoughtsTokenCount] || 0),
|
|
169
163
|
cache_read_tokens: usage[:cachedContentTokenCount],
|
|
170
164
|
),
|
|
@@ -287,7 +281,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
287
281
|
end
|
|
288
282
|
|
|
289
283
|
#--
|
|
290
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
284
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
291
285
|
def convert_file_part_to_gemini_format(file)
|
|
292
286
|
{ inlineData: { mimeType: file.media_type, data: file.data } }
|
|
293
287
|
end
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Mock provider for mocking LLM responses in tests; no external gems required.
|
|
5
4
|
class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
6
5
|
# @rbs @responses: Array[Hash[Symbol, untyped]]
|
|
7
6
|
# @rbs @current_index: Integer
|
|
8
7
|
# @rbs @stubbed_responses: Array[Hash[Symbol, untyped]]
|
|
9
8
|
|
|
10
|
-
# Returns the skill adapter for the mock model — XML when the model name
|
|
11
|
-
# contains +claude+ (mirroring a real Claude provider), else Markdown.
|
|
12
9
|
#--
|
|
13
10
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
14
11
|
def self.skills_adapter(model = nil)
|
|
12
|
+
# Mirrors the adapter a real Claude provider picks.
|
|
15
13
|
return Riffer::Skills::XmlAdapter if model&.include?("claude")
|
|
16
14
|
|
|
17
15
|
Riffer::Skills::MarkdownAdapter
|
|
18
16
|
end
|
|
19
17
|
|
|
20
|
-
# The GenAI semconv well-known provider name.
|
|
21
18
|
#--
|
|
22
19
|
#: () -> String
|
|
23
20
|
def self.semconv_provider_name
|
|
24
21
|
"mock"
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
# Array of recorded method calls for assertions.
|
|
28
24
|
attr_reader :calls #: Array[Hash[Symbol, untyped]] # @dynamic calls
|
|
29
25
|
|
|
30
|
-
# +responses:+ pre-configures canned responses (same shape as
|
|
31
|
-
# +#stub_response+) for standalone use; agent tests queue responses on
|
|
32
|
-
# <tt>agent.provider</tt> via +#stub_response+ instead.
|
|
33
|
-
#
|
|
34
|
-
# Riffer::Providers::Mock.new(responses: [
|
|
35
|
-
# {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
|
|
36
|
-
# {content: "Final answer"}
|
|
37
|
-
# ])
|
|
38
|
-
#
|
|
39
26
|
#--
|
|
40
27
|
#: (?responses: Array[Hash[Symbol, untyped]]) -> void
|
|
41
28
|
def initialize(responses: [])
|
|
@@ -46,28 +33,18 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
46
33
|
@stubbed_responses = []
|
|
47
34
|
end
|
|
48
35
|
|
|
49
|
-
# Stubs the next response; call repeatedly to queue several. +finish_reason+
|
|
50
|
-
# defaults to +:tool_calls+ when tool calls are present, else +:stop+.
|
|
51
|
-
#
|
|
52
|
-
# provider.stub_response("Hello")
|
|
53
|
-
# provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
|
|
54
|
-
# provider.stub_response("Final response",
|
|
55
|
-
# token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
56
|
-
# provider.stub_response("Truncated...", finish_reason: :length)
|
|
57
|
-
#
|
|
58
36
|
#--
|
|
59
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?) -> void
|
|
60
|
-
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil)
|
|
37
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?, ?reasoning: Array[Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart]) -> void
|
|
38
|
+
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
|
|
61
39
|
@stubbed_responses << normalize_response(
|
|
62
40
|
content: content,
|
|
63
41
|
tool_calls: tool_calls,
|
|
64
42
|
token_usage: token_usage,
|
|
65
43
|
finish_reason: finish_reason,
|
|
44
|
+
reasoning: reasoning,
|
|
66
45
|
)
|
|
67
46
|
end
|
|
68
47
|
|
|
69
|
-
# Clears all stubbed responses.
|
|
70
|
-
#
|
|
71
48
|
#--
|
|
72
49
|
#: () -> void
|
|
73
50
|
def clear_stubs
|
|
@@ -92,6 +69,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
92
69
|
role: response[:role] || "assistant",
|
|
93
70
|
content: response[:content] || "",
|
|
94
71
|
tool_calls: formatted_tool_calls,
|
|
72
|
+
reasoning: (response[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
|
|
95
73
|
token_usage: response[:token_usage],
|
|
96
74
|
finish_reason: response[:finish_reason] || (formatted_tool_calls.empty? ? :stop : :tool_calls),
|
|
97
75
|
}
|
|
@@ -129,6 +107,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
129
107
|
reason ? Riffer::Providers::FinishReason.new(reason: reason) : nil
|
|
130
108
|
end
|
|
131
109
|
|
|
110
|
+
#--
|
|
111
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
112
|
+
def extract_reasoning(response)
|
|
113
|
+
response[:reasoning] || []
|
|
114
|
+
end
|
|
115
|
+
|
|
132
116
|
#--
|
|
133
117
|
#: (untyped) -> String
|
|
134
118
|
def extract_content(response)
|
|
@@ -149,6 +133,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
149
133
|
tool_calls = response[:tool_calls] || []
|
|
150
134
|
token_usage = response[:token_usage]
|
|
151
135
|
web_search = response[:web_search]
|
|
136
|
+
reasoning = response[:reasoning] || []
|
|
152
137
|
|
|
153
138
|
if web_search
|
|
154
139
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("in_progress")
|
|
@@ -161,6 +146,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
161
146
|
)
|
|
162
147
|
end
|
|
163
148
|
|
|
149
|
+
reasoning.each do |part|
|
|
150
|
+
text = part.text
|
|
151
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(text) if text
|
|
152
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
153
|
+
end
|
|
154
|
+
|
|
164
155
|
unless full_content.empty?
|
|
165
156
|
content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
|
|
166
157
|
content_parts.each do |part|
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# OpenAI provider for GPT models. Requires the +openai+ gem.
|
|
5
4
|
class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
6
5
|
WEB_SEARCH_TOOL_TYPE = "web_search_preview" #: String
|
|
7
6
|
|
|
8
|
-
# The Responses API has no finish_reason field
|
|
9
|
-
#
|
|
10
|
-
# with <tt>incomplete_details.reason</tt>, so that branch nests one level.
|
|
7
|
+
# The Responses API has no finish_reason field, and an +incomplete+ status is only meaningful
|
|
8
|
+
# together with <tt>incomplete_details.reason</tt>.
|
|
11
9
|
FINISH_REASONS = {
|
|
12
10
|
"completed" => :stop,
|
|
13
11
|
"incomplete" => {
|
|
@@ -20,7 +18,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
20
18
|
"queued" => :other,
|
|
21
19
|
}.freeze #: Hash[String, Symbol | Hash[String, Symbol]]
|
|
22
20
|
|
|
23
|
-
# The GenAI semconv well-known provider name.
|
|
24
21
|
#--
|
|
25
22
|
#: () -> String
|
|
26
23
|
def self.semconv_provider_name
|
|
@@ -35,7 +32,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
35
32
|
end
|
|
36
33
|
|
|
37
34
|
#--
|
|
38
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
35
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
39
36
|
def file_delivery(file)
|
|
40
37
|
file.image? ? :url : :base64
|
|
41
38
|
end
|
|
@@ -48,12 +45,11 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
48
45
|
Riffer.config.openai.client
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
# Compacted so an unset value stays absent: the SDK reads +OPENAI_API_KEY+ /
|
|
52
|
-
# +OPENAI_BASE_URL+ only for a missing argument, and an explicit nil would
|
|
53
|
-
# suppress that fallback.
|
|
54
48
|
#--
|
|
55
49
|
#: () -> untyped
|
|
56
50
|
def build_client
|
|
51
|
+
# The SDK falls back to +OPENAI_API_KEY+ / +OPENAI_BASE_URL+ only for a missing argument; an
|
|
52
|
+
# explicit nil suppresses that fallback.
|
|
57
53
|
::OpenAI::Client.new(**{
|
|
58
54
|
api_key: Riffer.config.openai.api_key,
|
|
59
55
|
base_url: Riffer.config.openai.base_url,
|
|
@@ -80,9 +76,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
80
76
|
} #: Hash[Symbol, untyped]
|
|
81
77
|
|
|
82
78
|
unless tags.empty?
|
|
83
|
-
params[:metadata] = tags
|
|
84
|
-
# The reserved user_id also maps to the native safety identifier while
|
|
85
|
-
# staying in metadata as an ordinary tag.
|
|
79
|
+
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
86
80
|
user_id = tags["user_id"]
|
|
87
81
|
params[:safety_identifier] = user_id if user_id
|
|
88
82
|
end
|
|
@@ -164,7 +158,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
164
158
|
Riffer::Providers::FinishReason.new(reason: reason, raw: detail || status)
|
|
165
159
|
end
|
|
166
160
|
|
|
167
|
-
# The nested field that names the cause behind an ambiguous status.
|
|
168
161
|
#--
|
|
169
162
|
#: (untyped, String) -> String?
|
|
170
163
|
def finish_detail(response, status)
|
|
@@ -254,9 +247,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
254
247
|
end
|
|
255
248
|
end
|
|
256
249
|
ensure
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
# socket leaks until GC. close is idempotent and a no-op after EOF.
|
|
250
|
+
# The SDK doesn't close the SSE socket when iteration is interrupted (raise / fiber
|
|
251
|
+
# cancellation), so it leaks until GC. close is idempotent and a no-op after EOF.
|
|
260
252
|
stream.close
|
|
261
253
|
end
|
|
262
254
|
|
|
@@ -296,7 +288,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
296
288
|
#--
|
|
297
289
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
298
290
|
def handle_reasoning_summary_text_done(event, state:, yielder:)
|
|
299
|
-
yielder
|
|
291
|
+
yield_reasoning_done(yielder, event.text)
|
|
300
292
|
end
|
|
301
293
|
|
|
302
294
|
#--
|
|
@@ -348,8 +340,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
348
340
|
action = event.item.action
|
|
349
341
|
case action
|
|
350
342
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
|
|
351
|
-
# OpenPage carries a url but no query or sources, so it doesn't fit
|
|
352
|
-
# WebSearchDone — emit as a status notification instead.
|
|
343
|
+
# OpenPage carries a url but no query or sources, so it doesn't fit WebSearchDone.
|
|
353
344
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("open_page", url: action.url)
|
|
354
345
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
|
|
355
346
|
sources = (action.sources || []).map { |s| { title: nil, url: s.url } }
|
|
@@ -402,7 +393,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
402
393
|
type: "function_call",
|
|
403
394
|
call_id: tc.call_id,
|
|
404
395
|
name: encode_tool_name(tc.name),
|
|
405
|
-
arguments: tc.arguments
|
|
396
|
+
arguments: tc.arguments,
|
|
406
397
|
}
|
|
407
398
|
end
|
|
408
399
|
items
|
|
@@ -410,7 +401,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
410
401
|
end
|
|
411
402
|
|
|
412
403
|
#--
|
|
413
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
404
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
414
405
|
def convert_file_part_to_openai_format(file)
|
|
415
406
|
if file.image?
|
|
416
407
|
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
# OpenRouter provider (https://openrouter.ai). Requires the +openai+ gem —
|
|
7
|
-
# OpenRouter exposes an OpenAI-compatible endpoint, so this reuses the OpenAI
|
|
8
|
-
# SDK with a +base_url+ override. +api_key+ resolves from config, then
|
|
9
|
-
# +OPENROUTER_API_KEY+.
|
|
10
6
|
class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
11
7
|
BASE_URL = "https://openrouter.ai/api/v1" #: String
|
|
12
8
|
|
|
@@ -19,7 +15,27 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
19
15
|
"error" => :error,
|
|
20
16
|
}.freeze #: Hash[String, Symbol]
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
REASONING_TYPES = {
|
|
19
|
+
"reasoning.text" => :text,
|
|
20
|
+
"reasoning.summary" => :summary,
|
|
21
|
+
"reasoning.encrypted" => :encrypted,
|
|
22
|
+
}.freeze #: Hash[String, Symbol]
|
|
23
|
+
|
|
24
|
+
# The +reasoning_details+ formats OpenRouter documents. Only parts tagged with
|
|
25
|
+
# one of them are replayed, so parts from other adapters, or with no format,
|
|
26
|
+
# never reach the request.
|
|
27
|
+
REASONING_FORMATS = %w[
|
|
28
|
+
unknown
|
|
29
|
+
openai-responses-v1
|
|
30
|
+
azure-openai-responses-v1
|
|
31
|
+
bedrock-openai-responses-v1
|
|
32
|
+
bedrock-xai-responses-v1
|
|
33
|
+
xai-responses-v1
|
|
34
|
+
meta-responses-v1
|
|
35
|
+
anthropic-claude-v1
|
|
36
|
+
google-gemini-v1
|
|
37
|
+
].freeze #: Array[String]
|
|
38
|
+
|
|
23
39
|
#--
|
|
24
40
|
#: () -> String
|
|
25
41
|
def self.semconv_provider_name
|
|
@@ -34,7 +50,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
34
50
|
end
|
|
35
51
|
|
|
36
52
|
#--
|
|
37
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
53
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
38
54
|
def file_delivery(file)
|
|
39
55
|
file.image? ? :url : :base64
|
|
40
56
|
end
|
|
@@ -47,15 +63,12 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
47
63
|
Riffer.config.openrouter.client
|
|
48
64
|
end
|
|
49
65
|
|
|
50
|
-
# Deliberately not compacted: this borrows the OpenAI SDK to talk to a
|
|
51
|
-
# different vendor, so omitting an unset +api_key+ would let the SDK fall
|
|
52
|
-
# back to +OPENAI_API_KEY+ and send an OpenAI credential to OpenRouter.
|
|
53
|
-
# Passing nil raises in the SDK instead. +OPENROUTER_API_KEY+ is read here
|
|
54
|
-
# rather than left to the SDK for the same reason.
|
|
55
66
|
#--
|
|
56
67
|
#: () -> untyped
|
|
57
68
|
def build_client
|
|
58
69
|
api_key = Riffer.config.openrouter.api_key || ENV.fetch("OPENROUTER_API_KEY", nil)
|
|
70
|
+
# Pass a nil api_key rather than omitting it: an omitted key lets the SDK
|
|
71
|
+
# fall back to OPENAI_API_KEY and send an OpenAI credential to OpenRouter.
|
|
59
72
|
::OpenAI::Client.new(api_key: api_key, base_url: BASE_URL)
|
|
60
73
|
end
|
|
61
74
|
|
|
@@ -74,7 +87,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
74
87
|
} #: Hash[Symbol, untyped]
|
|
75
88
|
|
|
76
89
|
unless tags.empty?
|
|
77
|
-
params[:metadata] = tags
|
|
90
|
+
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
78
91
|
# OpenRouter exposes the legacy Chat Completions user field rather than
|
|
79
92
|
# safety_identifier; the reserved user_id maps there and stays in metadata.
|
|
80
93
|
user = tags["user_id"]
|
|
@@ -137,8 +150,6 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
137
150
|
build_finish_reason(choice&.finish_reason, native: native_finish_reason(choice))
|
|
138
151
|
end
|
|
139
152
|
|
|
140
|
-
# +native+ is the upstream model's own finish reason, which OpenRouter
|
|
141
|
-
# reports alongside its normalized one; it wins as +raw+ when present.
|
|
142
153
|
#--
|
|
143
154
|
#: (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
|
|
144
155
|
def build_finish_reason(finish_reason, native: nil)
|
|
@@ -147,15 +158,17 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
147
158
|
normalized = finish_reason.to_s
|
|
148
159
|
return nil if normalized.empty?
|
|
149
160
|
|
|
161
|
+
# OpenRouter reports the upstream model's own finish reason alongside its
|
|
162
|
+
# normalized one.
|
|
150
163
|
raw = native.to_s.empty? ? normalized : native.to_s
|
|
151
164
|
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(normalized, :other), raw: raw)
|
|
152
165
|
end
|
|
153
166
|
|
|
154
|
-
# +native_finish_reason+ is outside the OpenAI schema, so it is only
|
|
155
|
-
# reachable through the SDK model's raw data hash.
|
|
156
167
|
#--
|
|
157
168
|
#: (untyped) -> untyped
|
|
158
169
|
def native_finish_reason(choice)
|
|
170
|
+
# Outside the OpenAI schema, so only reachable through the SDK model's raw
|
|
171
|
+
# data hash.
|
|
159
172
|
choice && choice.to_h[:native_finish_reason]
|
|
160
173
|
end
|
|
161
174
|
|
|
@@ -187,6 +200,39 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
187
200
|
end
|
|
188
201
|
end
|
|
189
202
|
|
|
203
|
+
#--
|
|
204
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
205
|
+
def extract_reasoning(response)
|
|
206
|
+
typed_response = response #: OpenAI::Models::Chat::ChatCompletion
|
|
207
|
+
message = typed_response.choices.first&.message
|
|
208
|
+
details = message && reasoning_details(message)
|
|
209
|
+
(details || []).filter_map { |detail| build_reasoning_part(detail) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The openai gem's typed models strip fields outside OpenAI's spec, so
|
|
213
|
+
# +reasoning_details+ is only reachable through the model's raw data hash.
|
|
214
|
+
#--
|
|
215
|
+
#: (untyped) -> Array[Hash[Symbol, untyped]]?
|
|
216
|
+
def reasoning_details(model)
|
|
217
|
+
model[:reasoning_details]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
#--
|
|
221
|
+
#: (Hash[Symbol, untyped]) -> Riffer::Messages::Assistant::ReasoningPart?
|
|
222
|
+
def build_reasoning_part(detail)
|
|
223
|
+
type = REASONING_TYPES.fetch(detail[:type], nil)
|
|
224
|
+
return nil unless type
|
|
225
|
+
|
|
226
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
227
|
+
type: type,
|
|
228
|
+
text: detail[:text] || detail[:summary],
|
|
229
|
+
data: detail[:data],
|
|
230
|
+
signature: detail[:signature],
|
|
231
|
+
id: detail[:id],
|
|
232
|
+
format: detail[:format],
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
190
236
|
#--
|
|
191
237
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
192
238
|
def execute_stream(params, yielder)
|
|
@@ -196,17 +242,14 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
196
242
|
|
|
197
243
|
state = {
|
|
198
244
|
text: +"",
|
|
199
|
-
|
|
245
|
+
reasoning_details: {},
|
|
200
246
|
tool_calls: {},
|
|
201
247
|
finish_reason: nil,
|
|
202
248
|
native_finish_reason: nil,
|
|
203
249
|
} #: Hash[Symbol, untyped]
|
|
204
250
|
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
# events. We want raw ChatCompletionChunk objects with
|
|
208
|
-
# +choices.first.delta+ so we can map deltas to Riffer::StreamEvents
|
|
209
|
-
# ourselves.
|
|
251
|
+
# stream_raw, not stream: stream aggregates chunks into higher-level
|
|
252
|
+
# events, but mapping to Riffer::StreamEvents needs the raw deltas.
|
|
210
253
|
stream = client.chat.completions.stream_raw(**stream_params)
|
|
211
254
|
begin
|
|
212
255
|
stream.each do |chunk|
|
|
@@ -223,7 +266,10 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
223
266
|
emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
|
|
224
267
|
|
|
225
268
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
226
|
-
|
|
269
|
+
state[:reasoning_details].each_value do |detail|
|
|
270
|
+
part = build_reasoning_part(detail)
|
|
271
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part) if part
|
|
272
|
+
end
|
|
227
273
|
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
|
|
228
274
|
end
|
|
229
275
|
|
|
@@ -264,14 +310,26 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
264
310
|
#--
|
|
265
311
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
266
312
|
def handle_reasoning_delta(delta, state:, yielder:)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
313
|
+
details = reasoning_details(delta)
|
|
314
|
+
return if details.nil?
|
|
315
|
+
|
|
316
|
+
details.each do |detail|
|
|
317
|
+
accumulate_reasoning_detail(state[:reasoning_details], detail)
|
|
318
|
+
text = detail[:text] || detail[:summary]
|
|
319
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(text) unless text.nil? || text.empty?
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# OpenRouter streams one reasoning block as many fragments sharing an
|
|
324
|
+
# +index+: prose and payload arrive in pieces to concatenate, while the
|
|
325
|
+
# signature, id, and format arrive once, often on the last fragment. Keying
|
|
326
|
+
# on +type+ as well keeps index-less fragments of different kinds apart.
|
|
327
|
+
#--
|
|
328
|
+
#: (Hash[untyped, Hash[Symbol, untyped]], Hash[Symbol, untyped]) -> void
|
|
329
|
+
def accumulate_reasoning_detail(accumulated, detail)
|
|
330
|
+
entry = accumulated[[detail[:index], detail[:type]]] ||= { type: detail[:type] }
|
|
331
|
+
%i[text summary data].each { |key| (entry[key] ||= +"") << detail[key] if detail[key] }
|
|
332
|
+
%i[signature id format].each { |key| entry[key] = detail[key] if detail[key] }
|
|
275
333
|
end
|
|
276
334
|
|
|
277
335
|
#--
|
|
@@ -362,17 +420,35 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
362
420
|
type: "function",
|
|
363
421
|
function: {
|
|
364
422
|
name: encode_tool_name(tc.name),
|
|
365
|
-
arguments: tc.arguments
|
|
423
|
+
arguments: tc.arguments,
|
|
366
424
|
},
|
|
367
425
|
}
|
|
368
426
|
end
|
|
369
427
|
end
|
|
370
428
|
|
|
429
|
+
details = message.reasoning.filter_map do |part|
|
|
430
|
+
convert_reasoning_part_to_detail(part) if REASONING_FORMATS.include?(part.format)
|
|
431
|
+
end
|
|
432
|
+
msg[:reasoning_details] = details unless details.empty?
|
|
433
|
+
|
|
371
434
|
msg
|
|
372
435
|
end
|
|
373
436
|
|
|
374
437
|
#--
|
|
375
|
-
#: (Riffer::Messages::
|
|
438
|
+
#: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
|
|
439
|
+
def convert_reasoning_part_to_detail(part)
|
|
440
|
+
{
|
|
441
|
+
type: REASONING_TYPES.key(part.type),
|
|
442
|
+
(part.type == :summary ? :summary : :text) => part.text,
|
|
443
|
+
data: part.data,
|
|
444
|
+
signature: part.signature,
|
|
445
|
+
id: part.id,
|
|
446
|
+
format: part.format,
|
|
447
|
+
}.compact
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
#--
|
|
451
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
376
452
|
def convert_file_part_to_chat_completions_format(file)
|
|
377
453
|
if file.image?
|
|
378
454
|
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Resolves provider classes by identifier, combining the built-in REPO with
|
|
5
|
-
# consumer registrations added through +register+. Registration is not
|
|
6
|
-
# synchronized — register during boot, before concurrent generation begins.
|
|
7
4
|
module Riffer::Providers::Repository
|
|
8
5
|
extend self
|
|
9
6
|
|
|
@@ -22,11 +19,8 @@ module Riffer::Providers::Repository
|
|
|
22
19
|
|
|
23
20
|
@registrations = {} #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
|
|
24
21
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# Riffer::Providers::Repository.register(:jane) { MyApp::JaneProvider }
|
|
29
|
-
#
|
|
22
|
+
# Not synchronized — register during boot, before concurrent generation
|
|
23
|
+
# begins.
|
|
30
24
|
#--
|
|
31
25
|
#: ((String | Symbol)) { () -> singleton(Riffer::Providers::Base) } -> void
|
|
32
26
|
def register(identifier, &factory)
|
|
@@ -34,8 +28,6 @@ module Riffer::Providers::Repository
|
|
|
34
28
|
@key_for = nil
|
|
35
29
|
end
|
|
36
30
|
|
|
37
|
-
# Removes a custom registration by identifier, leaving any built-in of the
|
|
38
|
-
# same name intact.
|
|
39
31
|
#--
|
|
40
32
|
#: ((String | Symbol)) -> void
|
|
41
33
|
def unregister(identifier)
|
|
@@ -43,9 +35,6 @@ module Riffer::Providers::Repository
|
|
|
43
35
|
@key_for = nil
|
|
44
36
|
end
|
|
45
37
|
|
|
46
|
-
# Finds a provider class by identifier, preferring a custom registration over
|
|
47
|
-
# a built-in of the same name.
|
|
48
|
-
#
|
|
49
38
|
#--
|
|
50
39
|
#: ((String | Symbol)) -> singleton(Riffer::Providers::Base)?
|
|
51
40
|
def find(identifier)
|
|
@@ -53,7 +42,6 @@ module Riffer::Providers::Repository
|
|
|
53
42
|
(@registrations[key] || REPO[key])&.call
|
|
54
43
|
end
|
|
55
44
|
|
|
56
|
-
# Returns the registry identifier for a provider class, or nil when unregistered.
|
|
57
45
|
#--
|
|
58
46
|
#: (singleton(Riffer::Providers::Base)) -> Symbol?
|
|
59
47
|
def key_for(provider_class)
|