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
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
require "base64"
|
|
5
5
|
|
|
6
|
-
# Amazon Bedrock provider for Claude and other foundation models. Requires the
|
|
7
|
-
# +aws-sdk-bedrockruntime+ gem.
|
|
8
6
|
class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
9
|
-
#
|
|
10
|
-
# cross-region (+us.anthropic.claude-...+) ids.
|
|
7
|
+
# Bedrock ids are bare (+anthropic.claude-...+) or cross-region (+us.anthropic.claude-...+).
|
|
11
8
|
ANTHROPIC_MODEL_PATTERN = /(?:^|\.)anthropic\./ #: Regexp
|
|
12
9
|
|
|
13
10
|
FINISH_REASONS = {
|
|
@@ -22,8 +19,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
22
19
|
"model_context_window_exceeded" => :context_window,
|
|
23
20
|
}.freeze #: Hash[String, Symbol]
|
|
24
21
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
22
|
+
# Only parts carrying this tag are replayed, so reasoning captured by another
|
|
23
|
+
# adapter, whose signatures Converse cannot verify, never reaches the request.
|
|
24
|
+
REASONING_FORMAT = "bedrock-converse-v1" #: String
|
|
25
|
+
|
|
27
26
|
#--
|
|
28
27
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
29
28
|
def self.skills_adapter(model = nil)
|
|
@@ -32,7 +31,6 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
32
31
|
Riffer::Skills::MarkdownAdapter
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
# The GenAI semconv well-known provider name.
|
|
36
34
|
#--
|
|
37
35
|
#: () -> String
|
|
38
36
|
def self.semconv_provider_name
|
|
@@ -47,7 +45,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
#--
|
|
50
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
48
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
51
49
|
def file_delivery(file)
|
|
52
50
|
file.url&.start_with?("s3://") ? :url : :bytes
|
|
53
51
|
end
|
|
@@ -60,13 +58,13 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
60
58
|
Riffer.config.amazon_bedrock.client
|
|
61
59
|
end
|
|
62
60
|
|
|
63
|
-
# Compacted so an unset region stays absent: the AWS SDK resolves +AWS_REGION+
|
|
64
|
-
# and the shared config only for a missing argument, and raises
|
|
65
|
-
# +Aws::Errors::MissingRegionError+ on an explicit nil.
|
|
66
61
|
#--
|
|
67
62
|
#: () -> untyped
|
|
68
63
|
def build_client
|
|
69
64
|
api_token = Riffer.config.amazon_bedrock.api_token
|
|
65
|
+
# Compacted out when unset: the AWS SDK resolves +AWS_REGION+ and the shared
|
|
66
|
+
# config only for a missing argument, and raises
|
|
67
|
+
# +Aws::Errors::MissingRegionError+ on an explicit nil.
|
|
70
68
|
region = Riffer.config.amazon_bedrock.region
|
|
71
69
|
|
|
72
70
|
if api_token && !api_token.empty?
|
|
@@ -96,10 +94,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
96
94
|
**options.except(:tools, :structured_output, :cache_control, :tags),
|
|
97
95
|
} #: Hash[Symbol, untyped]
|
|
98
96
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
params[:request_metadata] = tags unless tags.empty?
|
|
97
|
+
# Converse has no dedicated end-user field, so every tag (including the
|
|
98
|
+
# reserved user_id) rides along in requestMetadata; a tag wins over a
|
|
99
|
+
# model_options request_metadata key.
|
|
100
|
+
params[:request_metadata] = (params[:request_metadata] || {}).merge(tags) unless tags.empty?
|
|
103
101
|
|
|
104
102
|
if tools && !tools.empty?
|
|
105
103
|
params[:tool_config] = {
|
|
@@ -108,9 +106,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
108
106
|
end
|
|
109
107
|
|
|
110
108
|
if structured_output
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# null values for optional fields that the model has no value for.
|
|
109
|
+
# Strict makes optional fields nullable; otherwise Bedrock may return
|
|
110
|
+
# string literals like ": null," for optional fields with no value.
|
|
114
111
|
params[:output_config] = {
|
|
115
112
|
text_format: {
|
|
116
113
|
type: "json_schema",
|
|
@@ -129,13 +126,13 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
129
126
|
params
|
|
130
127
|
end
|
|
131
128
|
|
|
132
|
-
# Converse treats +tools -> system -> messages+ as one prefix and looks back
|
|
133
|
-
# from a +cachePoint+ for the longest cached run, so the point on the final
|
|
134
|
-
# message reuses the previous step's cache wherever that point sat. Mixed
|
|
135
|
-
# ttls must be ordered 1h before 5m, so both points share one.
|
|
136
129
|
#--
|
|
137
130
|
#: (Hash[Symbol, untyped], untyped) -> void
|
|
138
131
|
def apply_cache_point(params, cache_control)
|
|
132
|
+
# Converse treats +tools -> system -> messages+ as one prefix and looks back
|
|
133
|
+
# from a +cachePoint+ for the longest cached run, so the point on the final
|
|
134
|
+
# message reuses the previous step's cache wherever that point sat. Mixed
|
|
135
|
+
# ttls must be ordered 1h before 5m, so both points share one.
|
|
139
136
|
cache_point = { cache_point: build_cache_point(cache_control) }
|
|
140
137
|
system = params[:system]
|
|
141
138
|
tools = params.dig(:tool_config, :tools)
|
|
@@ -172,8 +169,6 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
172
169
|
build_token_usage(typed_response.usage)
|
|
173
170
|
end
|
|
174
171
|
|
|
175
|
-
# Converse's +input_tokens+ excludes the cache buckets; TokenUsage's
|
|
176
|
-
# input includes them.
|
|
177
172
|
#--
|
|
178
173
|
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
179
174
|
def build_token_usage(usage)
|
|
@@ -182,6 +177,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
182
177
|
|
|
183
178
|
apply_pricing(
|
|
184
179
|
Riffer::Providers::TokenUsage.new(
|
|
180
|
+
# Converse's +input_tokens+ excludes the cache buckets; TokenUsage's includes them.
|
|
185
181
|
input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
|
|
186
182
|
output_tokens: usage.output_tokens,
|
|
187
183
|
cache_write_tokens: cache_write,
|
|
@@ -244,12 +240,50 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
244
240
|
tool_calls
|
|
245
241
|
end
|
|
246
242
|
|
|
243
|
+
#--
|
|
244
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
245
|
+
def extract_reasoning(response)
|
|
246
|
+
typed_response = response #: Aws::BedrockRuntime::Client::_ConverseResponseSuccess
|
|
247
|
+
content_blocks = typed_response.output&.message&.content || []
|
|
248
|
+
|
|
249
|
+
content_blocks.filter_map do |block|
|
|
250
|
+
case (reasoning = block.reasoning_content)
|
|
251
|
+
when Aws::BedrockRuntime::Types::ReasoningContentBlock::RedactedContent
|
|
252
|
+
build_reasoning_part(redacted_content: reasoning.redacted_content)
|
|
253
|
+
when Aws::BedrockRuntime::Types::ReasoningContentBlock::ReasoningText
|
|
254
|
+
build_reasoning_part(text: reasoning.reasoning_text.text, signature: reasoning.reasoning_text.signature)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# +redacted_content+ is raw bytes; it is stored Base64-encoded so the part
|
|
260
|
+
# survives JSON persistence.
|
|
261
|
+
#--
|
|
262
|
+
#: (?text: String?, ?signature: String?, ?redacted_content: String?) -> Riffer::Messages::Assistant::ReasoningPart
|
|
263
|
+
def build_reasoning_part(text: nil, signature: nil, redacted_content: nil)
|
|
264
|
+
if redacted_content
|
|
265
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
266
|
+
type: :encrypted,
|
|
267
|
+
data: Base64.strict_encode64(redacted_content),
|
|
268
|
+
format: REASONING_FORMAT,
|
|
269
|
+
)
|
|
270
|
+
else
|
|
271
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
272
|
+
type: :text,
|
|
273
|
+
text: text,
|
|
274
|
+
signature: signature,
|
|
275
|
+
format: REASONING_FORMAT,
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
247
280
|
#--
|
|
248
281
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
249
282
|
def execute_stream(params, yielder)
|
|
250
283
|
current_state = {
|
|
251
284
|
text: nil,
|
|
252
285
|
tool_call: nil,
|
|
286
|
+
reasoning: nil,
|
|
253
287
|
} #: Hash[Symbol, untyped]
|
|
254
288
|
|
|
255
289
|
stream_completed = false
|
|
@@ -262,19 +296,19 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
262
296
|
when Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
263
297
|
handle_content_block_delta_text_delta(event, state: current_state, yielder: yielder) if event.delta&.text
|
|
264
298
|
handle_content_block_delta_tool_use(event, state: current_state, yielder: yielder) if event.delta&.tool_use
|
|
299
|
+
handle_reasoning_delta(event, state: current_state, yielder: yielder) if event.delta&.reasoning_content
|
|
265
300
|
when Aws::BedrockRuntime::Types::ContentBlockStopEvent
|
|
266
301
|
handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
267
302
|
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if current_state[:tool_call]
|
|
303
|
+
handle_reasoning_stop(event, state: current_state, yielder: yielder) if current_state[:reasoning]
|
|
268
304
|
when Aws::BedrockRuntime::Types::MessageStopEvent
|
|
269
305
|
stream_completed = true
|
|
270
306
|
yield_finish_reason(yielder, build_finish_reason(event.stop_reason))
|
|
271
307
|
when Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
|
|
272
308
|
handle_metadata_usage(event, state: current_state, yielder: yielder) if event.usage
|
|
273
309
|
when Aws::Errors::EventError
|
|
274
|
-
# The SDK
|
|
275
|
-
#
|
|
276
|
-
# never raised. Re-raise it here so the failure surfaces instead of
|
|
277
|
-
# truncating the stream.
|
|
310
|
+
# The SDK hands an event-stream +:message-type: error+ frame to this
|
|
311
|
+
# block as an EventError event rather than raising it.
|
|
278
312
|
raise_stream_event_error!(event)
|
|
279
313
|
else
|
|
280
314
|
raise_if_stream_exception!(event)
|
|
@@ -287,27 +321,22 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
287
321
|
raise Riffer::IncompleteStreamError, "Bedrock ConverseStream ended without a messageStop event"
|
|
288
322
|
end
|
|
289
323
|
|
|
290
|
-
# Re-raises an +Aws::Errors::EventError+ event with a message built from its
|
|
291
|
-
# error code and message. The SDK's own +#message+ is just the class name,
|
|
292
|
-
# so without this the failure would be unreadable.
|
|
293
324
|
#--
|
|
294
325
|
#: (untyped) -> void
|
|
295
326
|
def raise_stream_event_error!(event)
|
|
327
|
+
# The SDK's own +#message+ is just the class name.
|
|
296
328
|
details = [event.error_code, event.error_message].compact.join(": ")
|
|
297
329
|
details = "Bedrock ConverseStream error event" if details.empty?
|
|
298
330
|
|
|
299
331
|
raise event.exception(details)
|
|
300
332
|
end
|
|
301
333
|
|
|
302
|
-
# Re-raises a Bedrock stream-exception event as the matching
|
|
303
|
-
# +Aws::BedrockRuntime::Errors+ class. ConverseStream delivers API errors on
|
|
304
|
-
# the same channel as content, so without this a mid-stream failure would
|
|
305
|
-
# silently end the stream with no content. Non-exception events that we do
|
|
306
|
-
# not consume (including the SDK's +:unknown_event+ struct) are ignored for
|
|
307
|
-
# forward compatibility.
|
|
308
334
|
#--
|
|
309
335
|
#: (untyped) -> void
|
|
310
336
|
def raise_if_stream_exception!(event)
|
|
337
|
+
# ConverseStream delivers API errors on the same channel as content.
|
|
338
|
+
# Unconsumed non-exception events (including the SDK's +:unknown_event+
|
|
339
|
+
# struct) are ignored for forward compatibility.
|
|
311
340
|
klass_name = event.class.name&.split("::")&.last
|
|
312
341
|
return unless klass_name&.end_with?("Exception")
|
|
313
342
|
|
|
@@ -332,11 +361,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
332
361
|
def handle_content_block_delta_text_delta(event, state:, yielder:)
|
|
333
362
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
334
363
|
delta_text = typed_event.delta.text
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
# off to TextDone and then cleared on block stop, so nothing reads the
|
|
338
|
-
# pre-append string, making in-place mutation safe. Seed with an unfrozen
|
|
339
|
-
# String (+"") so << does not raise under frozen_string_literal.
|
|
364
|
+
# << avoids += copying the whole buffer per delta (O(n^2)); safe because
|
|
365
|
+
# nothing reads the pre-append string before block stop clears it.
|
|
340
366
|
state[:text] ||= +""
|
|
341
367
|
state[:text] << delta_text
|
|
342
368
|
yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
|
|
@@ -357,6 +383,29 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
357
383
|
)
|
|
358
384
|
end
|
|
359
385
|
|
|
386
|
+
# A reasoning block streams its text in pieces, then its signature (or its
|
|
387
|
+
# redacted bytes) in a delta of its own.
|
|
388
|
+
#--
|
|
389
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
390
|
+
def handle_reasoning_delta(event, state:, yielder:)
|
|
391
|
+
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
392
|
+
delta = typed_event.delta.reasoning_content
|
|
393
|
+
reasoning = state[:reasoning] ||= { text: +"" }
|
|
394
|
+
|
|
395
|
+
reasoning[:text] << delta.text if delta.text
|
|
396
|
+
reasoning[:signature] = delta.signature if delta.signature
|
|
397
|
+
(reasoning[:redacted_content] ||= +"") << delta.redacted_content if delta.redacted_content
|
|
398
|
+
|
|
399
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(delta.text) unless delta.text.nil? || delta.text.empty?
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
#--
|
|
403
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
404
|
+
def handle_reasoning_stop(_event, state:, yielder:)
|
|
405
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(build_reasoning_part(**state[:reasoning]))
|
|
406
|
+
state[:reasoning] = nil
|
|
407
|
+
end
|
|
408
|
+
|
|
360
409
|
#--
|
|
361
410
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
362
411
|
def handle_content_block_stop_text_delta(_event, state:, yielder:)
|
|
@@ -414,7 +463,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
414
463
|
#--
|
|
415
464
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
416
465
|
def convert_assistant_to_bedrock_format(message)
|
|
417
|
-
content =
|
|
466
|
+
content = message.reasoning.filter_map do |part|
|
|
467
|
+
convert_reasoning_part_to_bedrock_format(part) if part.format == REASONING_FORMAT
|
|
468
|
+
end
|
|
418
469
|
content << { text: message.content } if message.content && !message.content.empty?
|
|
419
470
|
|
|
420
471
|
message.tool_calls.each do |tc|
|
|
@@ -430,6 +481,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
430
481
|
{ role: "assistant", content: content }
|
|
431
482
|
end
|
|
432
483
|
|
|
484
|
+
#--
|
|
485
|
+
#: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
|
|
486
|
+
def convert_reasoning_part_to_bedrock_format(part)
|
|
487
|
+
data = part.data
|
|
488
|
+
return { reasoning_content: { redacted_content: Base64.strict_decode64(data) } } if data
|
|
489
|
+
|
|
490
|
+
{ reasoning_content: { reasoning_text: { text: part.text, signature: part.signature }.compact } }
|
|
491
|
+
end
|
|
492
|
+
|
|
433
493
|
#--
|
|
434
494
|
#: (Array[Hash[Symbol, untyped]], Riffer::Messages::Tool) -> void
|
|
435
495
|
def append_tool_result(conversation_messages, message)
|
|
@@ -449,7 +509,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
449
509
|
end
|
|
450
510
|
|
|
451
511
|
#--
|
|
452
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
512
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
453
513
|
def convert_file_part_to_bedrock_format(file)
|
|
454
514
|
format = bedrock_format(file.media_type)
|
|
455
515
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Anthropic provider for Claude models via the Anthropic API. Requires the
|
|
5
|
-
# +anthropic+ gem.
|
|
6
4
|
class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
7
5
|
WEB_SEARCH_TOOL_TYPE = "web_search_20250305" #: String
|
|
8
6
|
|
|
@@ -18,15 +16,12 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
18
16
|
"pause_turn" => :other,
|
|
19
17
|
}.freeze #: Hash[String, Symbol]
|
|
20
18
|
|
|
21
|
-
# Returns the XML skill adapter for Anthropic/Claude.
|
|
22
|
-
#
|
|
23
19
|
#--
|
|
24
20
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
25
21
|
def self.skills_adapter(_model = nil)
|
|
26
22
|
Riffer::Skills::XmlAdapter
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
# The GenAI semconv well-known provider name.
|
|
30
25
|
#--
|
|
31
26
|
#: () -> String
|
|
32
27
|
def self.semconv_provider_name
|
|
@@ -48,12 +43,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
48
43
|
Riffer.config.anthropic.client
|
|
49
44
|
end
|
|
50
45
|
|
|
51
|
-
# Compacted for the same reason as the other providers: never hand an SDK an
|
|
52
|
-
# explicit nil credential, so its own +ANTHROPIC_API_KEY+ resolution stays
|
|
53
|
-
# reachable regardless of how that SDK distinguishes nil from absent.
|
|
54
46
|
#--
|
|
55
47
|
#: () -> untyped
|
|
56
48
|
def build_client
|
|
49
|
+
# Never pass an explicit nil credential, so the SDK's own
|
|
50
|
+
# ANTHROPIC_API_KEY resolution stays reachable.
|
|
57
51
|
::Anthropic::Client.new(**{ api_key: Riffer.config.anthropic.api_key }.compact)
|
|
58
52
|
end
|
|
59
53
|
|
|
@@ -78,8 +72,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
78
72
|
params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
|
|
79
73
|
|
|
80
74
|
# Anthropic's only request-metadata field is metadata.user_id (opaque, no
|
|
81
|
-
# PII)
|
|
82
|
-
# here and survive only on spans.
|
|
75
|
+
# PII); all other tags survive only on spans.
|
|
83
76
|
user_id = tags["user_id"]
|
|
84
77
|
params[:metadata] = { user_id: user_id } if user_id
|
|
85
78
|
|
|
@@ -93,13 +86,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
93
86
|
end
|
|
94
87
|
|
|
95
88
|
if structured_output
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# Merged over any caller-supplied output_config (e.g. effort) so those
|
|
101
|
-
# keys survive; the structured-output format wins because the run loop
|
|
102
|
-
# validates the response against it.
|
|
89
|
+
# Strict schema makes optional fields nullable; otherwise Anthropic may
|
|
90
|
+
# return empty strings or whitespace instead of null. The format wins
|
|
91
|
+
# over caller output_config keys because the run loop validates against it.
|
|
103
92
|
params[:output_config] = {
|
|
104
93
|
**(params[:output_config] || {}),
|
|
105
94
|
format: {
|
|
@@ -143,8 +132,6 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
143
132
|
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(raw, :other), raw: raw)
|
|
144
133
|
end
|
|
145
134
|
|
|
146
|
-
# Anthropic's +input_tokens+ excludes the cache buckets; TokenUsage's
|
|
147
|
-
# input includes them.
|
|
148
135
|
#--
|
|
149
136
|
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
150
137
|
def build_token_usage(usage)
|
|
@@ -153,6 +140,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
153
140
|
|
|
154
141
|
apply_pricing(
|
|
155
142
|
Riffer::Providers::TokenUsage.new(
|
|
143
|
+
# Anthropic's input_tokens excludes the cache buckets; TokenUsage's includes them.
|
|
156
144
|
input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
|
|
157
145
|
output_tokens: usage.output_tokens,
|
|
158
146
|
cache_write_tokens: cache_write,
|
|
@@ -287,11 +275,8 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
287
275
|
#--
|
|
288
276
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
289
277
|
def handle_text_event(event, state:, yielder:)
|
|
290
|
-
# Mutating append
|
|
291
|
-
#
|
|
292
|
-
# is handed to TextDone and cleared on block stop, so no reader observes the
|
|
293
|
-
# pre-append string. Seed with an unfrozen String so << is legal under
|
|
294
|
-
# frozen_string_literal.
|
|
278
|
+
# Mutating append avoids O(n^2) copying per content block; the buffer is
|
|
279
|
+
# handed to TextDone and cleared on block stop, so no reader sees it mid-append.
|
|
295
280
|
state[:text] ||= +""
|
|
296
281
|
state[:text] << event.text
|
|
297
282
|
yielder << Riffer::StreamEvents::TextDelta.new(event.text)
|
|
@@ -338,7 +323,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
338
323
|
#--
|
|
339
324
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
340
325
|
def handle_content_block_stop_thinking(_event, state:, yielder:)
|
|
341
|
-
yielder
|
|
326
|
+
yield_reasoning_done(yielder, state[:reasoning])
|
|
342
327
|
state[:reasoning] = nil
|
|
343
328
|
end
|
|
344
329
|
|
|
@@ -444,7 +429,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
444
429
|
end
|
|
445
430
|
|
|
446
431
|
#--
|
|
447
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
432
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
448
433
|
def convert_file_part_to_anthropic_format(file)
|
|
449
434
|
type = file.image? ? "image" : "document"
|
|
450
435
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Azure OpenAI provider for GPT models hosted on Azure. Requires the +openai+
|
|
5
|
-
# gem. Credentials resolve from config, then +AZURE_OPENAI_API_KEY+ /
|
|
6
|
-
# +AZURE_OPENAI_ENDPOINT+.
|
|
7
4
|
class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
8
|
-
# The GenAI semconv well-known provider name.
|
|
9
5
|
#--
|
|
10
6
|
#: () -> String
|
|
11
7
|
def self.semconv_provider_name
|
|
@@ -20,15 +16,14 @@ class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
|
20
16
|
Riffer.config.azure_openai.client
|
|
21
17
|
end
|
|
22
18
|
|
|
23
|
-
# Deliberately not compacted: this borrows the OpenAI SDK to talk to Azure, so
|
|
24
|
-
# omitting an unset argument would let the SDK fall back to +OPENAI_API_KEY+
|
|
25
|
-
# and +OPENAI_BASE_URL+ — sending Azure traffic, and an OpenAI credential, to
|
|
26
|
-
# whatever those name. Passing nil raises in the SDK instead.
|
|
27
19
|
#--
|
|
28
20
|
#: () -> untyped
|
|
29
21
|
def build_client
|
|
30
22
|
api_key = Riffer.config.azure_openai.api_key || ENV.fetch("AZURE_OPENAI_API_KEY", nil)
|
|
31
23
|
base_url = Riffer.config.azure_openai.endpoint || ENV.fetch("AZURE_OPENAI_ENDPOINT", nil)
|
|
24
|
+
# Pass nils rather than omitting them: an omitted argument lets the SDK fall
|
|
25
|
+
# back to OPENAI_API_KEY / OPENAI_BASE_URL, sending Azure traffic and an
|
|
26
|
+
# OpenAI credential elsewhere. A nil raises in the SDK instead.
|
|
32
27
|
::OpenAI::Client.new(api_key: api_key, base_url: base_url)
|
|
33
28
|
end
|
|
34
29
|
end
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
# Base class for all LLM providers. A template-method flow: subclasses implement
|
|
7
|
-
# the hooks (+build_request_params+, +execute_generate+, +execute_stream+,
|
|
8
|
-
# +extract_token_usage+, +extract_content+, +extract_tool_calls+) and the base
|
|
9
|
-
# class orchestrates them.
|
|
10
6
|
class Riffer::Providers::Base
|
|
11
7
|
# @rbs @current_tools: Array[singleton(Riffer::Tool)]
|
|
12
8
|
# @rbs @current_model: String?
|
|
@@ -15,18 +11,12 @@ class Riffer::Providers::Base
|
|
|
15
11
|
|
|
16
12
|
WIRE_SEPARATOR = "__" #: String
|
|
17
13
|
|
|
18
|
-
# Returns the preferred skill adapter for this provider; override in
|
|
19
|
-
# subclasses (optionally introspecting +model+) for provider-specific formats.
|
|
20
14
|
#--
|
|
21
15
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
22
16
|
def self.skills_adapter(_model = nil)
|
|
23
17
|
Riffer::Skills::MarkdownAdapter
|
|
24
18
|
end
|
|
25
19
|
|
|
26
|
-
# Returns the provider name stamped as <tt>gen_ai.provider.name</tt> on trace
|
|
27
|
-
# spans, ideally a GenAI semconv well-known value. Defaults to the snake_cased
|
|
28
|
-
# class name rather than raising like the abstract provider methods, so
|
|
29
|
-
# enabling tracing never breaks an otherwise-working custom provider.
|
|
30
20
|
#--
|
|
31
21
|
#: () -> String
|
|
32
22
|
def self.semconv_provider_name
|
|
@@ -34,13 +24,13 @@ class Riffer::Providers::Base
|
|
|
34
24
|
# later must pick up its real name, not a frozen "unknown".
|
|
35
25
|
class_name = name or return "unknown"
|
|
36
26
|
|
|
27
|
+
# A default rather than NotImplementedError, so enabling tracing never
|
|
28
|
+
# breaks an otherwise-working custom provider.
|
|
37
29
|
@semconv_provider_name ||= Riffer::Helpers::Identifier.derive(class_name.split("::").last)
|
|
38
30
|
end
|
|
39
31
|
|
|
40
|
-
# Generates text using the provider.
|
|
41
|
-
#
|
|
42
32
|
#--
|
|
43
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
33
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
44
34
|
def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
45
35
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
46
36
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -56,6 +46,7 @@ class Riffer::Providers::Base
|
|
|
56
46
|
|
|
57
47
|
content = extract_content(response)
|
|
58
48
|
tool_calls = extract_tool_calls(response)
|
|
49
|
+
reasoning = extract_reasoning(response)
|
|
59
50
|
token_usage = extract_token_usage(response)
|
|
60
51
|
finish_reason = extract_finish_reason(response)
|
|
61
52
|
structured_output = parse_structured_output(content) if options[:structured_output] && tool_calls.empty?
|
|
@@ -67,6 +58,7 @@ class Riffer::Providers::Base
|
|
|
67
58
|
Riffer::Messages::Assistant.new(
|
|
68
59
|
content,
|
|
69
60
|
tool_calls: tool_calls,
|
|
61
|
+
reasoning: reasoning,
|
|
70
62
|
token_usage: token_usage,
|
|
71
63
|
structured_output: structured_output,
|
|
72
64
|
finish_reason: finish_reason&.reason,
|
|
@@ -75,10 +67,8 @@ class Riffer::Providers::Base
|
|
|
75
67
|
end
|
|
76
68
|
end
|
|
77
69
|
|
|
78
|
-
# Streams text from the provider.
|
|
79
|
-
#
|
|
80
70
|
#--
|
|
81
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
71
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
82
72
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
83
73
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
84
74
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -105,7 +95,7 @@ class Riffer::Providers::Base
|
|
|
105
95
|
end
|
|
106
96
|
|
|
107
97
|
#--
|
|
108
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
98
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
109
99
|
def file_delivery(_file)
|
|
110
100
|
:url
|
|
111
101
|
end
|
|
@@ -117,21 +107,17 @@ class Riffer::Providers::Base
|
|
|
117
107
|
Riffer::Helpers::Dependencies.depends_on(gem_name)
|
|
118
108
|
end
|
|
119
109
|
|
|
120
|
-
# Returns the client for the current LLM call. A configured client wins,
|
|
121
|
-
# resolved on every call so a Proc can vary the client by process or
|
|
122
|
-
# credential lifetime; otherwise the provider builds one from the configured
|
|
123
|
-
# credentials, memoized for the life of the provider.
|
|
124
110
|
#--
|
|
125
111
|
#: () -> untyped
|
|
126
112
|
def client
|
|
127
113
|
configured = global_client
|
|
114
|
+
# Resolved on every call, never memoized, so a Proc can vary the client by
|
|
115
|
+
# process or credential lifetime.
|
|
128
116
|
return Riffer::Helpers::CallOrValue.resolve(configured) if configured
|
|
129
117
|
|
|
130
118
|
@client ||= build_client
|
|
131
119
|
end
|
|
132
120
|
|
|
133
|
-
# Returns the consumer-configured client for this provider; nil when none is
|
|
134
|
-
# configured, and for providers that take no configuration at all.
|
|
135
121
|
#--
|
|
136
122
|
#: () -> untyped
|
|
137
123
|
def global_client
|
|
@@ -216,14 +202,20 @@ class Riffer::Providers::Base
|
|
|
216
202
|
pricing.rates_for("#{key}/#{model}")
|
|
217
203
|
end
|
|
218
204
|
|
|
219
|
-
# Defaults to nil rather than raising — finish reasons are optional, so
|
|
220
|
-
# providers that don't report one stay valid.
|
|
221
205
|
#--
|
|
222
206
|
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
223
207
|
def extract_finish_reason(_response)
|
|
208
|
+
# Optional hook, so providers that don't report a finish reason stay valid.
|
|
224
209
|
nil
|
|
225
210
|
end
|
|
226
211
|
|
|
212
|
+
#--
|
|
213
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
214
|
+
def extract_reasoning(_response)
|
|
215
|
+
# Optional hook, so providers that don't expose replayable reasoning stay valid.
|
|
216
|
+
[]
|
|
217
|
+
end
|
|
218
|
+
|
|
227
219
|
#--
|
|
228
220
|
#: (untyped) -> String
|
|
229
221
|
def extract_content(response)
|
|
@@ -285,8 +277,6 @@ class Riffer::Providers::Base
|
|
|
285
277
|
attributes.merge(tag_attributes(options[:tags] || {}))
|
|
286
278
|
end
|
|
287
279
|
|
|
288
|
-
# Maps normalized tags to their namespaced span attribute form. An empty map
|
|
289
|
-
# yields an empty hash, so merging it is a no-op.
|
|
290
280
|
#--
|
|
291
281
|
#: (Hash[String, String]) -> Hash[String, String]
|
|
292
282
|
def tag_attributes(tags)
|
|
@@ -348,6 +338,14 @@ class Riffer::Providers::Base
|
|
|
348
338
|
Riffer.config.tracing.capture_messages && span.recording?
|
|
349
339
|
end
|
|
350
340
|
|
|
341
|
+
#--
|
|
342
|
+
#: (Riffer::Providers::_EventSink, String) -> void
|
|
343
|
+
def yield_reasoning_done(yielder, text)
|
|
344
|
+
# No +format+, so the reasoning persists for display but is never replayed.
|
|
345
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: text)
|
|
346
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
347
|
+
end
|
|
348
|
+
|
|
351
349
|
#--
|
|
352
350
|
#: (Riffer::Providers::_EventSink, Riffer::Providers::FinishReason?) -> void
|
|
353
351
|
def yield_finish_reason(yielder, finish_reason)
|
|
@@ -368,11 +366,11 @@ class Riffer::Providers::Base
|
|
|
368
366
|
end
|
|
369
367
|
|
|
370
368
|
#--
|
|
371
|
-
#: (
|
|
369
|
+
#: (String) -> Hash[String, untyped]
|
|
372
370
|
def parse_tool_arguments(arguments)
|
|
373
|
-
return {} if arguments.
|
|
371
|
+
return {} if arguments.empty?
|
|
374
372
|
|
|
375
|
-
|
|
373
|
+
JSON.parse(arguments)
|
|
376
374
|
end
|
|
377
375
|
|
|
378
376
|
#--
|
|
@@ -397,7 +395,7 @@ class Riffer::Providers::Base
|
|
|
397
395
|
end
|
|
398
396
|
|
|
399
397
|
#--
|
|
400
|
-
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
398
|
+
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
401
399
|
def normalize_messages(prompt:, system:, messages:, files: nil)
|
|
402
400
|
if messages && files && !files.empty?
|
|
403
401
|
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead"
|
|
@@ -407,7 +405,7 @@ class Riffer::Providers::Base
|
|
|
407
405
|
|
|
408
406
|
result = [] #: Array[Riffer::Messages::Base]
|
|
409
407
|
result << Riffer::Messages::System.new(system) if system
|
|
410
|
-
file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
408
|
+
file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
411
409
|
prompt_text = prompt #: String
|
|
412
410
|
result << Riffer::Messages::User.new(prompt_text, files: file_parts)
|
|
413
411
|
result
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Normalized reason an LLM call finished, paired with the provider's raw
|
|
5
|
-
# wire value. +reason+ carries the same meaning for every provider.
|
|
6
4
|
class Riffer::Providers::FinishReason
|
|
7
|
-
# The normalized vocabulary every provider maps into.
|
|
8
5
|
VALUES = %i[stop length tool_calls content_filter context_window malformed_output error other].freeze #: Array[Symbol]
|
|
9
6
|
|
|
10
|
-
# The normalized reason.
|
|
11
7
|
attr_reader :reason #: Symbol # @dynamic reason
|
|
12
8
|
|
|
13
|
-
# The provider's raw finish-reason value, when one exists on the wire.
|
|
14
9
|
attr_reader :raw #: String? # @dynamic raw
|
|
15
10
|
|
|
16
|
-
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
17
11
|
#--
|
|
18
12
|
#: (reason: Symbol, ?raw: String?) -> void
|
|
19
13
|
def initialize(reason:, raw: nil)
|