riffer 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) 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 +32 -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/STREAM_EVENTS.md +3 -3
  11. data/docs/TOOL_ADVANCED.md +1 -3
  12. data/docs/TRACING.md +1 -1
  13. data/docs/providers/AMAZON_BEDROCK.md +31 -0
  14. data/docs/providers/ANTHROPIC.md +10 -0
  15. data/docs/providers/AZURE_OPENAI.md +2 -0
  16. data/docs/providers/OPENAI.md +22 -0
  17. data/docs/providers/OPENROUTER.md +18 -1
  18. data/docs-site/build.rb +0 -6
  19. data/docs-site/check.rb +1 -5
  20. data/lib/riffer/agent/config.rb +4 -45
  21. data/lib/riffer/agent/context.rb +2 -26
  22. data/lib/riffer/agent/outcome.rb +0 -20
  23. data/lib/riffer/agent/response.rb +0 -38
  24. data/lib/riffer/agent/run.rb +11 -43
  25. data/lib/riffer/agent/serializer.rb +10 -39
  26. data/lib/riffer/agent/session/repair.rb +3 -15
  27. data/lib/riffer/agent/session.rb +25 -38
  28. data/lib/riffer/agent/structured_output/result.rb +0 -8
  29. data/lib/riffer/agent/structured_output.rb +0 -7
  30. data/lib/riffer/agent.rb +10 -130
  31. data/lib/riffer/config/amazon_bedrock.rb +30 -0
  32. data/lib/riffer/config/anthropic.rb +21 -0
  33. data/lib/riffer/config/azure_open_ai.rb +30 -0
  34. data/lib/riffer/config/evals.rb +18 -0
  35. data/lib/riffer/config/files.rb +61 -0
  36. data/lib/riffer/config/gemini.rb +21 -0
  37. data/lib/riffer/config/mcp.rb +31 -0
  38. data/lib/riffer/config/open_ai.rb +30 -0
  39. data/lib/riffer/config/open_router.rb +21 -0
  40. data/lib/riffer/config/pricing/rates.rb +36 -0
  41. data/lib/riffer/config/pricing.rb +64 -0
  42. data/lib/riffer/config/skills.rb +38 -0
  43. data/lib/riffer/config/tracing.rb +43 -0
  44. data/lib/riffer/config.rb +3 -362
  45. data/lib/riffer/evals/evaluator.rb +1 -29
  46. data/lib/riffer/evals/evaluator_runner.rb +0 -15
  47. data/lib/riffer/evals/judge.rb +0 -8
  48. data/lib/riffer/evals/result.rb +1 -11
  49. data/lib/riffer/evals/run_result.rb +0 -12
  50. data/lib/riffer/evals/scenario_result.rb +0 -19
  51. data/lib/riffer/files/downloader.rb +2 -3
  52. data/lib/riffer/files/resolver.rb +2 -7
  53. data/lib/riffer/guardrail.rb +2 -22
  54. data/lib/riffer/guardrails/modification.rb +0 -8
  55. data/lib/riffer/guardrails/result.rb +0 -17
  56. data/lib/riffer/guardrails/runner.rb +2 -11
  57. data/lib/riffer/guardrails/tripwire.rb +0 -8
  58. data/lib/riffer/guardrails.rb +0 -2
  59. data/lib/riffer/helpers/boolean.rb +0 -4
  60. data/lib/riffer/helpers/call_or_value.rb +0 -3
  61. data/lib/riffer/helpers/deep_dup.rb +3 -8
  62. data/lib/riffer/helpers/dependencies.rb +0 -4
  63. data/lib/riffer/helpers/identifier.rb +3 -12
  64. data/lib/riffer/helpers/validate.rb +42 -0
  65. data/lib/riffer/mcp/authenticated_tool.rb +4 -12
  66. data/lib/riffer/mcp/client.rb +0 -7
  67. data/lib/riffer/mcp/manifest.rb +3 -7
  68. data/lib/riffer/mcp/registration.rb +0 -10
  69. data/lib/riffer/mcp/registry.rb +0 -9
  70. data/lib/riffer/mcp/search_tool.rb +0 -4
  71. data/lib/riffer/mcp/tool.rb +1 -4
  72. data/lib/riffer/mcp/tool_factory.rb +3 -6
  73. data/lib/riffer/mcp.rb +2 -23
  74. data/lib/riffer/messages/assistant/reasoning_part.rb +4 -21
  75. data/lib/riffer/messages/assistant/tool_call.rb +1 -9
  76. data/lib/riffer/messages/assistant.rb +1 -21
  77. data/lib/riffer/messages/base.rb +0 -12
  78. data/lib/riffer/messages/system.rb +0 -3
  79. data/lib/riffer/messages/tool.rb +0 -15
  80. data/lib/riffer/messages/user/file_part.rb +2 -30
  81. data/lib/riffer/messages/user.rb +0 -4
  82. data/lib/riffer/params/boolean.rb +1 -5
  83. data/lib/riffer/params/param.rb +3 -31
  84. data/lib/riffer/params.rb +8 -41
  85. data/lib/riffer/providers/amazon_bedrock.rb +98 -46
  86. data/lib/riffer/providers/anthropic.rb +65 -65
  87. data/lib/riffer/providers/azure_open_ai.rb +10 -8
  88. data/lib/riffer/providers/base.rb +7 -28
  89. data/lib/riffer/providers/finish_reason.rb +0 -6
  90. data/lib/riffer/providers/gemini/client.rb +4 -17
  91. data/lib/riffer/providers/gemini.rb +4 -10
  92. data/lib/riffer/providers/mock.rb +1 -26
  93. data/lib/riffer/providers/open_ai.rb +94 -46
  94. data/lib/riffer/providers/open_router.rb +105 -30
  95. data/lib/riffer/providers/repository.rb +2 -14
  96. data/lib/riffer/providers/token_usage.rb +5 -14
  97. data/lib/riffer/registrable.rb +11 -45
  98. data/lib/riffer/runner/fibers.rb +1 -6
  99. data/lib/riffer/runner/sequential.rb +0 -1
  100. data/lib/riffer/runner/threaded.rb +0 -3
  101. data/lib/riffer/runner.rb +0 -3
  102. data/lib/riffer/skills/activate_tool.rb +0 -3
  103. data/lib/riffer/skills/adapter.rb +0 -8
  104. data/lib/riffer/skills/backend.rb +2 -7
  105. data/lib/riffer/skills/config.rb +4 -20
  106. data/lib/riffer/skills/context.rb +0 -29
  107. data/lib/riffer/skills/filesystem_backend.rb +0 -7
  108. data/lib/riffer/skills/frontmatter.rb +2 -16
  109. data/lib/riffer/skills/markdown_adapter.rb +3 -6
  110. data/lib/riffer/skills/xml_adapter.rb +0 -3
  111. data/lib/riffer/stream_events/base.rb +0 -3
  112. data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
  113. data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
  114. data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
  115. data/lib/riffer/stream_events/interrupt.rb +2 -12
  116. data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
  117. data/lib/riffer/stream_events/reasoning_done.rb +1 -5
  118. data/lib/riffer/stream_events/skill_activation.rb +0 -3
  119. data/lib/riffer/stream_events/text_delta.rb +0 -2
  120. data/lib/riffer/stream_events/text_done.rb +0 -2
  121. data/lib/riffer/stream_events/token_usage_done.rb +0 -2
  122. data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
  123. data/lib/riffer/stream_events/tool_call_done.rb +0 -5
  124. data/lib/riffer/stream_events/web_search_done.rb +0 -3
  125. data/lib/riffer/stream_events/web_search_status.rb +1 -5
  126. data/lib/riffer/testing/minitest.rb +4 -5
  127. data/lib/riffer/testing.rb +5 -38
  128. data/lib/riffer/tool.rb +2 -28
  129. data/lib/riffer/tools/response.rb +3 -32
  130. data/lib/riffer/tools/runtime/fibers.rb +0 -6
  131. data/lib/riffer/tools/runtime/inline.rb +0 -1
  132. data/lib/riffer/tools/runtime/threaded.rb +0 -6
  133. data/lib/riffer/tools/runtime.rb +5 -26
  134. data/lib/riffer/tools/toolable.rb +0 -37
  135. data/lib/riffer/tracing/capture.rb +3 -5
  136. data/lib/riffer/tracing/no_op.rb +0 -7
  137. data/lib/riffer/tracing/otel.rb +7 -16
  138. data/lib/riffer/tracing/stream_recorder.rb +0 -7
  139. data/lib/riffer/tracing.rb +4 -27
  140. data/lib/riffer/version.rb +1 -1
  141. data/lib/riffer.rb +2 -30
  142. data/sig/generated/riffer/agent/config.rbs +12 -48
  143. data/sig/generated/riffer/agent/context.rbs +2 -26
  144. data/sig/generated/riffer/agent/outcome.rbs +0 -20
  145. data/sig/generated/riffer/agent/response.rbs +1 -29
  146. data/sig/generated/riffer/agent/run.rbs +1 -27
  147. data/sig/generated/riffer/agent/serializer.rbs +2 -27
  148. data/sig/generated/riffer/agent/session/repair.rbs +2 -10
  149. data/sig/generated/riffer/agent/session.rbs +10 -36
  150. data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
  151. data/sig/generated/riffer/agent/structured_output.rbs +0 -7
  152. data/sig/generated/riffer/agent.rbs +5 -121
  153. data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
  154. data/sig/generated/riffer/config/anthropic.rbs +15 -0
  155. data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
  156. data/sig/generated/riffer/config/evals.rbs +13 -0
  157. data/sig/generated/riffer/config/files.rbs +43 -0
  158. data/sig/generated/riffer/config/gemini.rbs +15 -0
  159. data/sig/generated/riffer/config/mcp.rbs +19 -0
  160. data/sig/generated/riffer/config/open_ai.rbs +21 -0
  161. data/sig/generated/riffer/config/open_router.rbs +15 -0
  162. data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
  163. data/sig/generated/riffer/config/pricing.rbs +31 -0
  164. data/sig/generated/riffer/config/skills.rbs +19 -0
  165. data/sig/generated/riffer/config/tracing.rbs +25 -0
  166. data/sig/generated/riffer/config.rbs +2 -307
  167. data/sig/generated/riffer/evals/evaluator.rbs +0 -28
  168. data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
  169. data/sig/generated/riffer/evals/judge.rbs +0 -7
  170. data/sig/generated/riffer/evals/result.rbs +1 -11
  171. data/sig/generated/riffer/evals/run_result.rbs +0 -12
  172. data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
  173. data/sig/generated/riffer/files/resolver.rbs +0 -7
  174. data/sig/generated/riffer/guardrail.rbs +2 -22
  175. data/sig/generated/riffer/guardrails/modification.rbs +0 -6
  176. data/sig/generated/riffer/guardrails/result.rbs +0 -15
  177. data/sig/generated/riffer/guardrails/runner.rbs +0 -11
  178. data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
  179. data/sig/generated/riffer/guardrails.rbs +0 -2
  180. data/sig/generated/riffer/helpers/boolean.rbs +0 -4
  181. data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
  182. data/sig/generated/riffer/helpers/deep_dup.rbs +0 -8
  183. data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
  184. data/sig/generated/riffer/helpers/identifier.rbs +0 -12
  185. data/sig/generated/riffer/helpers/validate.rbs +21 -0
  186. data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
  187. data/sig/generated/riffer/mcp/client.rbs +0 -7
  188. data/sig/generated/riffer/mcp/manifest.rbs +3 -7
  189. data/sig/generated/riffer/mcp/registration.rbs +0 -10
  190. data/sig/generated/riffer/mcp/registry.rbs +0 -9
  191. data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
  192. data/sig/generated/riffer/mcp/tool.rbs +0 -4
  193. data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
  194. data/sig/generated/riffer/mcp.rbs +2 -22
  195. data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +4 -17
  196. data/sig/generated/riffer/messages/assistant/tool_call.rbs +1 -9
  197. data/sig/generated/riffer/messages/assistant.rbs +10 -30
  198. data/sig/generated/riffer/messages/base.rbs +0 -12
  199. data/sig/generated/riffer/messages/system.rbs +0 -3
  200. data/sig/generated/riffer/messages/tool.rbs +0 -12
  201. data/sig/generated/riffer/messages/user/file_part.rbs +0 -30
  202. data/sig/generated/riffer/messages/user.rbs +0 -4
  203. data/sig/generated/riffer/params/boolean.rbs +1 -4
  204. data/sig/generated/riffer/params/param.rbs +0 -31
  205. data/sig/generated/riffer/params.rbs +4 -40
  206. data/sig/generated/riffer/providers/amazon_bedrock.rbs +26 -26
  207. data/sig/generated/riffer/providers/anthropic.rbs +23 -22
  208. data/sig/generated/riffer/providers/azure_open_ai.rbs +5 -8
  209. data/sig/generated/riffer/providers/base.rbs +0 -28
  210. data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
  211. data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
  212. data/sig/generated/riffer/providers/gemini.rbs +0 -6
  213. data/sig/generated/riffer/providers/mock.rbs +0 -26
  214. data/sig/generated/riffer/providers/open_ai.rbs +36 -19
  215. data/sig/generated/riffer/providers/open_router.rbs +33 -14
  216. data/sig/generated/riffer/providers/repository.rbs +2 -14
  217. data/sig/generated/riffer/providers/token_usage.rbs +5 -14
  218. data/sig/generated/riffer/registrable.rbs +0 -45
  219. data/sig/generated/riffer/runner/fibers.rbs +0 -6
  220. data/sig/generated/riffer/runner/sequential.rbs +0 -1
  221. data/sig/generated/riffer/runner/threaded.rbs +0 -3
  222. data/sig/generated/riffer/runner.rbs +0 -3
  223. data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
  224. data/sig/generated/riffer/skills/adapter.rbs +0 -8
  225. data/sig/generated/riffer/skills/backend.rbs +2 -7
  226. data/sig/generated/riffer/skills/config.rbs +0 -20
  227. data/sig/generated/riffer/skills/context.rbs +0 -29
  228. data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
  229. data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
  230. data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
  231. data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
  232. data/sig/generated/riffer/stream_events/base.rbs +0 -3
  233. data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
  234. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
  235. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
  236. data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
  237. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
  238. data/sig/generated/riffer/stream_events/reasoning_done.rbs +1 -5
  239. data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
  240. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  241. data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
  242. data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
  243. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
  244. data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
  245. data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
  246. data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
  247. data/sig/generated/riffer/testing.rbs +0 -38
  248. data/sig/generated/riffer/tool.rbs +0 -27
  249. data/sig/generated/riffer/tools/response.rbs +3 -29
  250. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
  251. data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
  252. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
  253. data/sig/generated/riffer/tools/runtime.rbs +2 -24
  254. data/sig/generated/riffer/tools/toolable.rbs +0 -37
  255. data/sig/generated/riffer/tracing/capture.rbs +2 -5
  256. data/sig/generated/riffer/tracing/no_op.rbs +0 -7
  257. data/sig/generated/riffer/tracing/otel.rbs +7 -16
  258. data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
  259. data/sig/generated/riffer/tracing.rbs +4 -23
  260. data/sig/generated/riffer.rbs +2 -29
  261. data/sig/manual/riffer/helpers/validate.rbs +5 -0
  262. 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
- handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
268
301
  handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if current_state[:tool_call]
302
+ handle_reasoning_stop(event, state: current_state, yielder: yielder) if current_state[:reasoning]
269
303
  when Aws::BedrockRuntime::Types::MessageStopEvent
270
304
  stream_completed = true
305
+ yielder << Riffer::StreamEvents::TextDone.new(current_state[:text]) if current_state[:text]
271
306
  yield_finish_reason(yielder, build_finish_reason(event.stop_reason))
272
307
  when Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
273
308
  handle_metadata_usage(event, state: current_state, yielder: yielder) if event.usage
274
309
  when Aws::Errors::EventError
275
- # The SDK 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 buffer before message stop hands it to TextDone.
341
366
  state[:text] ||= +""
342
367
  state[:text] << delta_text
343
368
  yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
@@ -358,11 +383,27 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
358
383
  )
359
384
  end
360
385
 
386
+ # A reasoning block streams its text in pieces, then its signature (or its
387
+ # redacted bytes) in a delta of its own.
361
388
  #--
362
389
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
363
- def handle_content_block_stop_text_delta(_event, state:, yielder:)
364
- yielder << Riffer::StreamEvents::TextDone.new(state[:text])
365
- state[:text] = nil
390
+ def handle_reasoning_delta(event, state:, yielder:)
391
+ typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
392
+ delta = typed_event.delta.reasoning_content
393
+ reasoning = state[:reasoning] ||= { text: +"" }
394
+
395
+ reasoning[:text] << delta.text if delta.text
396
+ reasoning[:signature] = delta.signature if delta.signature
397
+ (reasoning[:redacted_content] ||= +"") << delta.redacted_content if delta.redacted_content
398
+
399
+ yielder << Riffer::StreamEvents::ReasoningDelta.new(delta.text) unless delta.text.nil? || delta.text.empty?
400
+ end
401
+
402
+ #--
403
+ #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
404
+ def handle_reasoning_stop(_event, state:, yielder:)
405
+ yielder << Riffer::StreamEvents::ReasoningDone.new(build_reasoning_part(**state[:reasoning]))
406
+ state[:reasoning] = nil
366
407
  end
