riffer 0.47.2 → 0.49.0

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