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
@@ -5,18 +5,8 @@ require "json"
5
5
  require "net/http"
6
6
  require "uri"
7
7
 
8
- # HTTP transport for the Gemini REST API. Riffer builds one from the
9
- # configured +api_key+ by default; construct your own to tune the HTTP knobs
10
- # and assign it to <tt>Riffer.config.gemini.client</tt>. Any object
11
- # implementing +post+ and +post_stream+ with these contracts works there —
12
- # the class is a default implementation, not a required base.
13
- #
14
- # Riffer.configure do |config|
15
- # config.gemini.client = Riffer::Providers::Gemini::Client.new(
16
- # api_key: ENV["GEMINI_API_KEY"],
17
- # read_timeout: 120
18
- # )
19
- # end
8
+ # <tt>Riffer.config.gemini.client</tt> accepts any object implementing +post+ and +post_stream+
9
+ # with these contracts; this class is the default, not a required base.
20
10
  class Riffer::Providers::Gemini::Client
21
11
  # @rbs @api_key: String?
22
12
  # @rbs @base_url: String
@@ -43,8 +33,7 @@ class Riffer::Providers::Gemini::Client
43
33
  @proxy_port = proxy_port
44
34
  end
45
35
 
46
- # POSTs a JSON body to an API path and returns the parsed response hash.
47
- # Raises Riffer::Error when the API responds with a non-success status.
36
+ # Raises Riffer::Error on a non-success status.
48
37
  #--
49
38
  #: (String, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
50
39
  def post(path, body)
@@ -54,9 +43,7 @@ class Riffer::Providers::Gemini::Client
54
43
  JSON.parse(response.body, symbolize_names: true)
55
44
  end
56
45
 
57
- # POSTs a JSON body to an API path, yielding raw response body chunks as
58
- # they arrive. Raises Riffer::Error when the API responds with a
59
- # non-success status.
46
+ # Raises Riffer::Error on a non-success status.
60
47
  #--
61
48
  #: (String, Hash[Symbol, untyped]) { (String) -> void } -> void
62
49
  def post_stream(path, body, &block)
@@ -4,7 +4,6 @@
4
4
  require "json"
5
5
  require "securerandom"
6
6
 
7
- # Google Gemini provider for Gemini models via the Gemini REST API.
8
7
  class Riffer::Providers::Gemini < Riffer::Providers::Base
9
8
  VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
10
9
 
@@ -29,7 +28,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
29
28
  "FINISH_REASON_UNSPECIFIED" => :other,
30
29
  }.freeze #: Hash[String, Symbol]
31
30
 
32
- # The GenAI semconv well-known provider name.
33
31
  #--
34
32
  #: () -> String
35
33
  def self.semconv_provider_name
@@ -37,7 +35,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
37
35
  end
38
36
 
39
37
  #--
40
- #: (Riffer::Messages::FilePart) -> Symbol
38
+ #: (Riffer::Messages::User::FilePart) -> Symbol
41
39
  def file_delivery(_file)
42
40
  :base64
43
41
  end
@@ -76,10 +74,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
76
74
  }]
77
75
  end
78
76
 
79
- # tags propagate to observability only: the Gemini Developer API has no
80
- # request labels field (unknown body fields are rejected), so :tags is
81
- # stripped here rather than mapped. Native labels would arrive with a Vertex
82
- # adapter. See docs/CONFIGURATION.md.
77
+ # The Gemini Developer API has no request labels field and rejects unknown
78
+ # body fields, so :tags reach observability only.
83
79
  generation_config = options.except(:tools, :structured_output, :tags)
84
80
 
85
81
  if structured_output
@@ -144,8 +140,6 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
144
140
  build_finish_reason(response.dig(:candidates, 0, :finishReason), tool_calls: has_function_call)
145
141
  end
146
142
 
147
- # Gemini reports STOP even when the candidate carries functionCall parts,
148
- # so tool-call presence overrides the raw value.
149
143
  #--
150
144
  #: (String?, tool_calls: bool) -> Riffer::Providers::FinishReason?
