riffer 0.48.0 → 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 +19 -0
- data/docs/AGENTS.md +3 -5
- data/docs/AGENT_LIFECYCLE.md +13 -16
- data/docs/CONFIGURATION.md +22 -33
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +31 -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 +4 -45
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +0 -38
- data/lib/riffer/agent/run.rb +11 -43
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +25 -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 +10 -130
- 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 +1 -29
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +0 -8
- 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 +2 -7
- 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 +3 -8
- 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 +4 -21
- data/lib/riffer/messages/assistant/tool_call.rb +1 -9
- data/lib/riffer/messages/assistant.rb +1 -21
- data/lib/riffer/messages/base.rb +0 -12
- data/lib/riffer/messages/system.rb +0 -3
- data/lib/riffer/messages/tool.rb +0 -15
- data/lib/riffer/messages/user/file_part.rb +2 -30
- data/lib/riffer/messages/user.rb +0 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +3 -31
- data/lib/riffer/params.rb +8 -41
- data/lib/riffer/providers/amazon_bedrock.rb +101 -42
- data/lib/riffer/providers/anthropic.rb +9 -24
- data/lib/riffer/providers/azure_open_ai.rb +3 -8
- data/lib/riffer/providers/base.rb +7 -28
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +4 -10
- data/lib/riffer/providers/mock.rb +1 -26
- data/lib/riffer/providers/open_ai.rb +7 -17
- data/lib/riffer/providers/open_router.rb +105 -30
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +5 -14
- 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 +4 -20
- 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 +1 -5
- 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 +5 -26
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +3 -5
- 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 +12 -48
- 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 +1 -29
- data/sig/generated/riffer/agent/run.rbs +1 -27
- 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 +5 -121
- 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 +0 -28
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +0 -7
- 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 +0 -7
- 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 +0 -8
- 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 +4 -17
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +1 -9
- data/sig/generated/riffer/messages/assistant.rbs +10 -30
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +0 -3
- data/sig/generated/riffer/messages/tool.rbs +0 -12
- data/sig/generated/riffer/messages/user/file_part.rbs +0 -30
- data/sig/generated/riffer/messages/user.rbs +0 -4
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +0 -31
- data/sig/generated/riffer/params.rbs +4 -40
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +29 -25
- data/sig/generated/riffer/providers/anthropic.rbs +0 -10
- data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
- data/sig/generated/riffer/providers/base.rbs +0 -28
- 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 +0 -6
- data/sig/generated/riffer/providers/mock.rbs +0 -26
- data/sig/generated/riffer/providers/open_ai.rbs +2 -9
- data/sig/generated/riffer/providers/open_router.rbs +33 -14
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +5 -14
- 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 +0 -20
- 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 +1 -5
- 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 +2 -24
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +2 -5
- 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/validate.rbs +5 -0
- metadata +30 -1
|
@@ -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
|
|
@@ -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
|
),
|
|
@@ -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,16 +33,6 @@ 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
|
-
# provider.stub_response("Answer", reasoning: [{type: :text, text: "Thinking...", format: "mock-v1"}])
|
|
58
|
-
#
|
|
59
36
|
#--
|
|
60
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
|
|
61
38
|
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
|
|
@@ -68,8 +45,6 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
68
45
|
)
|
|
69
46
|
end
|
|
70
47
|
|
|
71
|
-
# Clears all stubbed responses.
|
|
72
|
-
#
|
|
73
48
|
#--
|
|
74
49
|
#: () -> void
|
|
75
50
|
def clear_stubs
|
|
@@ -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
|
|
@@ -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,10 +76,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
80
76
|
} #: Hash[Symbol, untyped]
|
|
81
77
|
|
|
82
78
|
unless tags.empty?
|
|
83
|
-
# Merged over any metadata set in model_options; a tag wins on a shared key.
|
|
84
79
|
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
85
|
-
# The reserved user_id also maps to the native safety identifier while
|
|
86
|
-
# staying in metadata as an ordinary tag.
|
|
87
80
|
user_id = tags["user_id"]
|
|
88
81
|
params[:safety_identifier] = user_id if user_id
|
|
89
82
|
end
|
|
@@ -165,7 +158,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
165
158
|
Riffer::Providers::FinishReason.new(reason: reason, raw: detail || status)
|
|
166
159
|
end
|
|
167
160
|
|
|
168
|
-
# The nested field that names the cause behind an ambiguous status.
|
|
169
161
|
#--
|
|
170
162
|
#: (untyped, String) -> String?
|
|
171
163
|
def finish_detail(response, status)
|
|
@@ -255,9 +247,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
255
247
|
end
|
|
256
248
|
end
|
|
257
249
|
ensure
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
# 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.
|
|
261
252
|
stream.close
|
|
262
253
|
end
|
|
263
254
|
|
|
@@ -349,8 +340,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
349
340
|
action = event.item.action
|
|
350
341
|
case action
|
|
351
342
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
|
|
352
|
-
# OpenPage carries a url but no query or sources, so it doesn't fit
|
|
353
|
-
# WebSearchDone — emit as a status notification instead.
|
|
343
|
+
# OpenPage carries a url but no query or sources, so it doesn't fit WebSearchDone.
|
|
354
344
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("open_page", url: action.url)
|
|
355
345
|
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
|
|
356
346
|
sources = (action.sources || []).map { |s| { title: nil, url: s.url } }
|
|
@@ -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
|
|
@@ -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,6 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
74
87
|
} #: Hash[Symbol, untyped]
|
|
75
88
|
|
|
76
89
|
unless tags.empty?
|
|
77
|
-
# Merged over any metadata set in model_options; a tag wins on a shared key.
|
|
78
90
|
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
79
91
|
# OpenRouter exposes the legacy Chat Completions user field rather than
|
|
80
92
|
# safety_identifier; the reserved user_id maps there and stays in metadata.
|
|
@@ -138,8 +150,6 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
138
150
|
build_finish_reason(choice&.finish_reason, native: native_finish_reason(choice))
|
|
139
151
|
end
|
|
140
152
|
|
|
141
|
-
# +native+ is the upstream model's own finish reason, which OpenRouter
|
|
142
|
-
# reports alongside its normalized one; it wins as +raw+ when present.
|
|
143
153
|
#--
|
|
144
154
|
#: (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
|
|
145
155
|
def build_finish_reason(finish_reason, native: nil)
|
|
@@ -148,15 +158,17 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
148
158
|
normalized = finish_reason.to_s
|
|
149
159
|
return nil if normalized.empty?
|
|
150
160
|
|
|
161
|
+
# OpenRouter reports the upstream model's own finish reason alongside its
|
|
162
|
+
# normalized one.
|
|
151
163
|
raw = native.to_s.empty? ? normalized : native.to_s
|
|
152
164
|
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(normalized, :other), raw: raw)
|
|
153
165
|
end
|
|
154
166
|
|
|
155
|
-
# +native_finish_reason+ is outside the OpenAI schema, so it is only
|
|
156
|
-
# reachable through the SDK model's raw data hash.
|
|
157
167
|
#--
|
|
158
168
|
#: (untyped) -> untyped
|
|
159
169
|
def native_finish_reason(choice)
|
|
170
|
+
# Outside the OpenAI schema, so only reachable through the SDK model's raw
|
|
171
|
+
# data hash.
|
|
160
172
|
choice && choice.to_h[:native_finish_reason]
|
|
161
173
|
end
|
|
162
174
|
|
|
@@ -188,6 +200,39 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
188
200
|
end
|
|
189
201
|
end
|
|
190
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
|
+
|
|
191
236
|
#--
|
|
192
237
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
193
238
|
def execute_stream(params, yielder)
|
|
@@ -197,17 +242,14 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
197
242
|
|
|
198
243
|
state = {
|
|
199
244
|
text: +"",
|
|
200
|
-
|
|
245
|
+
reasoning_details: {},
|
|
201
246
|
tool_calls: {},
|
|
202
247
|
finish_reason: nil,
|
|
203
248
|
native_finish_reason: nil,
|
|
204
249
|
} #: Hash[Symbol, untyped]
|
|
205
250
|
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
# events. We want raw ChatCompletionChunk objects with
|
|
209
|
-
# +choices.first.delta+ so we can map deltas to Riffer::StreamEvents
|
|
210
|
-
# ourselves.
|
|
251
|
+
# stream_raw, not stream: stream aggregates chunks into higher-level
|
|
252
|
+
# events, but mapping to Riffer::StreamEvents needs the raw deltas.
|
|
211
253
|
stream = client.chat.completions.stream_raw(**stream_params)
|
|
212
254
|
begin
|
|
213
255
|
stream.each do |chunk|
|
|
@@ -224,7 +266,10 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
224
266
|
emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
|
|
225
267
|
|
|
226
268
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
227
|
-
|
|
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
|
|
228
273
|
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
|
|
229
274
|
end
|
|
230
275
|
|
|
@@ -265,14 +310,26 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
265
310
|
#--
|
|
266
311
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
267
312
|
def handle_reasoning_delta(delta, state:, yielder:)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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] }
|
|
276
333
|
end
|
|
277
334
|
|
|
278
335
|
#--
|
|
@@ -369,9 +426,27 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
369
426
|
end
|
|
370
427
|
end
|
|
371
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
|
+
|
|
372
434
|
msg
|
|
373
435
|
end
|
|
374
436
|
|
|
437
|
+
#--
|
|
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
|
+
|
|
375
450
|
#--
|
|
376
451
|
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
377
452
|
def convert_file_part_to_chat_completions_format(file)
|
|
@@ -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)
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Normalized token usage for an LLM API call. Buckets carry the same
|
|
5
|
-
# meaning for every provider.
|
|
6
4
|
class Riffer::Providers::TokenUsage
|
|
7
|
-
# Builds a TokenUsage from a hash, or returns +usage+ unchanged when it is
|
|
8
|
-
# already a TokenUsage.
|
|
9
5
|
#--
|
|
10
6
|
#: ((Hash[Symbol, untyped] | Riffer::Providers::TokenUsage)) -> Riffer::Providers::TokenUsage
|
|
11
7
|
def self.from_hash(usage)
|
|
@@ -20,19 +16,19 @@ class Riffer::Providers::TokenUsage
|
|
|
20
16
|
)
|
|
21
17
|
end
|
|
22
18
|
|
|
23
|
-
#
|
|
19
|
+
# Normalized across providers: includes cache reads and writes.
|
|
24
20
|
attr_reader :input_tokens #: Integer # @dynamic input_tokens
|
|
25
21
|
|
|
26
|
-
#
|
|
22
|
+
# Includes reasoning/thinking tokens.
|
|
27
23
|
attr_reader :output_tokens #: Integer # @dynamic output_tokens
|
|
28
24
|
|
|
29
|
-
# Subset of +input_tokens
|
|
25
|
+
# Subset of +input_tokens+.
|
|
30
26
|
attr_reader :cache_write_tokens #: Integer? # @dynamic cache_write_tokens
|
|
31
27
|
|
|
32
|
-
# Subset of +input_tokens
|
|
28
|
+
# Subset of +input_tokens+.
|
|
33
29
|
attr_reader :cache_read_tokens #: Integer? # @dynamic cache_read_tokens
|
|
34
30
|
|
|
35
|
-
#
|
|
31
|
+
# For observability, not billing.
|
|
36
32
|
attr_reader :cost #: Float? # @dynamic cost
|
|
37
33
|
|
|
38
34
|
#--
|
|
@@ -45,16 +41,12 @@ class Riffer::Providers::TokenUsage
|
|
|
45
41
|
@cost = cost
|
|
46
42
|
end
|
|
47
43
|
|
|
48
|
-
# Returns the total number of tokens (input + output).
|
|
49
|
-
#
|
|
50
44
|
#--
|
|
51
45
|
#: () -> Integer
|
|
52
46
|
def total_tokens
|
|
53
47
|
input_tokens + output_tokens
|
|
54
48
|
end
|
|
55
49
|
|
|
56
|
-
# Combines two TokenUsage objects for cumulative tracking.
|
|
57
|
-
#
|
|
58
50
|
#--
|
|
59
51
|
#: (Riffer::Providers::TokenUsage) -> Riffer::Providers::TokenUsage
|
|
60
52
|
def +(other)
|
|
@@ -67,7 +59,6 @@ class Riffer::Providers::TokenUsage
|
|
|
67
59
|
)
|
|
68
60
|
end
|
|
69
61
|
|
|
70
|
-
# Converts the token usage to a hash; cache tokens and cost are omitted when nil.
|
|
71
62
|
#--
|
|
72
63
|
#: () -> Hash[Symbol, (Integer | Float)]
|
|
73
64
|
def to_h
|