ruby_llm 2.0.0 → 2.1.1

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 (287) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -8
  3. data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
  5. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
  6. data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
  7. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
  8. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
  9. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
  10. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
  11. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
  13. data/lib/ruby_llm/accounting/usage.rb +86 -49
  14. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  15. data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
  16. data/lib/ruby_llm/active_record/batch.rb +1 -1
  17. data/lib/ruby_llm/active_record/chat_methods.rb +143 -43
  18. data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
  19. data/lib/ruby_llm/active_record/message_methods.rb +32 -7
  20. data/lib/ruby_llm/active_record/model.rb +1 -1
  21. data/lib/ruby_llm/active_record/provider_file.rb +64 -0
  22. data/lib/ruby_llm/active_record/record.rb +19 -0
  23. data/lib/ruby_llm/active_record/tool_call.rb +1 -1
  24. data/lib/ruby_llm/active_record/usage.rb +53 -3
  25. data/lib/ruby_llm/agent.rb +198 -33
  26. data/lib/ruby_llm/aliases.json +66 -8
  27. data/lib/ruby_llm/attachment.rb +54 -10
  28. data/lib/ruby_llm/batch.rb +26 -3
  29. data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
  30. data/lib/ruby_llm/chat/tool_search.rb +26 -0
  31. data/lib/ruby_llm/chat.rb +492 -82
  32. data/lib/ruby_llm/choice.rb +38 -0
  33. data/lib/ruby_llm/configuration.rb +36 -11
  34. data/lib/ruby_llm/context.rb +45 -32
  35. data/lib/ruby_llm/cost.rb +42 -1
  36. data/lib/ruby_llm/embedding.rb +9 -2
  37. data/lib/ruby_llm/error.rb +34 -2
  38. data/lib/ruby_llm/evaluation/assertions.rb +40 -0
  39. data/lib/ruby_llm/evaluation/case.rb +52 -0
  40. data/lib/ruby_llm/evaluation/dataset.rb +65 -0
  41. data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
  42. data/lib/ruby_llm/evaluation/evidence.rb +125 -0
  43. data/lib/ruby_llm/evaluation/minitest.rb +26 -0
  44. data/lib/ruby_llm/evaluation/report.rb +103 -0
  45. data/lib/ruby_llm/evaluation/result.rb +75 -0
  46. data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
  47. data/lib/ruby_llm/evaluation/rspec.rb +23 -0
  48. data/lib/ruby_llm/evaluation/runner.rb +93 -0
  49. data/lib/ruby_llm/evaluation/trial.rb +101 -0
  50. data/lib/ruby_llm/evaluation.rb +289 -0
  51. data/lib/ruby_llm/image.rb +12 -4
  52. data/lib/ruby_llm/judge/builder.rb +51 -0
  53. data/lib/ruby_llm/judge/data.rb +43 -0
  54. data/lib/ruby_llm/judge/question.rb +101 -0
  55. data/lib/ruby_llm/judge.rb +179 -0
  56. data/lib/ruby_llm/judgment.rb +125 -0
  57. data/lib/ruby_llm/mcp/apps.rb +25 -0
  58. data/lib/ruby_llm/mcp/client.rb +226 -0
  59. data/lib/ruby_llm/mcp/collection.rb +56 -0
  60. data/lib/ruby_llm/mcp/content.rb +44 -0
  61. data/lib/ruby_llm/mcp/error.rb +27 -0
  62. data/lib/ruby_llm/mcp/http.rb +379 -0
  63. data/lib/ruby_llm/mcp/input_request.rb +118 -0
  64. data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
  65. data/lib/ruby_llm/mcp/listener.rb +168 -0
  66. data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
  67. data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
  68. data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
  69. data/lib/ruby_llm/mcp/oauth.rb +653 -0
  70. data/lib/ruby_llm/mcp/param_headers.rb +83 -0
  71. data/lib/ruby_llm/mcp/prompt.rb +71 -0
  72. data/lib/ruby_llm/mcp/resource.rb +84 -0
  73. data/lib/ruby_llm/mcp/resource_template.rb +99 -0
  74. data/lib/ruby_llm/mcp/result.rb +76 -0
  75. data/lib/ruby_llm/mcp/session_expired.rb +10 -0
  76. data/lib/ruby_llm/mcp/stdio.rb +250 -0
  77. data/lib/ruby_llm/mcp/task.rb +151 -0
  78. data/lib/ruby_llm/mcp/tool.rb +130 -0
  79. data/lib/ruby_llm/mcp.rb +1101 -0
  80. data/lib/ruby_llm/message.rb +41 -6
  81. data/lib/ruby_llm/model.rb +2 -1
  82. data/lib/ruby_llm/models/aliases.rb +5 -2
  83. data/lib/ruby_llm/models/lookup.rb +80 -0
  84. data/lib/ruby_llm/models/registry.rb +2 -2
  85. data/lib/ruby_llm/models/schema.rb +1 -1
  86. data/lib/ruby_llm/models.json +23082 -10158
  87. data/lib/ruby_llm/models.rb +20 -69
  88. data/lib/ruby_llm/moderation.rb +7 -2
  89. data/lib/ruby_llm/ocr.rb +7 -2
  90. data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
  91. data/lib/ruby_llm/open_telemetry.rb +82 -0
  92. data/lib/ruby_llm/probability.rb +30 -0
  93. data/lib/ruby_llm/progress.rb +44 -0
  94. data/lib/ruby_llm/prompt/context.rb +55 -0
  95. data/lib/ruby_llm/prompt.rb +1 -1
  96. data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
  97. data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
  98. data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
  99. data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
  100. data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
  101. data/lib/ruby_llm/protocol/streaming.rb +38 -31
  102. data/lib/ruby_llm/protocol.rb +196 -40
  103. data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
  104. data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
  105. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
  106. data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
  107. data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
  108. data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
  109. data/lib/ruby_llm/protocols/anthropic.rb +20 -1
  110. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
  111. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
  112. data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
  113. data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
  114. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +21 -0
  115. data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
  116. data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
  117. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
  118. data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
  119. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
  120. data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
  121. data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
  122. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
  123. data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
  124. data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
  125. data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
  126. data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
  127. data/lib/ruby_llm/protocols/cohere.rb +1 -0
  128. data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
  129. data/lib/ruby_llm/protocols/converse/media.rb +16 -2
  130. data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
  131. data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
  132. data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
  133. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
  134. data/lib/ruby_llm/protocols/converse.rb +3 -5
  135. data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
  136. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
  137. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
  138. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
  139. data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
  140. data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
  141. data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
  142. data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
  143. data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
  144. data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
  145. data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
  146. data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
  147. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
  148. data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
  149. data/lib/ruby_llm/protocols/gemini.rb +1 -0
  150. data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
  151. data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
  152. data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
  153. data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
  154. data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
  155. data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
  156. data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
  157. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  158. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
  159. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
  160. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
  161. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
  162. data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
  163. data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
  164. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
  165. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
  166. data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
  167. data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
  168. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
  169. data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
  170. data/lib/ruby_llm/protocols/openai/files.rb +0 -7
  171. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
  172. data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
  173. data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
  174. data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
  175. data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
  176. data/lib/ruby_llm/protocols/responses/media.rb +11 -7
  177. data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
  178. data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
  179. data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
  180. data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
  181. data/lib/ruby_llm/protocols/responses.rb +1 -0
  182. data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
  183. data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
  184. data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
  185. data/lib/ruby_llm/protocols/system_one.rb +27 -0
  186. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
  187. data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
  188. data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
  189. data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
  190. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
  191. data/lib/ruby_llm/provider.rb +87 -15
  192. data/lib/ruby_llm/providers/anthropic.rb +23 -0
  193. data/lib/ruby_llm/providers/azure/chat.rb +4 -0
  194. data/lib/ruby_llm/providers/azure/images.rb +2 -2
  195. data/lib/ruby_llm/providers/azure/media.rb +8 -3
  196. data/lib/ruby_llm/providers/azure/videos.rb +7 -2
  197. data/lib/ruby_llm/providers/azure.rb +24 -4
  198. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
  199. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
  200. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
  201. data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
  202. data/lib/ruby_llm/providers/bedrock/models.rb +1 -2
  203. data/lib/ruby_llm/providers/bedrock.rb +38 -6
  204. data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
  205. data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
  206. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  207. data/lib/ruby_llm/providers/gemini.rb +4 -0
  208. data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
  209. data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
  210. data/lib/ruby_llm/providers/hetzner.rb +41 -0
  211. data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
  212. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +17 -3
  213. data/lib/ruby_llm/providers/mistral/media.rb +8 -4
  214. data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
  215. data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
  216. data/lib/ruby_llm/providers/ollama/media.rb +6 -2
  217. data/lib/ruby_llm/providers/ollama/models.rb +2 -0
  218. data/lib/ruby_llm/providers/ollama.rb +11 -0
  219. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
  220. data/lib/ruby_llm/providers/openai.rb +14 -0
  221. data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
  222. data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
  223. data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
  224. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
  225. data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
  226. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  227. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  228. data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
  229. data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
  230. data/lib/ruby_llm/providers/perplexity.rb +11 -0
  231. data/lib/ruby_llm/providers/typesafe.rb +32 -0
  232. data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
  233. data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
  234. data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
  235. data/lib/ruby_llm/providers/vertexai.rb +27 -28
  236. data/lib/ruby_llm/providers/xai/chat.rb +6 -0
  237. data/lib/ruby_llm/providers/xai/images.rb +9 -10
  238. data/lib/ruby_llm/providers/xai/responses.rb +10 -8
  239. data/lib/ruby_llm/providers/xai/videos.rb +11 -9
  240. data/lib/ruby_llm/providers/xai.rb +4 -0
  241. data/lib/ruby_llm/railtie.rb +11 -0
  242. data/lib/ruby_llm/request_shape/part.rb +117 -0
  243. data/lib/ruby_llm/request_shape/problem.rb +63 -0
  244. data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
  245. data/lib/ruby_llm/request_shape/turn.rb +54 -0
  246. data/lib/ruby_llm/request_shape.rb +160 -0
  247. data/lib/ruby_llm/rerank.rb +9 -2
  248. data/lib/ruby_llm/research_job.rb +21 -5
  249. data/lib/ruby_llm/score.rb +42 -0
  250. data/lib/ruby_llm/search_results.rb +2 -2
  251. data/lib/ruby_llm/server_tool_call.rb +10 -2
  252. data/lib/ruby_llm/speech.rb +8 -1
  253. data/lib/ruby_llm/support/cancellation.rb +39 -0
  254. data/lib/ruby_llm/support/inspectable.rb +3 -2
  255. data/lib/ruby_llm/support/instrumentation.rb +38 -1
  256. data/lib/ruby_llm/support/process_cache.rb +60 -0
  257. data/lib/ruby_llm/support/progress_reporter.rb +33 -0
  258. data/lib/ruby_llm/thinking.rb +2 -1
  259. data/lib/ruby_llm/tokens.rb +57 -14
  260. data/lib/ruby_llm/tool.rb +50 -3
  261. data/lib/ruby_llm/tool_call.rb +17 -2
  262. data/lib/ruby_llm/transcription.rb +10 -3
  263. data/lib/ruby_llm/transport/connection.rb +127 -44
  264. data/lib/ruby_llm/transport/error_middleware.rb +90 -24
  265. data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
  266. data/lib/ruby_llm/transport/json_response.rb +33 -0
  267. data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
  268. data/lib/ruby_llm/version.rb +1 -1
  269. data/lib/ruby_llm/video.rb +5 -3
  270. data/lib/ruby_llm/video_job.rb +48 -5
  271. data/lib/ruby_llm.rb +106 -5
  272. data/lib/tasks/ruby_llm/evaluations.rb +43 -0
  273. data/lib/tasks/ruby_llm.rake +6 -9
  274. data/skills/rubyllm/SKILL.md +33 -12
  275. metadata +101 -37
  276. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
  277. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
  278. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
  279. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
  280. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
  281. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
  282. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
  283. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
  284. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
  285. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
  286. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
  287. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Defines reusable probability, choice, and score questions over application data.