151
145
  def build_finish_reason(raw_reason, tool_calls:)
@@ -153,18 +147,18 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
153
147
 
154
148
  raw = raw_reason.to_s
155
149
  reason = FINISH_REASONS.fetch(raw, :other)
150
+ # Gemini reports STOP even when the candidate carries functionCall parts.
156
151
  reason = :tool_calls if reason == :stop && tool_calls
157
152
  Riffer::Providers::FinishReason.new(reason: reason, raw: raw)
158
153
  end
159
154
 
160
- # Gemini reports thinking tokens outside +candidatesTokenCount+;
161
- # TokenUsage's output includes them.
162
155
  #--
163
156
  #: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage
164
157
  def build_token_usage(usage)
165
158
  apply_pricing(
166
159
  Riffer::Providers::TokenUsage.new(
167
160
  input_tokens: usage[:promptTokenCount] || 0,
161
+ # Gemini reports thinking tokens outside candidatesTokenCount.
168
162
  output_tokens: (usage[:candidatesTokenCount] || 0) + (usage[:thoughtsTokenCount] || 0),
169
163
  cache_read_tokens: usage[:cachedContentTokenCount],
170
164
  ),
@@ -287,7 +281,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
287
281
  end
288
282
 
289
283
  #--
290
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
284
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
291
285
  def convert_file_part_to_gemini_format(file)
292
286
  { inlineData: { mimeType: file.media_type, data: file.data } }
293
287
  end
@@ -1,41 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Mock provider for mocking LLM responses in tests; no external gems required.
5
4
  class Riffer::Providers::Mock < Riffer::Providers::Base
6
5
  # @rbs @responses: Array[Hash[Symbol, untyped]]
7
6
  # @rbs @current_index: Integer
8
7
  # @rbs @stubbed_responses: Array[Hash[Symbol, untyped]]
9
8
 
10
- # Returns the skill adapter for the mock model — XML when the model name
11
- # contains +claude+ (mirroring a real Claude provider), else Markdown.
12
9
  #--
13
10
  #: (?String?) -> singleton(Riffer::Skills::Adapter)
14
11
  def self.skills_adapter(model = nil)
12
+ # Mirrors the adapter a real Claude provider picks.
15
13
  return Riffer::Skills::XmlAdapter if model&.include?("claude")
16
14
 
17
15
  Riffer::Skills::MarkdownAdapter
18
16
  end
19
17
 
20
- # The GenAI semconv well-known provider name.
21
18
  #--
22
19
  #: () -> String
23
20
  def self.semconv_provider_name
24
21
  "mock"
25
22
  end
26
23
 
27
- # Array of recorded method calls for assertions.
28
24
  attr_reader :calls #: Array[Hash[Symbol, untyped]] # @dynamic calls
29
25
 
30
- # +responses:+ pre-configures canned responses (same shape as
31
- # +#stub_response+) for standalone use; agent tests queue responses on
32
- # <tt>agent.provider</tt> via +#stub_response+ instead.
33
- #
34
- # Riffer::Providers::Mock.new(responses: [
35
- # {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
36
- # {content: "Final answer"}
37
- # ])
38
- #
39
26
  #--
40
27
  #: (?responses: Array[Hash[Symbol, untyped]]) -> void
41
28
  def initialize(responses: [])
@@ -46,28 +33,18 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
46
33
  @stubbed_responses = []
47
34
  end
48
35
 
49
- # Stubs the next response; call repeatedly to queue several. +finish_reason+
50
- # defaults to +:tool_calls+ when tool calls are present, else +:stop+.
51
- #
52
- # provider.stub_response("Hello")
53
- # provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
54
- # provider.stub_response("Final response",
55
- # token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
56
- # provider.stub_response("Truncated...", finish_reason: :length)
57
- #
58
36
  #--
59
- #: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?) -> void
60
- def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil)
37
+ #: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?, ?reasoning: Array[Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart]) -> void
38
+ def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
61
39
  @stubbed_responses << normalize_response(
62
40
  content: content,
63
41
  tool_calls: tool_calls,
64
42
  token_usage: token_usage,
65
43
  finish_reason: finish_reason,
44
+ reasoning: reasoning,
66
45
  )
