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.
Files changed (258) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/comments.md +2 -4
  3. data/.claude/rules/rbs-inline.md +2 -12
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +5 -0
  6. data/CHANGELOG.md +19 -0
  7. data/docs/AGENTS.md +3 -5
  8. data/docs/AGENT_LIFECYCLE.md +13 -16
  9. data/docs/CONFIGURATION.md +22 -33
  10. data/docs/TOOL_ADVANCED.md +1 -3
  11. data/docs/TRACING.md +1 -1
  12. data/docs/providers/AMAZON_BEDROCK.md +31 -0
  13. data/docs/providers/OPENROUTER.md +18 -1
  14. data/docs-site/build.rb +0 -6
  15. data/docs-site/check.rb +1 -5
  16. data/lib/riffer/agent/config.rb +4 -45
  17. data/lib/riffer/agent/context.rb +2 -26
  18. data/lib/riffer/agent/outcome.rb +0 -20
  19. data/lib/riffer/agent/response.rb +0 -38
  20. data/lib/riffer/agent/run.rb +11 -43
  21. data/lib/riffer/agent/serializer.rb +10 -39
  22. data/lib/riffer/agent/session/repair.rb +3 -15
  23. data/lib/riffer/agent/session.rb +25 -38
  24. data/lib/riffer/agent/structured_output/result.rb +0 -8
  25. data/lib/riffer/agent/structured_output.rb +0 -7
  26. data/lib/riffer/agent.rb +10 -130
  27. data/lib/riffer/config/amazon_bedrock.rb +30 -0
  28. data/lib/riffer/config/anthropic.rb +21 -0
  29. data/lib/riffer/config/azure_open_ai.rb +30 -0
  30. data/lib/riffer/config/evals.rb +18 -0
  31. data/lib/riffer/config/files.rb +61 -0
  32. data/lib/riffer/config/gemini.rb +21 -0
  33. data/lib/riffer/config/mcp.rb +31 -0
  34. data/lib/riffer/config/open_ai.rb +30 -0
  35. data/lib/riffer/config/open_router.rb +21 -0
  36. data/lib/riffer/config/pricing/rates.rb +36 -0
  37. data/lib/riffer/config/pricing.rb +64 -0
  38. data/lib/riffer/config/skills.rb +38 -0
  39. data/lib/riffer/config/tracing.rb +43 -0
  40. data/lib/riffer/config.rb +3 -362
  41. data/lib/riffer/evals/evaluator.rb +1 -29
  42. data/lib/riffer/evals/evaluator_runner.rb +0 -15
  43. data/lib/riffer/evals/judge.rb +0 -8
  44. data/lib/riffer/evals/result.rb +1 -11
  45. data/lib/riffer/evals/run_result.rb +0 -12
  46. data/lib/riffer/evals/scenario_result.rb +0 -19
  47. data/lib/riffer/files/downloader.rb +2 -3
  48. data/lib/riffer/files/resolver.rb +2 -7
  49. data/lib/riffer/guardrail.rb +2 -22
  50. data/lib/riffer/guardrails/modification.rb +0 -8
  51. data/lib/riffer/guardrails/result.rb +0 -17
  52. data/lib/riffer/guardrails/runner.rb +2 -11
  53. data/lib/riffer/guardrails/tripwire.rb +0 -8
  54. data/lib/riffer/guardrails.rb +0 -2
  55. data/lib/riffer/helpers/boolean.rb +0 -4
  56. data/lib/riffer/helpers/call_or_value.rb +0 -3
  57. data/lib/riffer/helpers/deep_dup.rb +3 -8
  58. data/lib/riffer/helpers/dependencies.rb +0 -4
  59. data/lib/riffer/helpers/identifier.rb +3 -12
  60. data/lib/riffer/helpers/validate.rb +42 -0
  61. data/lib/riffer/mcp/authenticated_tool.rb +4 -12
  62. data/lib/riffer/mcp/client.rb +0 -7
  63. data/lib/riffer/mcp/manifest.rb +3 -7
  64. data/lib/riffer/mcp/registration.rb +0 -10
  65. data/lib/riffer/mcp/registry.rb +0 -9
  66. data/lib/riffer/mcp/search_tool.rb +0 -4
  67. data/lib/riffer/mcp/tool.rb +1 -4
  68. data/lib/riffer/mcp/tool_factory.rb +3 -6
  69. data/lib/riffer/mcp.rb +2 -23
  70. data/lib/riffer/messages/assistant/reasoning_part.rb +4 -21
  71. data/lib/riffer/messages/assistant/tool_call.rb +1 -9
  72. data/lib/riffer/messages/assistant.rb +1 -21
  73. data/lib/riffer/messages/base.rb +0 -12
  74. data/lib/riffer/messages/system.rb +0 -3
  75. data/lib/riffer/messages/tool.rb +0 -15
  76. data/lib/riffer/messages/user/file_part.rb +2 -30
  77. data/lib/riffer/messages/user.rb +0 -4
  78. data/lib/riffer/params/boolean.rb +1 -5
  79. data/lib/riffer/params/param.rb +3 -31
  80. data/lib/riffer/params.rb +8 -41
  81. data/lib/riffer/providers/amazon_bedrock.rb +101 -42
  82. data/lib/riffer/providers/anthropic.rb +9 -24
  83. data/lib/riffer/providers/azure_open_ai.rb +3 -8
  84. data/lib/riffer/providers/base.rb +7 -28
  85. data/lib/riffer/providers/finish_reason.rb +0 -6
  86. data/lib/riffer/providers/gemini/client.rb +4 -17
  87. data/lib/riffer/providers/gemini.rb +4 -10
  88. data/lib/riffer/providers/mock.rb +1 -26
  89. data/lib/riffer/providers/open_ai.rb +7 -17
  90. data/lib/riffer/providers/open_router.rb +105 -30
  91. data/lib/riffer/providers/repository.rb +2 -14
  92. data/lib/riffer/providers/token_usage.rb +5 -14
  93. data/lib/riffer/registrable.rb +11 -45
  94. data/lib/riffer/runner/fibers.rb +1 -6
  95. data/lib/riffer/runner/sequential.rb +0 -1
  96. data/lib/riffer/runner/threaded.rb +0 -3
  97. data/lib/riffer/runner.rb +0 -3
  98. data/lib/riffer/skills/activate_tool.rb +0 -3
  99. data/lib/riffer/skills/adapter.rb +0 -8
  100. data/lib/riffer/skills/backend.rb +2 -7
  101. data/lib/riffer/skills/config.rb +4 -20
  102. data/lib/riffer/skills/context.rb +0 -29
  103. data/lib/riffer/skills/filesystem_backend.rb +0 -7
  104. data/lib/riffer/skills/frontmatter.rb +2 -16
  105. data/lib/riffer/skills/markdown_adapter.rb +3 -6
  106. data/lib/riffer/skills/xml_adapter.rb +0 -3
  107. data/lib/riffer/stream_events/base.rb +0 -3
  108. data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
  109. data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
  110. data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
  111. data/lib/riffer/stream_events/interrupt.rb +2 -12
  112. data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
  113. data/lib/riffer/stream_events/reasoning_done.rb +1 -5
  114. data/lib/riffer/stream_events/skill_activation.rb +0 -3
  115. data/lib/riffer/stream_events/text_delta.rb +0 -2
  116. data/lib/riffer/stream_events/text_done.rb +0 -2
  117. data/lib/riffer/stream_events/token_usage_done.rb +0 -2
  118. data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
  119. data/lib/riffer/stream_events/tool_call_done.rb +0 -5
  120. data/lib/riffer/stream_events/web_search_done.rb +0 -3
  121. data/lib/riffer/stream_events/web_search_status.rb +1 -5
  122. data/lib/riffer/testing/minitest.rb +4 -5
  123. data/lib/riffer/testing.rb +5 -38
  124. data/lib/riffer/tool.rb +2 -28
  125. data/lib/riffer/tools/response.rb +3 -32
  126. data/lib/riffer/tools/runtime/fibers.rb +0 -6
  127. data/lib/riffer/tools/runtime/inline.rb +0 -1
  128. data/lib/riffer/tools/runtime/threaded.rb +0 -6
  129. data/lib/riffer/tools/runtime.rb +5 -26
  130. data/lib/riffer/tools/toolable.rb +0 -37
  131. data/lib/riffer/tracing/capture.rb +3 -5
  132. data/lib/riffer/tracing/no_op.rb +0 -7
  133. data/lib/riffer/tracing/otel.rb +7 -16
  134. data/lib/riffer/tracing/stream_recorder.rb +0 -7
  135. data/lib/riffer/tracing.rb +4 -27
  136. data/lib/riffer/version.rb +1 -1
  137. data/lib/riffer.rb +2 -30
  138. data/sig/generated/riffer/agent/config.rbs +12 -48
  139. data/sig/generated/riffer/agent/context.rbs +2 -26
  140. data/sig/generated/riffer/agent/outcome.rbs +0 -20
  141. data/sig/generated/riffer/agent/response.rbs +1 -29
  142. data/sig/generated/riffer/agent/run.rbs +1 -27
  143. data/sig/generated/riffer/agent/serializer.rbs +2 -27
  144. data/sig/generated/riffer/agent/session/repair.rbs +2 -10
  145. data/sig/generated/riffer/agent/session.rbs +10 -36
  146. data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
  147. data/sig/generated/riffer/agent/structured_output.rbs +0 -7
  148. data/sig/generated/riffer/agent.rbs +5 -121
  149. data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
  150. data/sig/generated/riffer/config/anthropic.rbs +15 -0
  151. data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
  152. data/sig/generated/riffer/config/evals.rbs +13 -0
  153. data/sig/generated/riffer/config/files.rbs +43 -0
  154. data/sig/generated/riffer/config/gemini.rbs +15 -0
  155. data/sig/generated/riffer/config/mcp.rbs +19 -0
  156. data/sig/generated/riffer/config/open_ai.rbs +21 -0
  157. data/sig/generated/riffer/config/open_router.rbs +15 -0
  158. data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
  159. data/sig/generated/riffer/config/pricing.rbs +31 -0
  160. data/sig/generated/riffer/config/skills.rbs +19 -0
  161. data/sig/generated/riffer/config/tracing.rbs +25 -0
  162. data/sig/generated/riffer/config.rbs +2 -307
  163. data/sig/generated/riffer/evals/evaluator.rbs +0 -28
  164. data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
  165. data/sig/generated/riffer/evals/judge.rbs +0 -7
  166. data/sig/generated/riffer/evals/result.rbs +1 -11
  167. data/sig/generated/riffer/evals/run_result.rbs +0 -12
  168. data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
  169. data/sig/generated/riffer/files/resolver.rbs +0 -7
  170. data/sig/generated/riffer/guardrail.rbs +2 -22
  171. data/sig/generated/riffer/guardrails/modification.rbs +0 -6
  172. data/sig/generated/riffer/guardrails/result.rbs +0 -15
  173. data/sig/generated/riffer/guardrails/runner.rbs +0 -11
  174. data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
  175. data/sig/generated/riffer/guardrails.rbs +0 -2
  176. data/sig/generated/riffer/helpers/boolean.rbs +0 -4
  177. data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
  178. data/sig/generated/riffer/helpers/deep_dup.rbs +0 -8
  179. data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
  180. data/sig/generated/riffer/helpers/identifier.rbs +0 -12
  181. data/sig/generated/riffer/helpers/validate.rbs +21 -0
  182. data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
  183. data/sig/generated/riffer/mcp/client.rbs +0 -7
  184. data/sig/generated/riffer/mcp/manifest.rbs +3 -7
  185. data/sig/generated/riffer/mcp/registration.rbs +0 -10
  186. data/sig/generated/riffer/mcp/registry.rbs +0 -9
  187. data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
  188. data/sig/generated/riffer/mcp/tool.rbs +0 -4
  189. data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
  190. data/sig/generated/riffer/mcp.rbs +2 -22
  191. data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +4 -17
  192. data/sig/generated/riffer/messages/assistant/tool_call.rbs +1 -9
  193. data/sig/generated/riffer/messages/assistant.rbs +10 -30
  194. data/sig/generated/riffer/messages/base.rbs +0 -12
  195. data/sig/generated/riffer/messages/system.rbs +0 -3
  196. data/sig/generated/riffer/messages/tool.rbs +0 -12
  197. data/sig/generated/riffer/messages/user/file_part.rbs +0 -30
  198. data/sig/generated/riffer/messages/user.rbs +0 -4
  199. data/sig/generated/riffer/params/boolean.rbs +1 -4
  200. data/sig/generated/riffer/params/param.rbs +0 -31
  201. data/sig/generated/riffer/params.rbs +4 -40
  202. data/sig/generated/riffer/providers/amazon_bedrock.rbs +29 -25
  203. data/sig/generated/riffer/providers/anthropic.rbs +0 -10
  204. data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
  205. data/sig/generated/riffer/providers/base.rbs +0 -28
  206. data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
  207. data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
  208. data/sig/generated/riffer/providers/gemini.rbs +0 -6
  209. data/sig/generated/riffer/providers/mock.rbs +0 -26
  210. data/sig/generated/riffer/providers/open_ai.rbs +2 -9
  211. data/sig/generated/riffer/providers/open_router.rbs +33 -14
  212. data/sig/generated/riffer/providers/repository.rbs +2 -14
  213. data/sig/generated/riffer/providers/token_usage.rbs +5 -14
  214. data/sig/generated/riffer/registrable.rbs +0 -45
  215. data/sig/generated/riffer/runner/fibers.rbs +0 -6
  216. data/sig/generated/riffer/runner/sequential.rbs +0 -1
  217. data/sig/generated/riffer/runner/threaded.rbs +0 -3
  218. data/sig/generated/riffer/runner.rbs +0 -3
  219. data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
  220. data/sig/generated/riffer/skills/adapter.rbs +0 -8
  221. data/sig/generated/riffer/skills/backend.rbs +2 -7
  222. data/sig/generated/riffer/skills/config.rbs +0 -20
  223. data/sig/generated/riffer/skills/context.rbs +0 -29
  224. data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
  225. data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
  226. data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
  227. data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
  228. data/sig/generated/riffer/stream_events/base.rbs +0 -3
  229. data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
  230. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
  231. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
  232. data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
  233. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
  234. data/sig/generated/riffer/stream_events/reasoning_done.rbs +1 -5
  235. data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
  236. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  237. data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
  238. data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
  239. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
  240. data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
  241. data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
  242. data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
  243. data/sig/generated/riffer/testing.rbs +0 -38
  244. data/sig/generated/riffer/tool.rbs +0 -27
  245. data/sig/generated/riffer/tools/response.rbs +3 -29
  246. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
  247. data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
  248. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
  249. data/sig/generated/riffer/tools/runtime.rbs +2 -24
  250. data/sig/generated/riffer/tools/toolable.rbs +0 -37
  251. data/sig/generated/riffer/tracing/capture.rbs +2 -5
  252. data/sig/generated/riffer/tracing/no_op.rbs +0 -7
  253. data/sig/generated/riffer/tracing/otel.rbs +7 -16
  254. data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
  255. data/sig/generated/riffer/tracing.rbs +4 -23
  256. data/sig/generated/riffer.rbs +2 -29
  257. data/sig/manual/riffer/helpers/validate.rbs +5 -0
  258. 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