367
408
 
368
409
  #--
@@ -415,7 +456,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
415
456
  #--
416
457
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
417
458
  def convert_assistant_to_bedrock_format(message)
418
- content = [] #: Array[Hash[Symbol, untyped]]
459
+ content = message.reasoning.filter_map do |part|
460
+ convert_reasoning_part_to_bedrock_format(part) if part.format == REASONING_FORMAT
461
+ end
419
462
  content << { text: message.content } if message.content && !message.content.empty?
420
463
 
421
464
  message.tool_calls.each do |tc|
@@ -431,6 +474,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
431
474
  { role: "assistant", content: content }
432
475
  end
433
476
 
477
+ #--
478
+ #: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
479
+ def convert_reasoning_part_to_bedrock_format(part)
480
+ data = part.data
481
+ return { reasoning_content: { redacted_content: Base64.strict_decode64(data) } } if data
482
+
483
+ { reasoning_content: { reasoning_text: { text: part.text, signature: part.signature }.compact } }
484
+ end
485
+
434
486
  #--
435
487
  #: (Array[Hash[Symbol, untyped]], Riffer::Messages::Tool) -> void
436
488
  def append_tool_result(conversation_messages, message)
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Anthropic provider for Claude models via the Anthropic API. Requires the
5
- # +anthropic+ gem.
6
4
  class Riffer::Providers::Anthropic < Riffer::Providers::Base