67
46
  end
68
47
 
69
- # Clears all stubbed responses.
70
- #
71
48
  #--
72
49
  #: () -> void
73
50
  def clear_stubs
@@ -92,6 +69,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
92
69
  role: response[:role] || "assistant",
93
70
  content: response[:content] || "",
94
71
  tool_calls: formatted_tool_calls,
72
+ reasoning: (response[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
95
73
  token_usage: response[:token_usage],
96
74
  finish_reason: response[:finish_reason] || (formatted_tool_calls.empty? ? :stop : :tool_calls),
97
75
  }
@@ -129,6 +107,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
129
107
  reason ? Riffer::Providers::FinishReason.new(reason: reason) : nil
130
108
  end
131
109
 
110
+ #--
111
+ #: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
112
+ def extract_reasoning(response)
113
+ response[:reasoning] || []
114
+ end
115
+
132
116
  #--
133
117
  #: (untyped) -> String
134
118
  def extract_content(response)
@@ -149,6 +133,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
149
133
  tool_calls = response[:tool_calls] || []
150
134
  token_usage = response[:token_usage]
151
135
  web_search = response[:web_search]
136
+ reasoning = response[:reasoning] || []
152
137
 
153
138
  if web_search
154
139
  yielder << Riffer::StreamEvents::WebSearchStatus.new("in_progress")
@@ -161,6 +146,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
161
146
  )
162
147
  end
163
148
 
149
+ reasoning.each do |part|
150
+ text = part.text
151
+ yielder << Riffer::StreamEvents::ReasoningDelta.new(text) if text
152
+ yielder << Riffer::StreamEvents::ReasoningDone.new(part)
153
+ end
154
+
164
155
  unless full_content.empty?
165
156
  content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
166
157
  content_parts.each do |part|
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # OpenAI provider for GPT models. Requires the +openai+ gem.
5
4
  class Riffer::Providers::OpenAI < Riffer::Providers::Base
6
5
  WEB_SEARCH_TOOL_TYPE = "web_search_preview" #: String
7
6
 
