riffer 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/rules/comments.md +2 -4
- data/.claude/rules/rbs-inline.md +2 -12
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +19 -0
- data/docs/AGENTS.md +3 -5
- data/docs/AGENT_LIFECYCLE.md +13 -16
- data/docs/CONFIGURATION.md +22 -33
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +31 -0
- data/docs/providers/OPENROUTER.md +18 -1
- data/docs-site/build.rb +0 -6
- data/docs-site/check.rb +1 -5
- data/lib/riffer/agent/config.rb +4 -45
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +0 -38
- data/lib/riffer/agent/run.rb +11 -43
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +25 -38
- data/lib/riffer/agent/structured_output/result.rb +0 -8
- data/lib/riffer/agent/structured_output.rb +0 -7
- data/lib/riffer/agent.rb +10 -130
- data/lib/riffer/config/amazon_bedrock.rb +30 -0
- data/lib/riffer/config/anthropic.rb +21 -0
- data/lib/riffer/config/azure_open_ai.rb +30 -0
- data/lib/riffer/config/evals.rb +18 -0
- data/lib/riffer/config/files.rb +61 -0
- data/lib/riffer/config/gemini.rb +21 -0
- data/lib/riffer/config/mcp.rb +31 -0
- data/lib/riffer/config/open_ai.rb +30 -0
- data/lib/riffer/config/open_router.rb +21 -0
- data/lib/riffer/config/pricing/rates.rb +36 -0
- data/lib/riffer/config/pricing.rb +64 -0
- data/lib/riffer/config/skills.rb +38 -0
- data/lib/riffer/config/tracing.rb +43 -0
- data/lib/riffer/config.rb +3 -362
- data/lib/riffer/evals/evaluator.rb +1 -29
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +0 -8
- data/lib/riffer/evals/result.rb +1 -11
- data/lib/riffer/evals/run_result.rb +0 -12
- data/lib/riffer/evals/scenario_result.rb +0 -19
- data/lib/riffer/files/downloader.rb +2 -3
- data/lib/riffer/files/resolver.rb +2 -7
- data/lib/riffer/guardrail.rb +2 -22
- data/lib/riffer/guardrails/modification.rb +0 -8
- data/lib/riffer/guardrails/result.rb +0 -17
- data/lib/riffer/guardrails/runner.rb +2 -11
- data/lib/riffer/guardrails/tripwire.rb +0 -8
- data/lib/riffer/guardrails.rb +0 -2
- data/lib/riffer/helpers/boolean.rb +0 -4
- data/lib/riffer/helpers/call_or_value.rb +0 -3
- data/lib/riffer/helpers/deep_dup.rb +3 -8
- data/lib/riffer/helpers/dependencies.rb +0 -4
- data/lib/riffer/helpers/identifier.rb +3 -12
- data/lib/riffer/helpers/validate.rb +42 -0
- data/lib/riffer/mcp/authenticated_tool.rb +4 -12
- data/lib/riffer/mcp/client.rb +0 -7
- data/lib/riffer/mcp/manifest.rb +3 -7
- data/lib/riffer/mcp/registration.rb +0 -10
- data/lib/riffer/mcp/registry.rb +0 -9
- data/lib/riffer/mcp/search_tool.rb +0 -4
- data/lib/riffer/mcp/tool.rb +1 -4
- data/lib/riffer/mcp/tool_factory.rb +3 -6
- data/lib/riffer/mcp.rb +2 -23
- data/lib/riffer/messages/assistant/reasoning_part.rb +4 -21
- data/lib/riffer/messages/assistant/tool_call.rb +1 -9
- data/lib/riffer/messages/assistant.rb +1 -21
- data/lib/riffer/messages/base.rb +0 -12
- data/lib/riffer/messages/system.rb +0 -3
- data/lib/riffer/messages/tool.rb +0 -15
- data/lib/riffer/messages/user/file_part.rb +2 -30
- data/lib/riffer/messages/user.rb +0 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +3 -31
- data/lib/riffer/params.rb +8 -41
- data/lib/riffer/providers/amazon_bedrock.rb +101 -42
- data/lib/riffer/providers/anthropic.rb +9 -24
- data/lib/riffer/providers/azure_open_ai.rb +3 -8
- data/lib/riffer/providers/base.rb +7 -28
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +4 -10
- data/lib/riffer/providers/mock.rb +1 -26
- data/lib/riffer/providers/open_ai.rb +7 -17
- data/lib/riffer/providers/open_router.rb +105 -30
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +5 -14
- data/lib/riffer/registrable.rb +11 -45
- data/lib/riffer/runner/fibers.rb +1 -6
- data/lib/riffer/runner/sequential.rb +0 -1
- data/lib/riffer/runner/threaded.rb +0 -3
- data/lib/riffer/runner.rb +0 -3
- data/lib/riffer/skills/activate_tool.rb +0 -3
- data/lib/riffer/skills/adapter.rb +0 -8
- data/lib/riffer/skills/backend.rb +2 -7
- data/lib/riffer/skills/config.rb +4 -20
- data/lib/riffer/skills/context.rb +0 -29
- data/lib/riffer/skills/filesystem_backend.rb +0 -7
- data/lib/riffer/skills/frontmatter.rb +2 -16
- data/lib/riffer/skills/markdown_adapter.rb +3 -6
- data/lib/riffer/skills/xml_adapter.rb +0 -3
- data/lib/riffer/stream_events/base.rb +0 -3
- data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
- data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
- data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
- data/lib/riffer/stream_events/interrupt.rb +2 -12
- data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
- data/lib/riffer/stream_events/reasoning_done.rb +1 -5
- data/lib/riffer/stream_events/skill_activation.rb +0 -3
- data/lib/riffer/stream_events/text_delta.rb +0 -2
- data/lib/riffer/stream_events/text_done.rb +0 -2
- data/lib/riffer/stream_events/token_usage_done.rb +0 -2
- data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
- data/lib/riffer/stream_events/tool_call_done.rb +0 -5
- data/lib/riffer/stream_events/web_search_done.rb +0 -3
- data/lib/riffer/stream_events/web_search_status.rb +1 -5
- data/lib/riffer/testing/minitest.rb +4 -5
- data/lib/riffer/testing.rb +5 -38
- data/lib/riffer/tool.rb +2 -28
- data/lib/riffer/tools/response.rb +3 -32
- data/lib/riffer/tools/runtime/fibers.rb +0 -6
- data/lib/riffer/tools/runtime/inline.rb +0 -1
- data/lib/riffer/tools/runtime/threaded.rb +0 -6
- data/lib/riffer/tools/runtime.rb +5 -26
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +3 -5
- data/lib/riffer/tracing/no_op.rb +0 -7
- data/lib/riffer/tracing/otel.rb +7 -16
- data/lib/riffer/tracing/stream_recorder.rb +0 -7
- data/lib/riffer/tracing.rb +4 -27
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -30
- data/sig/generated/riffer/agent/config.rbs +12 -48
- data/sig/generated/riffer/agent/context.rbs +2 -26
- data/sig/generated/riffer/agent/outcome.rbs +0 -20
- data/sig/generated/riffer/agent/response.rbs +1 -29
- data/sig/generated/riffer/agent/run.rbs +1 -27
- data/sig/generated/riffer/agent/serializer.rbs +2 -27
- data/sig/generated/riffer/agent/session/repair.rbs +2 -10
- data/sig/generated/riffer/agent/session.rbs +10 -36
- data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
- data/sig/generated/riffer/agent/structured_output.rbs +0 -7
- data/sig/generated/riffer/agent.rbs +5 -121
- data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
- data/sig/generated/riffer/config/anthropic.rbs +15 -0
- data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
- data/sig/generated/riffer/config/evals.rbs +13 -0
- data/sig/generated/riffer/config/files.rbs +43 -0
- data/sig/generated/riffer/config/gemini.rbs +15 -0
- data/sig/generated/riffer/config/mcp.rbs +19 -0
- data/sig/generated/riffer/config/open_ai.rbs +21 -0
- data/sig/generated/riffer/config/open_router.rbs +15 -0
- data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
- data/sig/generated/riffer/config/pricing.rbs +31 -0
- data/sig/generated/riffer/config/skills.rbs +19 -0
- data/sig/generated/riffer/config/tracing.rbs +25 -0
- data/sig/generated/riffer/config.rbs +2 -307
- data/sig/generated/riffer/evals/evaluator.rbs +0 -28
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +0 -7
- data/sig/generated/riffer/evals/result.rbs +1 -11
- data/sig/generated/riffer/evals/run_result.rbs +0 -12
- data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
- data/sig/generated/riffer/files/resolver.rbs +0 -7
- data/sig/generated/riffer/guardrail.rbs +2 -22
- data/sig/generated/riffer/guardrails/modification.rbs +0 -6
- data/sig/generated/riffer/guardrails/result.rbs +0 -15
- data/sig/generated/riffer/guardrails/runner.rbs +0 -11
- data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
- data/sig/generated/riffer/guardrails.rbs +0 -2
- data/sig/generated/riffer/helpers/boolean.rbs +0 -4
- data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
- data/sig/generated/riffer/helpers/deep_dup.rbs +0 -8
- data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
- data/sig/generated/riffer/helpers/identifier.rbs +0 -12
- data/sig/generated/riffer/helpers/validate.rbs +21 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
- data/sig/generated/riffer/mcp/client.rbs +0 -7
- data/sig/generated/riffer/mcp/manifest.rbs +3 -7
- data/sig/generated/riffer/mcp/registration.rbs +0 -10
- data/sig/generated/riffer/mcp/registry.rbs +0 -9
- data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
- data/sig/generated/riffer/mcp.rbs +2 -22
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +4 -17
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +1 -9
- data/sig/generated/riffer/messages/assistant.rbs +10 -30
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +0 -3
- data/sig/generated/riffer/messages/tool.rbs +0 -12
- data/sig/generated/riffer/messages/user/file_part.rbs +0 -30
- data/sig/generated/riffer/messages/user.rbs +0 -4
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +0 -31
- data/sig/generated/riffer/params.rbs +4 -40
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +29 -25
- data/sig/generated/riffer/providers/anthropic.rbs +0 -10
- data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
- data/sig/generated/riffer/providers/base.rbs +0 -28
- data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
- data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
- data/sig/generated/riffer/providers/gemini.rbs +0 -6
- data/sig/generated/riffer/providers/mock.rbs +0 -26
- data/sig/generated/riffer/providers/open_ai.rbs +2 -9
- data/sig/generated/riffer/providers/open_router.rbs +33 -14
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +5 -14
- data/sig/generated/riffer/registrable.rbs +0 -45
- data/sig/generated/riffer/runner/fibers.rbs +0 -6
- data/sig/generated/riffer/runner/sequential.rbs +0 -1
- data/sig/generated/riffer/runner/threaded.rbs +0 -3
- data/sig/generated/riffer/runner.rbs +0 -3
- data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
- data/sig/generated/riffer/skills/adapter.rbs +0 -8
- data/sig/generated/riffer/skills/backend.rbs +2 -7
- data/sig/generated/riffer/skills/config.rbs +0 -20
- data/sig/generated/riffer/skills/context.rbs +0 -29
- data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
- data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
- data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
- data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
- data/sig/generated/riffer/stream_events/base.rbs +0 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
- data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
- data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
- data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
- data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +1 -5
- data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
- data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
- data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
- data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
- data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
- data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
- data/sig/generated/riffer/testing.rbs +0 -38
- data/sig/generated/riffer/tool.rbs +0 -27
- data/sig/generated/riffer/tools/response.rbs +3 -29
- data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
- data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
- data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
- data/sig/generated/riffer/tools/runtime.rbs +2 -24
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +2 -5
- data/sig/generated/riffer/tracing/no_op.rbs +0 -7
- data/sig/generated/riffer/tracing/otel.rbs +7 -16
- data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
- data/sig/generated/riffer/tracing.rbs +4 -23
- data/sig/generated/riffer.rbs +2 -29
- data/sig/manual/riffer/helpers/validate.rbs +5 -0
- metadata +30 -1
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
301
|
handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
268
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]
|
|
269
304
|
when Aws::BedrockRuntime::Types::MessageStopEvent
|
|
270
305
|
stream_completed = true
|
|
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 pre-append string before block stop clears it.
|
|
341
366
|
state[:text] ||= +""
|
|
342
367
|
state[:text] << delta_text
|
|
343
368
|
yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
|
|
@@ -358,6 +383,29 @@ 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.
|
|
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
|
+
|
|
361
409
|
#--
|
|
362
410
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
363
411
|
def handle_content_block_stop_text_delta(_event, state:, yielder:)
|
|
@@ -415,7 +463,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
415
463
|
#--
|
|
416
464
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
417
465
|
def convert_assistant_to_bedrock_format(message)
|
|
418
|
-
content =
|
|
466
|
+
content = message.reasoning.filter_map do |part|
|
|
467
|
+
convert_reasoning_part_to_bedrock_format(part) if part.format == REASONING_FORMAT
|
|
468
|
+
end
|
|
419
469
|
content << { text: message.content } if message.content && !message.content.empty?
|
|
420
470
|
|
|
421
471
|
message.tool_calls.each do |tc|
|
|
@@ -431,6 +481,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
431
481
|
{ role: "assistant", content: content }
|
|
432
482
|
end
|
|
433
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
|
+
|
|
434
493
|
#--
|
|
435
494
|
#: (Array[Hash[Symbol, untyped]], Riffer::Messages::Tool) -> void
|
|
436
495
|
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,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)
|
|
@@ -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,11 +24,11 @@ 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
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)
|
|
@@ -77,8 +67,6 @@ class Riffer::Providers::Base
|
|
|
77
67
|
end
|
|
78
68
|
end
|
|
79
69
|
|
|
80
|
-
# Streams text from the provider.
|
|
81
|
-
#
|
|
82
70
|
#--
|
|
83
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]
|
|
84
72
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
@@ -119,21 +107,17 @@ class Riffer::Providers::Base
|
|
|
119
107
|
Riffer::Helpers::Dependencies.depends_on(gem_name)
|
|
120
108
|
end
|
|
121
109
|
|
|
122
|
-
# Returns the client for the current LLM call. A configured client wins,
|
|
123
|
-
# resolved on every call so a Proc can vary the client by process or
|
|
124
|
-
# credential lifetime; otherwise the provider builds one from the configured
|
|
125
|
-
# credentials, memoized for the life of the provider.
|
|
126
110
|
#--
|
|
127
111
|
#: () -> untyped
|
|
128
112
|
def client
|
|
129
113
|
configured = global_client
|
|
114
|
+
# Resolved on every call, never memoized, so a Proc can vary the client by
|
|
115
|
+
# process or credential lifetime.
|
|
130
116
|
return Riffer::Helpers::CallOrValue.resolve(configured) if configured
|
|
131
117
|
|
|
132
118
|
@client ||= build_client
|
|
133
119
|
end
|
|
134
120
|
|
|
135
|
-
# Returns the consumer-configured client for this provider; nil when none is
|
|
136
|
-
# configured, and for providers that take no configuration at all.
|
|
137
121
|
#--
|
|
138
122
|
#: () -> untyped
|
|
139
123
|
def global_client
|
|
@@ -218,19 +202,17 @@ class Riffer::Providers::Base
|
|
|
218
202
|
pricing.rates_for("#{key}/#{model}")
|
|
219
203
|
end
|
|
220
204
|
|
|
221
|
-
# Defaults to nil rather than raising — finish reasons are optional, so
|
|
222
|
-
# providers that don't report one stay valid.
|
|
223
205
|
#--
|
|
224
206
|
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
225
207
|
def extract_finish_reason(_response)
|
|
208
|
+
# Optional hook, so providers that don't report a finish reason stay valid.
|
|
226
209
|
nil
|
|
227
210
|
end
|
|
228
211
|
|
|
229
|
-
# Defaults to no parts rather than raising — reasoning parts are optional, so
|
|
230
|
-
# providers that don't expose replayable reasoning stay valid.
|
|
231
212
|
#--
|
|
232
213
|
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
233
214
|
def extract_reasoning(_response)
|
|
215
|
+
# Optional hook, so providers that don't expose replayable reasoning stay valid.
|
|
234
216
|
[]
|
|
235
217
|
end
|
|
236
218
|
|
|
@@ -295,8 +277,6 @@ class Riffer::Providers::Base
|
|
|
295
277
|
attributes.merge(tag_attributes(options[:tags] || {}))
|
|
296
278
|
end
|
|
297
279
|
|
|
298
|
-
# Maps normalized tags to their namespaced span attribute form. An empty map
|
|
299
|
-
# yields an empty hash, so merging it is a no-op.
|
|
300
280
|
#--
|
|
301
281
|
#: (Hash[String, String]) -> Hash[String, String]
|
|
302
282
|
def tag_attributes(tags)
|
|
@@ -358,11 +338,10 @@ class Riffer::Providers::Base
|
|
|
358
338
|
Riffer.config.tracing.capture_messages && span.recording?
|
|
359
339
|
end
|
|
360
340
|
|
|
361
|
-
# Wraps reasoning text that an adapter cannot yet replay in a +:text+ part
|
|
362
|
-
# with no +format+, so it persists for display but is never sent back.
|
|
363
341
|
#--
|
|
364
342
|
#: (Riffer::Providers::_EventSink, String) -> void
|
|
365
343
|
def yield_reasoning_done(yielder, text)
|
|
344
|
+
# No +format+, so the reasoning persists for display but is never replayed.
|
|
366
345
|
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: text)
|
|
367
346
|
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
368
347
|
end
|
|
@@ -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)
|