7
5
  WEB_SEARCH_TOOL_TYPE = "web_search_20250305" #: String
8
6
 
@@ -18,15 +16,17 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
18
16
  "pause_turn" => :other,
19
17
  }.freeze #: Hash[String, Symbol]
20
18
 
21
- # Returns the XML skill adapter for Anthropic/Claude.
22
- #
19
+ # Only parts carrying this tag are replayed, so reasoning captured by another
20
+ # adapter, whose signatures the Messages API may not accept, never reaches the
21
+ # request.
22
+ REASONING_FORMAT = "anthropic-messages-v1" #: String
23
+
23
24
  #--
24
25
  #: (?String?) -> singleton(Riffer::Skills::Adapter)
25
26
  def self.skills_adapter(_model = nil)
26
27
  Riffer::Skills::XmlAdapter
27
28
  end
28
29
 
29
- # The GenAI semconv well-known provider name.
30
30
  #--
31
31
  #: () -> String
32
32
  def self.semconv_provider_name
@@ -48,12 +48,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
48
48
  Riffer.config.anthropic.client
49
49
  end
50
50
 
51
- # Compacted for the same reason as the other providers: never hand an SDK an
52
- # explicit nil credential, so its own +ANTHROPIC_API_KEY+ resolution stays
53
- # reachable regardless of how that SDK distinguishes nil from absent.
54
51
  #--
