riffer 0.48.0 → 0.50.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 +32 -0
- data/docs/AGENTS.md +3 -5
- data/docs/AGENT_LIFECYCLE.md +13 -16
- data/docs/CONFIGURATION.md +22 -33
- data/docs/STREAM_EVENTS.md +3 -3
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +31 -0
- data/docs/providers/ANTHROPIC.md +10 -0
- data/docs/providers/AZURE_OPENAI.md +2 -0
- data/docs/providers/OPENAI.md +22 -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 +98 -46
- data/lib/riffer/providers/anthropic.rb +65 -65
- data/lib/riffer/providers/azure_open_ai.rb +10 -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 +94 -46
- 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 +26 -26
- data/sig/generated/riffer/providers/anthropic.rbs +23 -22
- data/sig/generated/riffer/providers/azure_open_ai.rbs +5 -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 +36 -19
- 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
data/lib/riffer/params.rb
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# A DSL for defining tool parameters and structured-output schemas, used within
|
|
5
|
-
# a Tool's +params+ block.
|
|
6
|
-
#
|
|
7
|
-
# params do
|
|
8
|
-
# required :city, String, description: "The city name"
|
|
9
|
-
# optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
|
|
10
|
-
# end
|
|
11
|
-
#
|
|
12
4
|
class Riffer::Params
|
|
13
|
-
# The defined parameters.
|
|
14
5
|
attr_reader :parameters #: Array[Riffer::Params::Param] # @dynamic parameters
|
|
15
6
|
|
|
16
7
|
#--
|
|
@@ -19,13 +10,8 @@ class Riffer::Params
|
|
|
19
10
|
@parameters = []
|
|
20
11
|
end
|
|
21
12
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# outside the Params-expressible subset of JSON Schema.
|
|
25
|
-
#
|
|
26
|
-
# schema = params.to_json_schema(strict: false)
|
|
27
|
-
# Riffer::Params.from_json_schema(schema) # => equivalent Riffer::Params
|
|
28
|
-
#
|
|
13
|
+
# The inverse of +to_json_schema(strict: false)+. Raises Riffer::ArgumentError
|
|
14
|
+
# on features outside the Params-expressible subset of JSON Schema.
|
|
29
15
|
#--
|
|
30
16
|
#: (Hash[Symbol, untyped]) -> Riffer::Params
|
|
31
17
|
def self.from_json_schema(schema)
|
|
@@ -42,8 +28,6 @@ class Riffer::Params
|
|
|
42
28
|
params
|
|
43
29
|
end
|
|
44
30
|
|
|
45
|
-
# Defines a required parameter.
|
|
46
|
-
#
|
|
47
31
|
#--
|
|
48
32
|
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
49
33
|
def required(name, type, description: nil, enum: nil, of: nil, &)
|
|
@@ -59,8 +43,6 @@ class Riffer::Params
|
|
|
59
43
|
)
|
|
60
44
|
end
|
|
61
45
|
|
|
62
|
-
# Defines an optional parameter.
|
|
63
|
-
#
|
|
64
46
|
#--
|
|
65
47
|
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
66
48
|
def optional(name, type, description: nil, enum: nil, default: nil, of: nil, &)
|
|
@@ -77,15 +59,6 @@ class Riffer::Params
|
|
|
77
59
|
)
|
|
78
60
|
end
|
|
79
61
|
|
|
80
|
-
# Validates arguments against parameter definitions.
|
|
81
|
-
#
|
|
82
|
-
# A Float param accepts an Integer (JSON Schema <tt>"number"</tt> covers
|
|
83
|
-
# integers) and its value is coerced with +to_f+, so callers always get a
|
|
84
|
-
# Float. The same holds for the items of an <tt>of: Float</tt> array. No other
|
|
85
|
-
# type is coerced.
|
|
86
|
-
#
|
|
87
|
-
# Raises Riffer::ValidationError if validation fails.
|
|
88
|
-
#
|
|
89
62
|
#--
|
|
90
63
|
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
91
64
|
def validate(arguments)
|
|
@@ -125,11 +98,6 @@ class Riffer::Params
|
|
|
125
98
|
validated
|
|
126
99
|
end
|
|
127
100
|
|
|
128
|
-
# Converts all parameters to JSON Schema format. When +strict+ is true, every
|
|
129
|
-
# property is listed in +required+ and optional ones are made nullable
|
|
130
|
-
# instead, satisfying providers that enforce strict structured output schemas.
|
|
131
|
-
# Raises Riffer::ArgumentError when +strict+ and a Hash param (at any depth)
|
|
132
|
-
# has no block or an Array param has neither a block nor <tt>of:</tt>.
|
|
133
101
|
#--
|
|
134
102
|
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
135
103
|
def to_json_schema(strict: false)
|
|
@@ -138,6 +106,8 @@ class Riffer::Params
|
|
|
138
106
|
|
|
139
107
|
@parameters.each do |param|
|
|
140
108
|
properties[param.name.to_s] = param.to_json_schema(strict: strict)
|
|
109
|
+
# Providers enforcing strict structured output require every property in
|
|
110
|
+
# +required+; strict mode makes optional ones nullable instead.
|
|
141
111
|
required_params << param.name.to_s if strict || param.required
|
|
142
112
|
end
|
|
143
113
|
|
|
@@ -151,8 +121,8 @@ class Riffer::Params
|
|
|
151
121
|
|
|
152
122
|
private
|
|
153
123
|
|
|
154
|
-
# +dup+
|
|
155
|
-
#
|
|
124
|
+
# Without this, +dup+ shares parameters, so defining one on either copy
|
|
125
|
+
# would reach the other.
|
|
156
126
|
#--
|
|
157
127
|
#: (Riffer::Params) -> void
|
|
158
128
|
def initialize_copy(source)
|
|
@@ -233,7 +203,6 @@ class Riffer::Params
|
|
|
233
203
|
end
|
|
234
204
|
end
|
|
235
205
|
|
|
236
|
-
# Returns the array with its valid items coerced by +coerce_value+.
|
|
237
206
|
#--
|
|
238
207
|
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
239
208
|
def validate_typed_array(param, value, errors)
|
|
@@ -258,13 +227,11 @@ class Riffer::Params
|
|
|
258
227
|
end
|
|
259
228
|
end
|
|
260
229
|
|
|
261
|
-
# Coerces an already-validated value to the Ruby type its param declares.
|
|
262
|
-
# Only Float coerces today, because JSON Schema "number" accepts integers and
|
|
263
|
-
# callers should not get a type that depends on whether the model wrote a
|
|
264
|
-
# decimal point. Add a branch here rather than inline at a call site.
|
|
265
230
|
#--
|
|
266
231
|
#: (Module, untyped) -> untyped
|
|
267
232
|
def coerce_value(type, value)
|
|
233
|
+
# JSON Schema "number" accepts integers; callers shouldn't get a type that
|
|
234
|
+
# depends on whether the model wrote a decimal point.
|
|
268
235
|
return value.to_f if type == Float
|
|
269
236
|
|
|
270
237
|
value
|
|
@@ -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
|
|
@@ -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,9 @@ 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
|
-
# set in model_options; a tag wins on a shared key.
|
|
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.
|
|
103
100
|
params[:request_metadata] = (params[:request_metadata] || {}).merge(tags) unless tags.empty?
|
|
104
101
|
|
|
105
102
|
if tools && !tools.empty?
|
|
@@ -109,9 +106,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
109
106
|
end
|
|
110
107
|
|
|
111
108
|
if structured_output
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# 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.
|
|
115
111
|
params[:output_config] = {
|
|
116
112
|
text_format: {
|
|
117
113
|
type: "json_schema",
|
|
@@ -130,13 +126,13 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
130
126
|
params
|
|
131
127
|
end
|
|
132
128
|
|
|
133
|
-
# Converse treats +tools -> system -> messages+ as one prefix and looks back
|
|
134
|
-
# from a +cachePoint+ for the longest cached run, so the point on the final
|
|
135
|
-
# message reuses the previous step's cache wherever that point sat. Mixed
|
|
136
|
-
# ttls must be ordered 1h before 5m, so both points share one.
|
|
137
129
|
#--
|
|
138
130
|
#: (Hash[Symbol, untyped], untyped) -> void
|
|
139
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.
|
|
140
136
|
cache_point = { cache_point: build_cache_point(cache_control) }
|
|
141
137
|
system = params[:system]
|
|
142
138
|
tools = params.dig(:tool_config, :tools)
|
|
@@ -173,8 +169,6 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
173
169
|
build_token_usage(typed_response.usage)
|
|
174
170
|
end
|
|
175
171
|
|
|
176
|
-
# Converse's +input_tokens+ excludes the cache buckets; TokenUsage's
|
|
177
|
-
# input includes them.
|
|
178
172
|
#--
|
|
179
173
|
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
180
174
|
def build_token_usage(usage)
|
|
@@ -183,6 +177,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
183
177
|
|
|
184
178
|
apply_pricing(
|
|
185
179
|
Riffer::Providers::TokenUsage.new(
|
|
180
|
+
# Converse's +input_tokens+ excludes the cache buckets; TokenUsage's includes them.
|
|
186
181
|
input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
|
|
187
182
|
output_tokens: usage.output_tokens,
|
|
188
183
|
cache_write_tokens: cache_write,
|
|
@@ -245,12 +240,50 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
245
240
|
tool_calls
|
|
246
241
|
end
|
|
247
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
|
+
|
|
248
280
|
#--
|
|
249
281
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
250
282
|
def execute_stream(params, yielder)
|
|
251
283
|
current_state = {
|
|
252
284
|
text: nil,
|
|
253
285
|
tool_call: nil,
|
|
286
|
+
reasoning: nil,
|
|
254
287
|
} #: Hash[Symbol, untyped]
|
|
255
288
|
|
|
256
289
|
stream_completed = false
|
|
@@ -263,19 +296,19 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
263
296
|
when Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
264
297
|
handle_content_block_delta_text_delta(event, state: current_state, yielder: yielder) if event.delta&.text
|
|
265
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
|
|
266
300
|
when Aws::BedrockRuntime::Types::ContentBlockStopEvent
|
|
267
|
-
handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
268
301
|
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if current_state[:tool_call]
|
|
302
|
+
handle_reasoning_stop(event, state: current_state, yielder: yielder) if current_state[:reasoning]
|
|
269
303
|
when Aws::BedrockRuntime::Types::MessageStopEvent
|
|
270
304
|
stream_completed = true
|
|
305
|
+
yielder << Riffer::StreamEvents::TextDone.new(current_state[:text]) if current_state[:text]
|
|
271
306
|
yield_finish_reason(yielder, build_finish_reason(event.stop_reason))
|
|
272
307
|
when Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
|
|
273
308
|
handle_metadata_usage(event, state: current_state, yielder: yielder) if event.usage
|
|
274
309
|
when Aws::Errors::EventError
|
|
275
|
-
# The SDK
|
|
276
|
-
#
|
|
277
|
-
# never raised. Re-raise it here so the failure surfaces instead of
|
|
278
|
-
# 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.
|
|
279
312
|
raise_stream_event_error!(event)
|
|
280
313
|
else
|
|
281
314
|
raise_if_stream_exception!(event)
|
|
@@ -288,27 +321,22 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
288
321
|
raise Riffer::IncompleteStreamError, "Bedrock ConverseStream ended without a messageStop event"
|
|
289
322
|
end
|
|
290
323
|
|
|
291
|
-
# Re-raises an +Aws::Errors::EventError+ event with a message built from its
|
|
292
|
-
# error code and message. The SDK's own +#message+ is just the class name,
|
|
293
|
-
# so without this the failure would be unreadable.
|
|
294
324
|
#--
|
|
295
325
|
#: (untyped) -> void
|
|
296
326
|
def raise_stream_event_error!(event)
|
|
327
|
+
# The SDK's own +#message+ is just the class name.
|
|
297
328
|
details = [event.error_code, event.error_message].compact.join(": ")
|
|
298
329
|
details = "Bedrock ConverseStream error event" if details.empty?
|
|
299
330
|
|
|
300
331
|
raise event.exception(details)
|
|
301
332
|
end
|
|
302
333
|
|
|
303
|
-
# Re-raises a Bedrock stream-exception event as the matching
|
|
304
|
-
# +Aws::BedrockRuntime::Errors+ class. ConverseStream delivers API errors on
|
|
305
|
-
# the same channel as content, so without this a mid-stream failure would
|
|
306
|
-
# silently end the stream with no content. Non-exception events that we do
|
|
307
|
-
# not consume (including the SDK's +:unknown_event+ struct) are ignored for
|
|
308
|
-
# forward compatibility.
|
|
309
334
|
#--
|
|
310
335
|
#: (untyped) -> void
|
|
311
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.
|
|
312
340
|
klass_name = event.class.name&.split("::")&.last
|
|
313
341
|
return unless klass_name&.end_with?("Exception")
|
|
314
342
|
|
|
@@ -333,11 +361,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
333
361
|
def handle_content_block_delta_text_delta(event, state:, yielder:)
|
|
334
362
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
335
363
|
delta_text = typed_event.delta.text
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
# off to TextDone and then cleared on block stop, so nothing reads the
|
|
339
|
-
# pre-append string, making in-place mutation safe. Seed with an unfrozen
|
|
340
|
-
# 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 buffer before message stop hands it to TextDone.
|
|
341
366
|
state[:text] ||= +""
|
|
342
367
|
state[:text] << delta_text
|
|
343
368
|
yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
|
|
@@ -358,11 +383,27 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
358
383
|
)
|
|
359
384
|
end
|
|
360
385
|
|
|
386
|
+
# A reasoning block streams its text in pieces, then its signature (or its
|
|
387
|
+
# redacted bytes) in a delta of its own.
|
|
361
388
|
#--
|
|
362
389
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
363
|
-
def
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
366
407
|
end
|
|
367
408
|
|
|
368
409
|
#--
|
|
@@ -415,7 +456,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
415
456
|
#--
|
|
416
457
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
417
458
|
def convert_assistant_to_bedrock_format(message)
|
|
418
|
-
content =
|
|
459
|
+
content = message.reasoning.filter_map do |part|
|
|
460
|
+
convert_reasoning_part_to_bedrock_format(part) if part.format == REASONING_FORMAT
|
|
461
|
+
end
|
|
419
462
|
content << { text: message.content } if message.content && !message.content.empty?
|
|
420
463
|
|
|
421
464
|
message.tool_calls.each do |tc|
|
|
@@ -431,6 +474,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
431
474
|
{ role: "assistant", content: content }
|
|
432
475
|
end
|
|
433
476
|
|
|
477
|
+
#--
|
|
478
|
+
#: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
|
|
479
|
+
def convert_reasoning_part_to_bedrock_format(part)
|
|
480
|
+
data = part.data
|
|
481
|
+
return { reasoning_content: { redacted_content: Base64.strict_decode64(data) } } if data
|
|
482
|
+
|
|
483
|
+
{ reasoning_content: { reasoning_text: { text: part.text, signature: part.signature }.compact } }
|
|
484
|
+
end
|
|
485
|
+
|
|
434
486
|
#--
|
|
435
487
|
#: (Array[Hash[Symbol, untyped]], Riffer::Messages::Tool) -> void
|
|
436
488
|
def append_tool_result(conversation_messages, message)
|
|
@@ -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,17 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
18
16
|
"pause_turn" => :other,
|
|
19
17
|
}.freeze #: Hash[String, Symbol]
|
|
20
18
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
19
|
+
# Only parts carrying this tag are replayed, so reasoning captured by another
|
|
20
|
+
# adapter, whose signatures the Messages API may not accept, never reaches the
|
|
21
|
+
# request.
|
|
22
|
+
REASONING_FORMAT = "anthropic-messages-v1" #: String
|
|
23
|
+
|
|
23
24
|
#--
|
|
24
25
|
#: (?String?) -> singleton(Riffer::Skills::Adapter)
|
|
25
26
|
def self.skills_adapter(_model = nil)
|
|
26
27
|
Riffer::Skills::XmlAdapter
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
# The GenAI semconv well-known provider name.
|
|
30
30
|
#--
|
|
31
31
|
#: () -> String
|
|
32
32
|
def self.semconv_provider_name
|
|
@@ -48,12 +48,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
48
48
|
Riffer.config.anthropic.client
|
|
49
49
|
end
|
|
50
50
|
|
|
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
51
|
#--
|
|
55
52
|
#: () -> untyped
|
|
56
53
|
def build_client
|
|
54
|
+
# Never pass an explicit nil credential, so the SDK's own
|
|
55
|
+
# ANTHROPIC_API_KEY resolution stays reachable.
|
|
57
56
|
::Anthropic::Client.new(**{ api_key: Riffer.config.anthropic.api_key }.compact)
|
|
58
57
|
end
|
|
59
58
|
|
|
@@ -78,8 +77,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
78
77
|
params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
|
|
79
78
|
|
|
80
79
|
# Anthropic's only request-metadata field is metadata.user_id (opaque, no
|
|
81
|
-
# PII)
|
|
82
|
-
# here and survive only on spans.
|
|
80
|
+
# PII); all other tags survive only on spans.
|
|
83
81
|
user_id = tags["user_id"]
|
|
84
82
|
params[:metadata] = { user_id: user_id } if user_id
|
|
85
83
|
|
|
@@ -93,13 +91,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
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.
|
|
94
|
+
# Strict schema makes optional fields nullable; otherwise Anthropic may
|
|
95
|
+
# return empty strings or whitespace instead of null. The format wins
|
|
96
|
+
# over caller output_config keys because the run loop validates against it.
|
|
103
97
|
params[:output_config] = {
|
|
104
98
|
**(params[:output_config] || {}),
|
|
105
99
|
format: {
|
|
@@ -143,8 +137,6 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
143
137
|
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(raw, :other), raw: raw)
|
|
144
138
|
end
|
|
145
139
|
|
|
146
|
-
# Anthropic's +input_tokens+ excludes the cache buckets; TokenUsage's
|
|
147
|
-
# input includes them.
|
|
148
140
|
#--
|
|
149
141
|
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
150
142
|
def build_token_usage(usage)
|
|
@@ -153,6 +145,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
153
145
|
|
|
154
146
|
apply_pricing(
|
|
155
147
|
Riffer::Providers::TokenUsage.new(
|
|
148
|
+
# Anthropic's input_tokens excludes the cache buckets; TokenUsage's includes them.
|
|
156
149
|
input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
|
|
157
150
|
output_tokens: usage.output_tokens,
|
|
158
151
|
cache_write_tokens: cache_write,
|
|
@@ -165,16 +158,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
165
158
|
#: (untyped) -> String
|
|
166
159
|
def extract_content(response)
|
|
167
160
|
message = response #: Anthropic::Models::Message
|
|
168
|
-
|
|
169
|
-
return "" if content_blocks.nil? || content_blocks.empty?
|
|
170
|
-
|
|
171
|
-
text_content = ""
|
|
172
|
-
|
|
173
|
-
content_blocks.each do |block|
|
|
174
|
-
text_content = block.text if block.is_a?(::Anthropic::Models::TextBlock)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
text_content
|
|
161
|
+
(message.content || []).filter_map { |block| block.text if block.is_a?(::Anthropic::Models::TextBlock) }.join
|
|
178
162
|
end
|
|
179
163
|
|
|
180
164
|
#--
|
|
@@ -199,12 +183,34 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
199
183
|
tool_calls
|
|
200
184
|
end
|
|
201
185
|
|
|
186
|
+
#--
|
|
187
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
188
|
+
def extract_reasoning(response)
|
|
189
|
+
message = response #: Anthropic::Models::Message
|
|
190
|
+
(message.content || []).filter_map { |block| build_reasoning_part(block) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
#--
|
|
194
|
+
#: (untyped) -> Riffer::Messages::Assistant::ReasoningPart?
|
|
195
|
+
def build_reasoning_part(block)
|
|
196
|
+
case block
|
|
197
|
+
when ::Anthropic::Models::ThinkingBlock
|
|
198
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
199
|
+
type: :text,
|
|
200
|
+
text: block.thinking,
|
|
201
|
+
signature: block.signature,
|
|
202
|
+
format: REASONING_FORMAT,
|
|
203
|
+
)
|
|
204
|
+
when ::Anthropic::Models::RedactedThinkingBlock
|
|
205
|
+
Riffer::Messages::Assistant::ReasoningPart.new(type: :encrypted, data: block.data, format: REASONING_FORMAT)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
202
209
|
#--
|
|
203
210
|
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
204
211
|
def execute_stream(params, yielder)
|
|
205
212
|
current_state = {
|
|
206
213
|
text: nil,
|
|
207
|
-
reasoning: nil,
|
|
208
214
|
tool_call: nil,
|
|
209
215
|
web_search_index: nil,
|
|
210
216
|
web_search_json: nil,
|
|
@@ -230,19 +236,15 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
230
236
|
when ::Anthropic::Helpers::Streaming::TextEvent
|
|
231
237
|
handle_text_event(event, state: current_state, yielder: yielder)
|
|
232
238
|
when ::Anthropic::Helpers::Streaming::ThinkingEvent
|
|
233
|
-
|
|
239
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(event.thinking)
|
|
234
240
|
when ::Anthropic::Helpers::Streaming::InputJsonEvent
|
|
235
241
|
handle_input_json_event(event, state: current_state, yielder: yielder)
|
|
236
242
|
when ::Anthropic::Helpers::Streaming::ContentBlockStopEvent
|
|
237
243
|
case event.content_block
|
|
238
|
-
when ::Anthropic::Models::TextBlock
|
|
239
|
-
handle_content_block_stop_text(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
240
244
|
when ::Anthropic::Models::ToolUseBlock
|
|
241
245
|
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder)
|
|
242
|
-
when ::Anthropic::Models::ThinkingBlock
|
|
243
|
-
|
|
244
|
-
handle_content_block_stop_thinking(event, state: current_state, yielder: yielder)
|
|
245
|
-
end
|
|
246
|
+
when ::Anthropic::Models::ThinkingBlock, ::Anthropic::Models::RedactedThinkingBlock
|
|
247
|
+
handle_content_block_stop_reasoning(event, yielder: yielder)
|
|
246
248
|
when ::Anthropic::Models::ServerToolUseBlock
|
|
247
249
|
handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder)
|
|
248
250
|
when ::Anthropic::Models::WebSearchToolResultBlock
|
|
@@ -250,7 +252,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
250
252
|
end
|
|
251
253
|
when ::Anthropic::Helpers::Streaming::MessageStopEvent
|
|
252
254
|
stream_completed = true
|
|
253
|
-
handle_message_stop(
|
|
255
|
+
handle_message_stop(
|
|
256
|
+
event, state: current_state, accumulated_message: stream.accumulated_message, yielder: yielder,
|
|
257
|
+
)
|
|
254
258
|
end
|
|
255
259
|
end
|
|
256
260
|
ensure
|
|
@@ -287,24 +291,13 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
287
291
|
#--
|
|
288
292
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
289
293
|
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.
|
|
294
|
+
# Mutating append avoids O(n^2) copying across the response; the buffer
|
|
295
|
+
# reaches TextDone only at message stop, so no reader sees it mid-append.
|
|
295
296
|
state[:text] ||= +""
|
|
296
297
|
state[:text] << event.text
|
|
297
298
|
yielder << Riffer::StreamEvents::TextDelta.new(event.text)
|
|
298
299
|
end
|
|
299
300
|
|
|
300
|
-
#--
|
|
301
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
302
|
-
def handle_thinking_event(event, state:, yielder:)
|
|
303
|
-
state[:reasoning] ||= +""
|
|
304
|
-
state[:reasoning] << event.thinking
|
|
305
|
-
yielder << Riffer::StreamEvents::ReasoningDelta.new(event.thinking)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
301
|
#--
|
|
309
302
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
310
303
|
def handle_input_json_event(event, state:, yielder:)
|
|
@@ -335,18 +328,13 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
335
328
|
state[:tool_call] = nil
|
|
336
329
|
end
|
|
337
330
|
|
|
331
|
+
# The SDK's accumulated block already carries the full thinking text and the
|
|
332
|
+
# signature from its trailing signature_delta.
|
|
338
333
|
#--
|
|
339
|
-
#: (untyped,
|
|
340
|
-
def
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
#--
|
|
346
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
347
|
-
def handle_content_block_stop_text(_event, state:, yielder:)
|
|
348
|
-
yielder << Riffer::StreamEvents::TextDone.new(state[:text])
|
|
349
|
-
state[:text] = nil
|
|
334
|
+
#: (untyped, yielder: Riffer::Providers::_EventSink) -> void
|
|
335
|
+
def handle_content_block_stop_reasoning(event, yielder:)
|
|
336
|
+
part = build_reasoning_part(event.content_block) #: Riffer::Messages::Assistant::ReasoningPart
|
|
337
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
350
338
|
end
|
|
351
339
|
|
|
352
340
|
#--
|
|
@@ -376,8 +364,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
376
364
|
end
|
|
377
365
|
|
|
378
366
|
#--
|
|
379
|
-
#: (untyped, accumulated_message: untyped, yielder: Riffer::Providers::_EventSink) -> void
|
|
380
|
-
def handle_message_stop(_event, accumulated_message:, yielder:)
|
|
367
|
+
#: (untyped, state: Hash[Symbol, untyped], accumulated_message: untyped, yielder: Riffer::Providers::_EventSink) -> void
|
|
368
|
+
def handle_message_stop(_event, state:, accumulated_message:, yielder:)
|
|
369
|
+
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) if state[:text]
|
|
370
|
+
|
|
381
371
|
message = accumulated_message #: Anthropic::Models::Message?
|
|
382
372
|
yield_finish_reason(yielder, build_finish_reason(message&.stop_reason))
|
|
383
373
|
|
|
@@ -428,7 +418,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
428
418
|
#--
|
|
429
419
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
430
420
|
def convert_assistant_to_anthropic_format(message)
|
|
431
|
-
content =
|
|
421
|
+
content = message.reasoning.filter_map do |part|
|
|
422
|
+
convert_reasoning_part_to_anthropic_format(part) if part.format == REASONING_FORMAT
|
|
423
|
+
end
|
|
432
424
|
content << { type: "text", text: message.content } if message.content && !message.content.empty?
|
|
433
425
|
|
|
434
426
|
message.tool_calls.each do |tc|
|
|
@@ -443,6 +435,14 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
443
435
|
{ role: "assistant", content: content }
|
|
444
436
|
end
|
|
445
437
|
|
|
438
|
+
#--
|
|
439
|
+
#: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
|
|
440
|
+
def convert_reasoning_part_to_anthropic_format(part)
|
|
441
|
+
return { type: "redacted_thinking", data: part.data } if part.type == :encrypted
|
|
442
|
+
|
|
443
|
+
{ type: "thinking", thinking: part.text, signature: part.signature }
|
|
444
|
+
end
|
|
445
|
+
|
|
446
446
|
#--
|
|
447
447
|
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
448
448
|
def convert_file_part_to_anthropic_format(file)
|