8
- # The Responses API has no finish_reason field. The response +status+ is
9
- # the primary signal; an +incomplete+ status is only meaningful together
10
- # with <tt>incomplete_details.reason</tt>, so that branch nests one level.
7
+ # The Responses API has no finish_reason field, and an +incomplete+ status is only meaningful
8
+ # together with <tt>incomplete_details.reason</tt>.
11
9
  FINISH_REASONS = {
12
10
  "completed" => :stop,
13
11
  "incomplete" => {
@@ -20,7 +18,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
20
18
  "queued" => :other,
21
19
  }.freeze #: Hash[String, Symbol | Hash[String, Symbol]]
22
20
 
23
- # The GenAI semconv well-known provider name.
24
21
  #--
25
22
  #: () -> String
26
23
  def self.semconv_provider_name
@@ -35,7 +32,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
35
32
  end
36
33
 
37
34
  #--
38
- #: (Riffer::Messages::FilePart) -> Symbol
35
+ #: (Riffer::Messages::User::FilePart) -> Symbol
39
36
  def file_delivery(file)
40
37
  file.image? ? :url : :base64
41
38
  end
@@ -48,12 +45,11 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
48
45
  Riffer.config.openai.client
49
46
  end
50
47
 
51
- # Compacted so an unset value stays absent: the SDK reads +OPENAI_API_KEY+ /
52
- # +OPENAI_BASE_URL+ only for a missing argument, and an explicit nil would
53
- # suppress that fallback.
54
48
  #--
55
49
  #: () -> untyped
56
50
  def build_client
51
+ # The SDK falls back to +OPENAI_API_KEY+ / +OPENAI_BASE_URL+ only for a missing argument; an
52
+ # explicit nil suppresses that fallback.
57
53
  ::OpenAI::Client.new(**{
58
54
  api_key: Riffer.config.openai.api_key,
59
55
  base_url: Riffer.config.openai.base_url,
@@ -80,9 +76,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
80
76
  } #: Hash[Symbol, untyped]
81
77
 
82
78
  unless tags.empty?
83
- params[:metadata] = tags
84
- # The reserved user_id also maps to the native safety identifier while
85
- # staying in metadata as an ordinary tag.
79
+ params[:metadata] = (params[:metadata] || {}).merge(tags)
86
80
  user_id = tags["user_id"]
87
81
  params[:safety_identifier] = user_id if user_id
88
82
  end
@@ -164,7 +158,6 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
164
158
  Riffer::Providers::FinishReason.new(reason: reason, raw: detail || status)
165
159
  end
166
160
 
167
- # The nested field that names the cause behind an ambiguous status.
168
161
  #--
169
162
  #: (untyped, String) -> String?
170
163
  def finish_detail(response, status)
@@ -254,9 +247,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
254
247
  end
255
248
  end
256
249
  ensure
257
- # OpenAI SDK does not auto-close the underlying HTTP stream when
258
- # iteration is interrupted (raise / fiber cancellation), so the SSE
259
- # socket leaks until GC. close is idempotent and a no-op after EOF.
250
+ # The SDK doesn't close the SSE socket when iteration is interrupted (raise / fiber
251
+ # cancellation), so it leaks until GC. close is idempotent and a no-op after EOF.
260
252
  stream.close
261
253
  end
262
254
 
@@ -296,7 +288,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
296
288
  #--
297
289
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
298
290
  def handle_reasoning_summary_text_done(event, state:, yielder:)
299
- yielder << Riffer::StreamEvents::ReasoningDone.new(event.text)
291
+ yield_reasoning_done(yielder, event.text)
300
292
  end
301
293
 
302
294
  #--
@@ -348,8 +340,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
348
340
  action = event.item.action
349
341
  case action
350
342
  when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
351
- # OpenPage carries a url but no query or sources, so it doesn't fit
352
- # WebSearchDone — emit as a status notification instead.
343
+ # OpenPage carries a url but no query or sources, so it doesn't fit WebSearchDone.
353
344
  yielder << Riffer::StreamEvents::WebSearchStatus.new("open_page", url: action.url)
354
345
  when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
355
346
  sources = (action.sources || []).map { |s| { title: nil, url: s.url } }
@@ -402,7 +393,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
402
393
  type: "function_call",
403
394
  call_id: tc.call_id,
404
395
  name: encode_tool_name(tc.name),
405
- arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json,
396
+ arguments: tc.arguments,
406
397
  }
407
398
  end
408
399
  items
@@ -410,7 +401,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
410
401
  end
411
402
 
412
403
  #--
413
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
404
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
414
405
  def convert_file_part_to_openai_format(file)
415
406
  if file.image?
416
407
  image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
@@ -3,10 +3,6 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # OpenRouter provider (https://openrouter.ai). Requires the +openai+ gem —
7
- # OpenRouter exposes an OpenAI-compatible endpoint, so this reuses the OpenAI
8
- # SDK with a +base_url+ override. +api_key+ resolves from config, then
9
- # +OPENROUTER_API_KEY+.
10
6
  class Riffer::Providers::OpenRouter < Riffer::Providers::Base
11
7
  BASE_URL = "https://openrouter.ai/api/v1" #: String
12
8
 
@@ -19,7 +15,27 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
19
15
  "error" => :error,
20
16
  }.freeze #: Hash[String, Symbol]
21
17
 