55
52
  #: () -> untyped
56
53
  def build_client
54
+ # Never pass an explicit nil credential, so the SDK's own
55
+ # ANTHROPIC_API_KEY resolution stays reachable.
57
56
  ::Anthropic::Client.new(**{ api_key: Riffer.config.anthropic.api_key }.compact)
58
57
  end
59
58
 
@@ -78,8 +77,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
78
77
  params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
79
78
 
80
79
  # Anthropic's only request-metadata field is metadata.user_id (opaque, no
81
- # PII). It carries the reserved user_id tag; all other tags are dropped
82
- # here and survive only on spans.
80
+ # PII); all other tags survive only on spans.
83
81
  user_id = tags["user_id"]
84
82
  params[:metadata] = { user_id: user_id } if user_id
85
83
 
@@ -93,13 +91,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
93
91
  end
94
92
 
95
93
  if structured_output
96
- # 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.
94
+ # Strict schema makes optional fields nullable; otherwise Anthropic may
95
+ # return empty strings or whitespace instead of null. The format wins
96
+ # over caller output_config keys because the run loop validates against it.
103
97
  params[:output_config] = {
104
98
  **(params[:output_config] || {}),
105
99
  format: {
@@ -143,8 +137,6 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
143
137
  Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(raw, :other), raw: raw)
144
138
  end
145
139
 
146
- # Anthropic's +input_tokens+ excludes the cache buckets; TokenUsage's
147
- # input includes them.
148
140
  #--
149
141
  #: (untyped) -> Riffer::Providers::TokenUsage
150
142
  def build_token_usage(usage)
@@ -153,6 +145,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
153
145
 
154
146
  apply_pricing(
155
147
  Riffer::Providers::TokenUsage.new(
148
+ # Anthropic's input_tokens excludes the cache buckets; TokenUsage's includes them.
156
149
  input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
157
150
  output_tokens: usage.output_tokens,
158
151
  cache_write_tokens: cache_write,
@@ -165,16 +158,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
165
158
  #: (untyped) -> String
166
159
  def extract_content(response)
167
160
  message = response #: Anthropic::Models::Message
168
- content_blocks = message.content
169
- return "" if content_blocks.nil? || content_blocks.empty?
170
-
171
- text_content = ""
172
-
173
- content_blocks.each do |block|
174
- text_content = block.text if block.is_a?(::Anthropic::Models::TextBlock)
175
- end
176
-
177
- text_content
161
+ (message.content || []).filter_map { |block| block.text if block.is_a?(::Anthropic::Models::TextBlock) }.join
178
162
  end
179
163
 
180
164
  #--
@@ -199,12 +183,34 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
199
183
  tool_calls
200
184
  end
201
185
 
186
+ #--
187
+ #: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
188
+ def extract_reasoning(response)
189
+ message = response #: Anthropic::Models::Message
190
+ (message.content || []).filter_map { |block| build_reasoning_part(block) }
191
+ end
192
+
193
+ #--
194
+ #: (untyped) -> Riffer::Messages::Assistant::ReasoningPart?
195
+ def build_reasoning_part(block)
196
+ case block
197
+ when ::Anthropic::Models::ThinkingBlock
198
+ Riffer::Messages::Assistant::ReasoningPart.new(
199
+ type: :text,
200
+ text: block.thinking,
201
+ signature: block.signature,
202
+ format: REASONING_FORMAT,
203
+ )
204
+ when ::Anthropic::Models::RedactedThinkingBlock
205
+ Riffer::Messages::Assistant::ReasoningPart.new(type: :encrypted, data: block.data, format: REASONING_FORMAT)
206
+ end
207
+ end
208
+
202
209
  #--
203
210
  #: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
204
211
  def execute_stream(params, yielder)
205
212
  current_state = {
206
213
  text: nil,
207
- reasoning: nil,
208
214
  tool_call: nil,
209
215
  web_search_index: nil,
210
216
  web_search_json: nil,
@@ -230,19 +236,15 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
230
236
  when ::Anthropic::Helpers::Streaming::TextEvent
231
237
  handle_text_event(event, state: current_state, yielder: yielder)
232
238
  when ::Anthropic::Helpers::Streaming::ThinkingEvent
233
- handle_thinking_event(event, state: current_state, yielder: yielder)
239
+ yielder << Riffer::StreamEvents::ReasoningDelta.new(event.thinking)
234
240
  when ::Anthropic::Helpers::Streaming::InputJsonEvent
235
241
  handle_input_json_event(event, state: current_state, yielder: yielder)
236
242
  when ::Anthropic::Helpers::Streaming::ContentBlockStopEvent
237
243
  case event.content_block
238
- when ::Anthropic::Models::TextBlock
239
- handle_content_block_stop_text(event, state: current_state, yielder: yielder) if current_state[:text]
240
244
  when ::Anthropic::Models::ToolUseBlock
241
245
  handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder)
242
- when ::Anthropic::Models::ThinkingBlock
243
- if current_state[:reasoning]
244
- handle_content_block_stop_thinking(event, state: current_state, yielder: yielder)
245
- end
246
+ when ::Anthropic::Models::ThinkingBlock, ::Anthropic::Models::RedactedThinkingBlock
247
+ handle_content_block_stop_reasoning(event, yielder: yielder)
246
248
  when ::Anthropic::Models::ServerToolUseBlock
247
249
  handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder)
248
250
  when ::Anthropic::Models::WebSearchToolResultBlock
@@ -250,7 +252,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
250
252
  end
251
253
  when ::Anthropic::Helpers::Streaming::MessageStopEvent
252
254
  stream_completed = true
253
- handle_message_stop(event, accumulated_message: stream.accumulated_message, yielder: yielder)
255
+ handle_message_stop(
256
+ event, state: current_state, accumulated_message: stream.accumulated_message, yielder: yielder,
257
+ )
254
258
  end
255
259
  end
256
260
  ensure
@@ -287,24 +291,13 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
287
291
  #--
288
292
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
289
293
  def handle_text_event(event, state:, yielder:)
290
- # Mutating append 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.
294
+ # Mutating append avoids O(n^2) copying across the response; the buffer
295
+ # reaches TextDone only at message stop, so no reader sees it mid-append.
295
296
  state[:text] ||= +""
296
297
  state[:text] << event.text
297
298
  yielder << Riffer::StreamEvents::TextDelta.new(event.text)
298
299
  end
299
300
 
300
- #--
301
- #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
302
- def handle_thinking_event(event, state:, yielder:)
303
- state[:reasoning] ||= +""
304
- state[:reasoning] << event.thinking
305
- yielder << Riffer::StreamEvents::ReasoningDelta.new(event.thinking)
306
- end
307
-
308
301
  #--
309
302
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
310
303
  def handle_input_json_event(event, state:, yielder:)
@@ -335,18 +328,13 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
335
328
  state[:tool_call] = nil
336
329
  end
337
330
 
331
+ # The SDK's accumulated block already carries the full thinking text and the
332
+ # signature from its trailing signature_delta.
338
333
  #--
339
- #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
340
- def handle_content_block_stop_thinking(_event, state:, yielder:)
341
- yield_reasoning_done(yielder, state[:reasoning])
342
- state[:reasoning] = nil
343
- end
344
-
345
- #--
346
- #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
347
- def handle_content_block_stop_text(_event, state:, yielder:)
348
- yielder << Riffer::StreamEvents::TextDone.new(state[:text])
349
- state[:text] = nil
334
+ #: (untyped, yielder: Riffer::Providers::_EventSink) -> void
335
+ def handle_content_block_stop_reasoning(event, yielder:)
336
+ part = build_reasoning_part(event.content_block) #: Riffer::Messages::Assistant::ReasoningPart
337
+ yielder << Riffer::StreamEvents::ReasoningDone.new(part)
350
338
  end
351
339
 
352
340
  #--
@@ -376,8 +364,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
376
364
  end
377
365
 
378
366
  #--
379
- #: (untyped, accumulated_message: untyped, yielder: Riffer::Providers::_EventSink) -> void
380
- def handle_message_stop(_event, accumulated_message:, yielder:)
367
+ #: (untyped, state: Hash[Symbol, untyped], accumulated_message: untyped, yielder: Riffer::Providers::_EventSink) -> void
368
+ def handle_message_stop(_event, state:, accumulated_message:, yielder:)
369
+ yielder << Riffer::StreamEvents::TextDone.new(state[:text]) if state[:text]
370
+
381
371
  message = accumulated_message #: Anthropic::Models::Message?
382
372
  yield_finish_reason(yielder, build_finish_reason(message&.stop_reason))
383
373
 
@@ -428,7 +418,9 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
428
418
  #--
429
419
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
430
420
  def convert_assistant_to_anthropic_format(message)
431
- content = [] #: Array[Hash[Symbol, untyped]]
421
+ content = message.reasoning.filter_map do |part|
422
+ convert_reasoning_part_to_anthropic_format(part) if part.format == REASONING_FORMAT
423
+ end
432
424
  content << { type: "text", text: message.content } if message.content && !message.content.empty?
433
425
 
434
426
  message.tool_calls.each do |tc|
@@ -443,6 +435,14 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
443
435
  { role: "assistant", content: content }
444
436
  end
445
437
 
438
+ #--
439
+ #: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
440
+ def convert_reasoning_part_to_anthropic_format(part)
441
+ return { type: "redacted_thinking", data: part.data } if part.type == :encrypted
442
+
443
+ { type: "thinking", thinking: part.text, signature: part.signature }
444
+ end
445
+
446
446
  #--
447
447
  #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
448
448
  def convert_file_part_to_anthropic_format(file)