5
+ #
6
+ # class TicketTriage < RubyLLM::Judge
7
+ # probability :urgent, "Does this need attention today?"
8
+ # choice :department, "Which team should handle this?" do
9
+ # billing "Payments and refunds"
10
+ # technical "Bugs and integrations"
11
+ # end
12
+ # end
13
+ #
14
+ # TicketTriage.judge("Please refund my duplicate charge today.").urgent.probability
15
+ #
16
+ # Uses Configuration#default_judgment_model unless a model is declared or
17
+ # supplied for the call. An isolated context supplies its own default.
18
+ #
19
+ # Blocks and procs resolve once per judgment, with declared inputs available
20
+ # as methods. Hashes and arrays preserve structured instructions and criteria.
21
+ class Judge
22
+ class << self
23
+ def inherited(subclass) # :nodoc:
24
+ super
25
+ subclass.instance_variable_set(:@model_options, model.dup)
26
+ subclass.instance_variable_set(:@input_names, inputs.dup)
27
+ subclass.instance_variable_set(:@question_definitions, question_definitions.dup)
28
+ subclass.instance_variable_set(:@provider_options, provider_options)
29
+ end
30
+
31
+ # Sets the model and provider used by this judge. A block or proc resolves
32
+ # the model using runtime inputs. With no arguments, returns the settings.
33
+ # Without a declaration, uses Configuration#default_judgment_model.
34
+ def model(value = nil, **options, &block)
35
+ return @model_options || {} if value.nil? && options.empty? && !block
36
+
37
+ raise ArgumentError, 'Pass a model or a block, not both' if !value.nil? && block
38
+
39
+ @model_options = options.merge(model: block || value).freeze
40
+ end
41
+
42
+ # Declares required runtime inputs available in question and data blocks.
43
+ # With no arguments, returns the declared names.
44
+ #
45
+ # inputs :teams
46
+ # choice :department, "Which team?", -> { teams.to_h { |team| [team.slug, team.description] } }
47
+ def inputs(*names)
48
+ return @input_names || [] if names.empty?
49
+
50
+ @input_names = names.flatten.map(&:to_sym).freeze
51
+ end
52
+
53
+ # Sets provider-specific request options as a Hash, proc, or block.
54
+ # With no arguments, returns the declared options.
55
+ def provider_options(value = nil, &block)
56
+ return @provider_options || {} if value.nil? && !block
57
+
58
+ raise ArgumentError, 'Pass provider options or a block, not both' if !value.nil? && block
59
+
60
+ @provider_options = Data.copy(block || value)
61
+ end
62
+
63
+ # Declares a yes/no question whose answer is the probability of yes.
64
+ # Optional criteria describe +yes+ and +no+ in a Hash, proc, or block.
65
+ #
66
+ # probability :urgent, "Does this need attention today?" do
67
+ # yes "An explicit deadline today"
68
+ # no "No deadline or a later deadline"
69
+ # end
70
+ def probability(name, instructions = nil, criteria = nil, &)
71
+ declare_question(name, :probability, instructions, criteria, &)
72
+ end
73
+
74
+ # Declares a question selecting one named option. Options are a Hash,
75
+ # a proc returning a Hash, or a block declaring named descriptions.
76
+ #
77
+ # choice :department, "Which team?" do
78
+ # billing "Payments and refunds"
79
+ # other nil
80
+ # end
81
+ def choice(name, instructions = nil, options = nil, &)
82
+ declare_question(name, :choice, instructions, options, &)
83
+ end
84
+
85
+ # Declares a question scored against ordered levels. Levels are an Array
86
+ # or a proc or block returning one. Scores can fall between level indexes.
87
+ #
88
+ # score :frustration, "How frustrated is the customer?", ["Calm", "Frustrated", "Angry"]
89
+ def score(name, instructions = nil, levels = nil, &)
90
+ declare_question(name, :score, instructions, levels, &)
91
+ end
92
+
93
+ # Judges text, a Hash, an Array, or images passed as +with:+ and returns
94
+ # a Judgment. A block or proc can supply the input. Declared inputs are
95
+ # accepted as keyword arguments; remaining options are forwarded to #judge.
96
+ #
97
+ # TicketTriage.judge do
98
+ # message "Please refund the duplicate charge today."
99
+ # end
100
+ def judge(input = nil, **options, &)
101
+ values = options.slice(*inputs)
102
+ new(**values).judge(input, **options.except(*inputs), &)
103
+ end
104
+
105
+ def question_definitions # :nodoc:
106
+ @question_definitions ||= {}
107
+ end
108
+
109
+ private
110
+
111
+ def declare_question(name, type, instructions, criteria, &)
112
+ question = Question.new(name, type:, instructions:, criteria:, &)
113
+ @declared_names ||= []
114
+ raise ArgumentError, "Duplicate question: #{name}" if @declared_names.include?(name.to_s)
115
+
116
+ @declared_names << name.to_s
117
+ question_definitions[name.to_s] = question
118
+ self
119
+ end
120
+ end
121
+
122
+ # Creates a judge with the runtime inputs declared by its class.
123
+ def initialize(**inputs)
124
+ missing = self.class.inputs - inputs.keys
125
+ extra = inputs.keys - self.class.inputs
126
+ raise ArgumentError, "Missing judge inputs: #{missing.join(', ')}" unless missing.empty?
127
+ raise ArgumentError, "Unknown judge inputs: #{extra.join(', ')}" unless extra.empty?
128
+
129
+ inputs.each do |name, value|
130
+ raise ArgumentError, "Judge input conflicts with a method: #{name}" if respond_to?(name, true)
131
+
132
+ define_singleton_method(name) { value }
133
+ end
134
+ end
135
+
136
+ # Judges the supplied input and returns a Judgment. Accepts model and
137
+ # provider overrides, images as +with:+, an isolated +context:+,
138
+ # +provider_options:+, instrumentation +metadata:+, and a usage +owner:+,
139
+ # as RubyLLM.with_usage_owner describes. Additional
140
+ # +questions:+ are a Hash keyed by question name, with +type:+,
141
+ # +instructions:+, and +criteria:+ (probability), +options:+ (choice), or
142
+ # +levels:+ (score). The block supplies input only. Input may be omitted
143
+ # when images are supplied.
144
+ #
145
+ # RubyLLM.judge("Please help today",
146
+ # questions: { urgent: { type: :probability, instructions: "Is this urgent?" } })
147
+ # Receipt.judge(with: "receipt.png")
148
+ def judge(input = nil, questions: {}, with: nil, context: nil, metadata: nil, owner: nil, **options, &block)
149
+ raise ArgumentError, 'Pass judgment input or a block, not both' if !input.nil? && block
150
+
151
+ data = resolve_data(block || input)
152
+ definitions = resolve_questions(questions)
153
+ settings = self.class.model.merge(provider_options: self.class.provider_options).merge(options)
154
+ settings = settings.transform_values { |value| resolve_data(value) }
155
+ Judgment.judge(data, questions: definitions, with:, context:, metadata:, owner:, **settings)
156
+ end
157
+
158
+ private
159
+
160
+ def resolve_data(value)
161
+ Data.copy(value) { |callable| Builder.resolve(callable, scope: self) }
162
+ end
163
+
164
+ def resolve_questions(questions)
165
+ questions = resolve_data(questions)
166
+ raise ArgumentError, 'Questions must be a Hash' unless questions.is_a?(Hash)
167
+
168
+ definitions = self.class.question_definitions.dup
169
+ questions.each do |name, definition|
170
+ raise ArgumentError, "Duplicate question: #{name}" if definitions.key?(name.to_s)
171
+
172
+ definitions[name.to_s] = Question.from_h(name, definition)
173
+ end
174
+ raise ArgumentError, 'A judgment needs at least one question' if definitions.empty?
175
+
176
+ definitions.transform_values { |definition| definition.resolve(self) }
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # The typed answers to a set of questions, with model identity and accounting.
5
+ # Obtain one through RubyLLM.judge or Judge.judge.
6
+ #
7
+ # judgment.urgent.probability
8
+ # judgment.department.choice
9
+ # judgment.frustration.score
10
+ #
11
+ # Named answers are available as readers. Use #[] or #fetch for dynamic
12
+ # names or names that conflict with existing methods, such as #model.
13
+ class Judgment
14
+ include Enumerable
15
+ include Support::Inspectable
16
+ include Accounting::Usage::Result
17
+
18
+ # Returns the answers keyed by their declared String or Symbol names.
19
+ attr_reader :answers
20
+
21
+ # Returns the actual model ID reported by the provider.
22
+ attr_reader :model
23
+
24
+ # Returns the original provider response.
25
+ attr_reader :raw
26
+
27
+ def initialize(answers:, model:, tokens: Tokens.new, raw: nil, model_info: nil) # :nodoc:
28
+ @answers = answers.dup.freeze
29
+ @answer_keys = answers.keys.to_h { |key| [key.to_s, key] }.freeze
30
+ @model = model
31
+ @tokens = tokens
32
+ @raw = raw
33
+ @model_info = model_info
34
+ end
35
+
36
+ # Returns the named answer, accepting a String or Symbol, or nil if absent.
37
+ def [](name)
38
+ answers[@answer_keys[name.to_s]]
39
+ end
40
+
41
+ # Returns the named answer or raises KeyError if it is absent.
42
+ def fetch(name)
43
+ answers.fetch(@answer_keys.fetch(name.to_s))
44
+ end
45
+
46
+ # Yields each question name and answer, or returns an Enumerator.
47
+ def each(&)
48
+ answers.each(&)
49
+ end
50
+
51
+ # Returns token usage across all provider attempts.
52
+ def tokens
53
+ ruby_llm_usage_entries.empty? ? @tokens : ruby_llm_usage_tokens
54
+ end
55
+
56
+ # Returns cost across all provider attempts, preserving unknown amounts.
57
+ def cost
58
+ return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
59
+
60
+ Cost.new(tokens:, model: @model_info)
61
+ end
62
+
63
+ # Returns the model, typed answers, token usage, and cost as a Hash.
64
+ def to_h
65
+ { model:, answers: answers.transform_values(&:to_h), tokens: tokens.to_h, cost: cost.to_h }
66
+ end
67
+
68
+ def inspect_attributes # :nodoc:
69
+ { model:, answers: answers.keys }
70
+ end
71
+
72
+ def self.judge(input, questions:, with: nil, model: nil, provider: nil, context: nil, # :nodoc:
73
+ assume_model_exists: false, provider_options: {}, metadata: nil, owner: nil)
74
+ config = context&.config || RubyLLM.config
75
+ attachments = Attachment.wrap(with, config:)
76
+ validate_input!(input, attachments)
77
+ raise ArgumentError, 'A judgment requires a model' unless model || config.default_judgment_model
78
+
79
+ model, provider_instance = Models.resolve(model, provider:, assume_model_exists:, config:, operation: :judge,
80
+ default_model: config.default_judgment_model)
81
+ empty_tokens = Tokens.new
82
+ payload = {
83
+ provider: provider_instance.slug,
84
+ provider_class: provider_instance.name,
85
+ model: model.id,
86
+ model_info: model,
87
+ question_count: questions.size,
88
+ attachment_count: attachments.size,
89
+ provider_options:,
90
+ metadata:,
91
+ tokens: empty_tokens,
92
+ cost: Cost.new(tokens: empty_tokens, model:)
93
+ }
94
+
95
+ RubyLLM.instrument('judgment.ruby_llm', payload, config:) do |event|
96
+ result = Accounting::Usage.owned_by(owner) do
97
+ provider_instance.judge(input, questions:, model:, with: attachments, provider_options:)
98
+ end
99
+ event[:result] = result
100
+ event[:tokens] = result.tokens
101
+ event[:cost] = result.cost
102
+ result
103
+ end
104
+ end
105
+
106
+ def self.validate_input!(input, attachments)
107
+ return if input.is_a?(String) || input.is_a?(Hash) || input.is_a?(Array) || (input.nil? && attachments.any?)
108
+
109
+ raise ArgumentError, 'Judgment input must be text, a Hash, or an Array'
110
+ end
111
+ private_class_method :validate_input!
112
+
113
+ private
114
+
115
+ def method_missing(name, *args, &block)
116
+ return super unless args.empty? && !block && @answer_keys&.key?(name.to_s)
117
+
118
+ self[name]
119
+ end
120
+
121
+ def respond_to_missing?(name, include_private = nil)
122
+ @answer_keys&.key?(name.to_s) || super
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # Reads what the MCP Apps extension keeps in a tool's _meta: the UI that
6
+ # renders the tool's results, and who may call the tool. Servers on
7
+ # older SDKs name the UI with the flat ui/resourceUri key the spec
8
+ # deprecated.
9
+ module Apps # :nodoc:
10
+ EXTENSION = 'io.modelcontextprotocol/ui'
11
+ MIME_TYPE = 'text/html;profile=mcp-app'
12
+ VISIBILITY = %w[model app].freeze
13
+
14
+ module_function
15
+
16
+ def uri(meta)
17
+ meta.dig('ui', 'resourceUri') || meta['ui/resourceUri']
18
+ end
19
+
20
+ def visibility(meta)
21
+ Array(meta.dig('ui', 'visibility') || VISIBILITY).map(&:to_sym)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # Speaks JSON-RPC to one MCP server over a transport. It speaks the
6
+ # 2026-07-28 revision and falls back to the initialize handshake for
7
+ # servers that predate it, declaring only its extensions so those
8
+ # servers never call back. A server that rejects 2026-07-28 while
9
+ # listing it gets the request once more; one that rejects it while
10
+ # listing a revision from LEGACY_VERSIONS, or without listing any,
11
+ # gets the handshake instead; one that answers the handshake with a
12
+ # revision missing from LEGACY_VERSIONS is disconnected.
13
+ #
14
+ # Servers that predate 2026-07-28 may announce that their tools,
15
+ # prompts, or resources changed on the stream of any request. Those
16
+ # notifications reach the block given to ::new once the request is
17
+ # answered, so the block can make requests of its own. #listen opens a
18
+ # subscription for them instead: subscriptions/listen on newer servers,
19
+ # resources/subscribe and the session's own stream on older ones, which
20
+ # never announce the status of tasks. Their subscription counts as
21
+ # acknowledged once a ping confirms the session, starting a new one if
22
+ # it ended.
23
+ class Client # :nodoc:
24
+ VERSION = '2026-07-28'
25
+ LEGACY_VERSIONS = %w[2025-11-25 2025-06-18 2025-03-26 2024-11-05].freeze
26
+ UNSUPPORTED_VERSION = -32_022
27
+ MODERN_ERRORS = [-32_020, -32_021, UNSUPPORTED_VERSION].freeze
28
+ METHOD_NOT_FOUND = -32_601
29
+ DISCOVERY_TIMEOUT = 10
30
+ ACKNOWLEDGED = 'notifications/subscriptions/acknowledged'
31
+ CHANGES = %w[
32
+ notifications/tools/list_changed notifications/prompts/list_changed
33
+ notifications/resources/list_changed notifications/resources/updated
34
+ ].freeze
35
+
36
+ # The answer to a request the server sends: RubyLLM only answers pings.
37
+ def self.reply(request)
38
+ if request['method'] == 'ping'
39
+ { jsonrpc: '2.0', id: request['id'], result: {} }
40
+ else
41
+ { jsonrpc: '2.0', id: request['id'], error: { code: METHOD_NOT_FOUND, message: 'Method not found' } }
42
+ end
43
+ end
44
+
45
+ attr_reader :version
46
+
47
+ def initialize(transport, capabilities: {}, &on_change)
48
+ @transport = transport
49
+ @capabilities = capabilities
50
+ @on_change = on_change
51
+ @connecting = Mutex.new
52
+ @subscribed = []
53
+ end
54
+
55
+ def server
56
+ @connecting.synchronize { @server ||= discover || handshake }
57
+ end
58
+
59
+ def request(method, params = {}, headers: {}, &)
60
+ server
61
+ call(method, params, headers:, &)
62
+ rescue SessionExpired
63
+ @connecting.synchronize { @server = handshake }
64
+ call(method, params, headers:, &)
65
+ end
66
+
67
+ def list(method, key)
68
+ page = request(method)
69
+ items = page.fetch(key, [])
70
+ while (cursor = page['nextCursor'])
71
+ page = request(method, { cursor: })
72
+ items += page.fetch(key, [])
73
+ end
74
+ items
75
+ end
76
+
77
+ # Subscribes to +changes+, a subscriptions/listen filter, and yields
78
+ # each notification of the subscription, starting with the server's
79
+ # acknowledgment. Returns when the server ends the subscription and
80
+ # raises Error when it refuses one or the stream breaks.
81
+ def listen(changes, &)
82
+ server
83
+ raise ConfigurationError, 'The MCP transport does not respond to listen' unless @transport.respond_to?(:listen)
84
+
85
+ modern? ? subscribe(changes, &) : listen_to_session(changes, &)
86
+ end
87
+
88
+ def modern?
89
+ version == VERSION
90
+ end
91
+
92
+ def close
93
+ @connecting.synchronize do
94
+ @transport.close
95
+ @server = nil
96
+ @version = nil
97
+ @subscribed = []
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def discover
104
+ @version = VERSION
105
+ result = call('server/discover', timeout: DISCOVERY_TIMEOUT)
106
+ result if Array(result['supportedVersions']).include?(VERSION)
107
+ rescue Error => e
108
+ raise if MODERN_ERRORS.include?(e.code) && !predates_revision?(e)
109
+ end
110
+
111
+ def handshake
112
+ @version = nil
113
+ params = { protocolVersion: LEGACY_VERSIONS.first, capabilities: @capabilities.slice(:extensions),
114
+ clientInfo: client_info }
115
+ result = call('initialize', params)
116
+ unless LEGACY_VERSIONS.include?(result['protocolVersion'])
117
+ @transport.close
118
+ raise Error, "The server answered with protocol version #{result['protocolVersion']}, " \
119
+ 'which RubyLLM does not speak'
120
+ end
121
+
122
+ @version = result['protocolVersion']
123
+ @subscribed = []
124
+ @transport.notify(message('notifications/initialized'), version:)
125
+ result
126
+ end
127
+
128
+ def call(method, params = {}, timeout: nil, headers: {}, retried: false, &on_notification)
129
+ request = message(method, params, id: SecureRandom.uuid)
130
+ answer(exchange(request, timeout:, headers:, &on_notification))
131
+ rescue Error => e
132
+ raise if retried || !offers_version?(e)
133
+
134
+ call(method, params, timeout:, headers:, retried: true, &on_notification)
135
+ end
136
+
137
+ def exchange(request, timeout:, headers:, &on_notification)
138
+ changes = []
139
+ @transport.request(request, version:, timeout:, headers:) do |notification|
140
+ CHANGES.include?(notification['method']) ? changes << notification : on_notification&.call(notification)
141
+ end
142
+ rescue CancelledError
143
+ cancel(request)
144
+ raise
145
+ ensure
146
+ changes.each { |change| @on_change&.call(change) }
147
+ end
148
+
149
+ def subscribe(changes)
150
+ request = message('subscriptions/listen', { notifications: changes }, id: SecureRandom.uuid)
151
+ ending = catch(request) do
152
+ @transport.listen(request, version:) do |notification|
153
+ throw request, notification if cancels?(notification, request)
154
+ yield notification
155
+ end
156
+ end
157
+ answer(ending)
158
+ rescue CancelledError
159
+ cancel(request)
160
+ raise
161
+ end
162
+
163
+ def listen_to_session(changes, &)
164
+ uris = Array(changes[:resourceSubscriptions])
165
+ (@subscribed - uris).each { |uri| request('resources/unsubscribe', { uri: }) }
166
+ uris.each { |uri| request('resources/subscribe', { uri: }) }
167
+ @subscribed = uris
168
+ request('ping')
169
+ listened = changes.except(:taskIds).transform_keys(&:to_s)
170
+ yield({ 'method' => ACKNOWLEDGED, 'params' => { 'notifications' => listened } })
171
+ @transport.listen(nil, version:, &)
172
+ rescue SessionExpired
173
+ @connecting.synchronize { @server = handshake }
174
+ raise
175
+ end
176
+
177
+ def cancels?(notification, request)
178
+ notification['method'] == 'notifications/cancelled' && notification.dig('params', 'requestId') == request[:id]
179
+ end
180
+
181
+ def cancel(request)
182
+ @transport.cancel(message('notifications/cancelled', { requestId: request[:id] }), version:)
183
+ end
184
+
185
+ def answer(response)
186
+ error = response['error']
187
+ raise Error.new(error['message'], code: error['code'], data: error['data']) if error
188
+
189
+ response['result']
190
+ end
191
+
192
+ def offers_version?(error)
193
+ modern? && error.code == UNSUPPORTED_VERSION && supported_versions(error).include?(VERSION)
194
+ end
195
+
196
+ def predates_revision?(error)
197
+ return false unless error.code == UNSUPPORTED_VERSION
198
+
199
+ supported = supported_versions(error)
200
+ supported.empty? || supported.intersect?(LEGACY_VERSIONS)
201
+ end
202
+
203
+ def supported_versions(error)
204
+ supported = error.data['supported'] if error.data.is_a?(Hash)
205
+ Array(supported)
206
+ end
207
+
208
+ def message(method, params = {}, id: nil)
209
+ params = params.merge(_meta: meta.merge(params.fetch(:_meta, {}))) if modern?
210
+ { jsonrpc: '2.0', id:, method:, params: }.compact
211
+ end
212
+
213
+ def meta
214
+ {
215
+ 'io.modelcontextprotocol/protocolVersion' => VERSION,
216
+ 'io.modelcontextprotocol/clientInfo' => client_info,
217
+ 'io.modelcontextprotocol/clientCapabilities' => @capabilities
218
+ }
219
+ end
220
+
221
+ def client_info
222
+ { name: 'ruby_llm', version: RubyLLM::VERSION }
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # The MCP servers connected to a chat, readable by name.
6
+ #
7
+ # chat.with_mcp(Linear.new(user: current_user))
8
+ # chat.mcp.linear # => #<Linear ...>
9
+ # chat.mcp[:linear] # => #<Linear ...>
10
+ # chat.mcp.map(&:name) # => ["linear"]
11
+ #
12
+ class Collection
13
+ include Enumerable
14
+
15
+ def initialize # :nodoc:
16
+ @servers = {}
17
+ end
18
+
19
+ def <<(server) # :nodoc:
20
+ @servers[server.name.to_sym] = server
21
+ self
22
+ end
23
+
24
+ # Returns the server named +name+, or +nil+.
25
+ def [](name)
26
+ @servers[name.to_sym]
27
+ end
28
+
29
+ # Yields each server.
30
+ def each(&)
31
+ @servers.each_value(&)
32
+ end
33
+
34
+ # Returns whether no servers are connected.
35
+ def empty?
36
+ @servers.empty?
37
+ end
38
+
39
+ def inspect # :nodoc:
40
+ "#<#{self.class.name} #{@servers.keys.join(', ')}>"
41
+ end
42
+
43
+ private
44
+
45
+ def method_missing(name, *arguments)
46
+ return super unless arguments.empty? && @servers.key?(name)
47
+
48
+ @servers[name]
49
+ end
50
+
51
+ def respond_to_missing?(name, include_private = nil)
52
+ @servers.key?(name) || super
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # Reads MCP content blocks into text and attachments. Tool results and
6
+ # prompt messages share the format.
7
+ module Content # :nodoc:
8
+ module_function
9
+
10
+ def read(blocks)
11
+ texts, attachments = Array(blocks).filter_map { |block| part(block) }.partition { |part| part.is_a?(String) }
12
+ [texts.join("\n\n"), attachments]
13
+ end
14
+
15
+ def part(block)
16
+ case block['type']
17
+ when 'text' then block['text']
18
+ when 'image', 'audio' then attachment(block['data'], block['mimeType'], block['type'])
19
+ when 'resource' then embedded(block['resource'] || {})
20
+ when 'resource_link' then [block['title'] || block['name'], block['uri']].compact.join(': ')
21
+ end
22
+ end
23
+
24
+ def embedded(resource)
25
+ return resource['text'] if resource['text']
26
+ return unless resource['blob']
27
+
28
+ attachment(resource['blob'], resource['mimeType'], filename(resource['uri']))
29
+ end
30
+
31
+ def attachment(data, mime_type, name)
32
+ extension = Marcel::TYPE_EXTS[mime_type]&.first
33
+ name = "#{name}.#{extension}" unless name.to_s.include?('.') || extension.nil?
34
+ Attachment.new(StringIO.new(Base64.decode64(data.to_s)), filename: name.to_s)
35
+ end
36
+
37
+ def filename(uri)
38
+ File.basename(URI(uri.to_s).path.to_s)
39
+ rescue URI::InvalidURIError
40
+ 'resource'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # Raised when an MCP server answers with a JSON-RPC error, an unexpected
6
+ # HTTP status, or not at all.
7
+ #
8
+ # begin
9
+ # github.search_issues(query: "flaky")
10
+ # rescue RubyLLM::MCP::Error => e
11
+ # e.code # => -32602
12
+ # end
13
+ class Error < RubyLLM::Error
14
+ # The JSON-RPC error code, or +nil+ when the failure had none.
15
+ attr_reader :code
16
+
17
+ # Additional information the server attached to the error, or +nil+.
18
+ attr_reader :data
19
+
20
+ def initialize(message = nil, code: nil, data: nil, response: nil) # :nodoc:
21
+ @code = code
22
+ @data = data
23
+ super(message, response:)
24
+ end
25
+ end
26
+ end
27
+ end