22
- # The GenAI semconv well-known provider name.
18
+ REASONING_TYPES = {
19
+ "reasoning.text" => :text,
20
+ "reasoning.summary" => :summary,
21
+ "reasoning.encrypted" => :encrypted,
22
+ }.freeze #: Hash[String, Symbol]
23
+
24
+ # The +reasoning_details+ formats OpenRouter documents. Only parts tagged with
25
+ # one of them are replayed, so parts from other adapters, or with no format,
26
+ # never reach the request.
27
+ REASONING_FORMATS = %w[
28
+ unknown
29
+ openai-responses-v1
30
+ azure-openai-responses-v1
31
+ bedrock-openai-responses-v1
32
+ bedrock-xai-responses-v1
33
+ xai-responses-v1
34
+ meta-responses-v1
35
+ anthropic-claude-v1
36
+ google-gemini-v1
37
+ ].freeze #: Array[String]
38
+
23
39
  #--
24
40
  #: () -> String
25
41
  def self.semconv_provider_name
@@ -34,7 +50,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
34
50
  end
35
51
 
36
52
  #--
37
- #: (Riffer::Messages::FilePart) -> Symbol
53
+ #: (Riffer::Messages::User::FilePart) -> Symbol
38
54
  def file_delivery(file)
39
55
  file.image? ? :url : :base64
40
56
  end
@@ -47,15 +63,12 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
47
63
  Riffer.config.openrouter.client
48
64
  end
49
65
 
50
- # Deliberately not compacted: this borrows the OpenAI SDK to talk to a
51
- # different vendor, so omitting an unset +api_key+ would let the SDK fall
52
- # back to +OPENAI_API_KEY+ and send an OpenAI credential to OpenRouter.
53
- # Passing nil raises in the SDK instead. +OPENROUTER_API_KEY+ is read here
54
- # rather than left to the SDK for the same reason.
55
66
  #--
56
67
  #: () -> untyped
57
68
  def build_client
58
69
  api_key = Riffer.config.openrouter.api_key || ENV.fetch("OPENROUTER_API_KEY", nil)
70
+ # Pass a nil api_key rather than omitting it: an omitted key lets the SDK
71
+ # fall back to OPENAI_API_KEY and send an OpenAI credential to OpenRouter.
59
72
  ::OpenAI::Client.new(api_key: api_key, base_url: BASE_URL)
60
73
  end
61
74
 
@@ -74,7 +87,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
74
87
  } #: Hash[Symbol, untyped]
75
88
 
76
89
  unless tags.empty?
77
- params[:metadata] = tags
90
+ params[:metadata] = (params[:metadata] || {}).merge(tags)
78
91
  # OpenRouter exposes the legacy Chat Completions user field rather than
79
92
  # safety_identifier; the reserved user_id maps there and stays in metadata.
80
93
  user = tags["user_id"]
@@ -137,8 +150,6 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
137
150
  build_finish_reason(choice&.finish_reason, native: native_finish_reason(choice))
138
151
  end
139
152
 
140
- # +native+ is the upstream model's own finish reason, which OpenRouter
141
- # reports alongside its normalized one; it wins as +raw+ when present.
142
153
  #--
143
154
  #: (untyped, ?native: untyped) -> Riffer::Providers::FinishReason?
144
155
  def build_finish_reason(finish_reason, native: nil)
@@ -147,15 +158,17 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
147
158
  normalized = finish_reason.to_s
148
159
  return nil if normalized.empty?
149
160
 
161
+ # OpenRouter reports the upstream model's own finish reason alongside its
162
+ # normalized one.
150
163
  raw = native.to_s.empty? ? normalized : native.to_s
151
164
  Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(normalized, :other), raw: raw)
152
165
  end
153
166
 
154
- # +native_finish_reason+ is outside the OpenAI schema, so it is only
155
- # reachable through the SDK model's raw data hash.
156
167
  #--
157
168
  #: (untyped) -> untyped
158
169
  def native_finish_reason(choice)
170
+ # Outside the OpenAI schema, so only reachable through the SDK model's raw
171
+ # data hash.
159
172
  choice && choice.to_h[:native_finish_reason]
160
173
  end
161
174
 
@@ -187,6 +200,39 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
187
200
  end
188
201
  end
189
202
 