- # Reconstructs a Params from a JSON Schema object — the inverse of
23
- # +to_json_schema(strict: false)+. Raises Riffer::ArgumentError on features
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+ would leave the copy sharing this one's parameters, so a caller
155
- # defining a parameter on either would reach the other.
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
- # Matches Anthropic models on Bedrock — bare (+anthropic.claude-...+) and
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
- # Returns the skill adapter for the Bedrock model — XML for Anthropic models
26
- # (which Bedrock hosts alongside other vendors'), else Markdown.
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
- # requestMetadata is a flat String=>String map used to filter invocation
100
- # logs; every tag (including the reserved user_id) rides along, since
101
- # Converse has no dedicated end-user field. Merged over any request_metadata
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
- # Use strict schema to make optional fields nullable. Without this,
113
- # Bedrock may return string literals like ": null," instead of actual
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 turns an event-stream +:message-type: error+ frame into an
276
- # EventError instance and hands it to this block as an event; it is
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
- # Mutating append: += would reallocate and copy the whole accumulated
337
- # buffer on every delta (O(n^2) per content block). state[:text] is handed
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 = [] #: Array[Hash[Symbol, untyped]]
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). It carries the reserved user_id tag; all other tags are dropped
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
- # Use strict schema to make optional fields nullable. Without this,
97
- # Anthropic may return empty strings or whitespace instead of null
98
- # for optional fields that the model has no value for.
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 instead of += to avoid reallocating and copying the whole
291
- # accumulated buffer on every delta (O(n^2) per content block). The buffer
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)