203
+ #--
204
+ #: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
205
+ def extract_reasoning(response)
206
+ typed_response = response #: OpenAI::Models::Chat::ChatCompletion
207
+ message = typed_response.choices.first&.message
208
+ details = message && reasoning_details(message)
209
+ (details || []).filter_map { |detail| build_reasoning_part(detail) }
210
+ end
211
+
212
+ # The openai gem's typed models strip fields outside OpenAI's spec, so
213
+ # +reasoning_details+ is only reachable through the model's raw data hash.
214
+ #--
215
+ #: (untyped) -> Array[Hash[Symbol, untyped]]?
216
+ def reasoning_details(model)
217
+ model[:reasoning_details]
218
+ end
219
+
220
+ #--
221
+ #: (Hash[Symbol, untyped]) -> Riffer::Messages::Assistant::ReasoningPart?
222
+ def build_reasoning_part(detail)
223
+ type = REASONING_TYPES.fetch(detail[:type], nil)
224
+ return nil unless type
225
+
226
+ Riffer::Messages::Assistant::ReasoningPart.new(
227
+ type: type,
228
+ text: detail[:text] || detail[:summary],
229
+ data: detail[:data],
230
+ signature: detail[:signature],
231
+ id: detail[:id],
232
+ format: detail[:format],
233
+ )
234
+ end
235
+
190
236
  #--
191
237
  #: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
192
238
  def execute_stream(params, yielder)
@@ -196,17 +242,14 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
196
242
 
197
243
  state = {
198
244
  text: +"",
199
- reasoning: +"",
245
+ reasoning_details: {},
200
246
  tool_calls: {},
201
247
  finish_reason: nil,
202
248
  native_finish_reason: nil,
203
249
  } #: Hash[Symbol, untyped]
204
250
 
205
- # Use stream_raw (not stream) — the latter yields a higher-level
206
- # ChatChunkEvent helper that aggregates content/tool calls into typed
207
- # events. We want raw ChatCompletionChunk objects with
208
- # +choices.first.delta+ so we can map deltas to Riffer::StreamEvents
209
- # ourselves.
251
+ # stream_raw, not stream: stream aggregates chunks into higher-level
252
+ # events, but mapping to Riffer::StreamEvents needs the raw deltas.
210
253
  stream = client.chat.completions.stream_raw(**stream_params)
211
254
  begin
212
255
  stream.each do |chunk|
@@ -223,7 +266,10 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
223
266
  emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
224
267
 
225
268
  yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
226
- yielder << Riffer::StreamEvents::ReasoningDone.new(state[:reasoning]) unless state[:reasoning].empty?
269
+ state[:reasoning_details].each_value do |detail|
270
+ part = build_reasoning_part(detail)
271
+ yielder << Riffer::StreamEvents::ReasoningDone.new(part) if part
272
+ end
227
273
  yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
228
274
  end
229
275
 
@@ -264,14 +310,26 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
264
310
  #--
265
311
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
266
312
  def handle_reasoning_delta(delta, state:, yielder:)
267
- # The openai gem's typed Delta model strips fields not in OpenAI's spec
268
- # (so +delta.reasoning+ raises NoMethodError), but the underlying data
269
- # hash retains them. Access via +#[]+ which reads from BaseModel#@data.
270
- reasoning = delta[:reasoning] if delta.respond_to?(:[])
271
- return if reasoning.nil? || reasoning.empty?
272
-
273
- state[:reasoning] << reasoning
274
- yielder << Riffer::StreamEvents::ReasoningDelta.new(reasoning)
313
+ details = reasoning_details(delta)
314
+ return if details.nil?
315
+
316
+ details.each do |detail|
317
+ accumulate_reasoning_detail(state[:reasoning_details], detail)
318
+ text = detail[:text] || detail[:summary]
319
+ yielder << Riffer::StreamEvents::ReasoningDelta.new(text) unless text.nil? || text.empty?
320
+ end
321
+ end
322
+
323
+ # OpenRouter streams one reasoning block as many fragments sharing an
324
+ # +index+: prose and payload arrive in pieces to concatenate, while the
325
+ # signature, id, and format arrive once, often on the last fragment. Keying
326
+ # on +type+ as well keeps index-less fragments of different kinds apart.
327
+ #--
328
+ #: (Hash[untyped, Hash[Symbol, untyped]], Hash[Symbol, untyped]) -> void
329
+ def accumulate_reasoning_detail(accumulated, detail)
330
+ entry = accumulated[[detail[:index], detail[:type]]] ||= { type: detail[:type] }
331
+ %i[text summary data].each { |key| (entry[key] ||= +"") << detail[key] if detail[key] }
332
+ %i[signature id format].each { |key| entry[key] = detail[key] if detail[key] }
275
333
  end
276
334
 
277
335
  #--
@@ -362,17 +420,35 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
362
420
  type: "function",
363
421
  function: {
364
422
  name: encode_tool_name(tc.name),
365
- arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json,
423
+ arguments: tc.arguments,
366
424
  },
367
425
  }
368
426
  end
369
427
  end
370
428
 
429
+ details = message.reasoning.filter_map do |part|
430
+ convert_reasoning_part_to_detail(part) if REASONING_FORMATS.include?(part.format)
431
+ end
432
+ msg[:reasoning_details] = details unless details.empty?
433
+
371
434
  msg
372
435
  end
373
436
 
374
437
  #--
375
- #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
438
+ #: (Riffer::Messages::Assistant::ReasoningPart) -> Hash[Symbol, untyped]
439
+ def convert_reasoning_part_to_detail(part)
440
+ {
441
+ type: REASONING_TYPES.key(part.type),
442
+ (part.type == :summary ? :summary : :text) => part.text,
443
+ data: part.data,
444
+ signature: part.signature,
445
+ id: part.id,
446
+ format: part.format,
447
+ }.compact
448
+ end
449
+
450
+ #--
451
+ #: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
376
452
  def convert_file_part_to_chat_completions_format(file)
377
453
  if file.image?
378
454
  image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Resolves provider classes by identifier, combining the built-in REPO with
5
- # consumer registrations added through +register+. Registration is not
6
- # synchronized — register during boot, before concurrent generation begins.
7
4
  module Riffer::Providers::Repository
8
5
  extend self
9
6
 
@@ -22,11 +19,8 @@ module Riffer::Providers::Repository
22
19
 
23
20
  @registrations = {} #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
24
21
 
25
- # Registers a custom provider under +identifier+, resolved lazily by the block.
26
- # Takes precedence over a built-in sharing the identifier.
27
- #
28
- # Riffer::Providers::Repository.register(:jane) { MyApp::JaneProvider }
29
- #
22
+ # Not synchronized — register during boot, before concurrent generation
23
+ # begins.
30
24
  #--
31
25
  #: ((String | Symbol)) { () -> singleton(Riffer::Providers::Base) } -> void
32
26
  def register(identifier, &factory)
@@ -34,8 +28,6 @@ module Riffer::Providers::Repository
34
28
  @key_for = nil
35
29
  end
36
30
 
37
- # Removes a custom registration by identifier, leaving any built-in of the
38
- # same name intact.
39
31
  #--
40
32
  #: ((String | Symbol)) -> void
41
33
  def unregister(identifier)
@@ -43,9 +35,6 @@ module Riffer::Providers::Repository
43
35
  @key_for = nil
44
36
  end
45
37
 
46
- # Finds a provider class by identifier, preferring a custom registration over
47
- # a built-in of the same name.
48
- #
49
38
  #--
50
39
  #: ((String | Symbol)) -> singleton(Riffer::Providers::Base)?
51
40
  def find(identifier)
@@ -53,7 +42,6 @@ module Riffer::Providers::Repository
53
42
  (@registrations[key] || REPO[key])&.call
54
43
  end
55
44
 
56
- # Returns the registry identifier for a provider class, or nil when unregistered.
57
45
  #--
58
46
  #: (singleton(Riffer::Providers::Base)) -> Symbol?
59
47
  def key_for(provider_class)