ruby_llm 2.0.0 → 2.1.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 (284) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -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 +136 -40
  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 +488 -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 +213 -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 +192 -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/media.rb +34 -8
  115. data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
  116. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
  117. data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
  118. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
  119. data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
  120. data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
  121. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
  122. data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
  123. data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
  124. data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
  125. data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
  126. data/lib/ruby_llm/protocols/cohere.rb +1 -0
  127. data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
  128. data/lib/ruby_llm/protocols/converse/media.rb +16 -2
  129. data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
  130. data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
  131. data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
  132. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
  133. data/lib/ruby_llm/protocols/converse.rb +3 -5
  134. data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
  135. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
  136. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
  137. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
  138. data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
  139. data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
  140. data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
  141. data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
  142. data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
  143. data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
  144. data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
  145. data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
  146. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
  147. data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
  148. data/lib/ruby_llm/protocols/gemini.rb +1 -0
  149. data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
  150. data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
  151. data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
  152. data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
  153. data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
  154. data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
  155. data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
  156. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  157. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
  158. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
  159. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
  160. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
  161. data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
  162. data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
  163. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
  164. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
  165. data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
  166. data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
  167. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
  168. data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
  169. data/lib/ruby_llm/protocols/openai/files.rb +0 -7
  170. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
  171. data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
  172. data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
  173. data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
  174. data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
  175. data/lib/ruby_llm/protocols/responses/media.rb +11 -7
  176. data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
  177. data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
  178. data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
  179. data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
  180. data/lib/ruby_llm/protocols/responses.rb +1 -0
  181. data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
  182. data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
  183. data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
  184. data/lib/ruby_llm/protocols/system_one.rb +27 -0
  185. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
  186. data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
  187. data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
  188. data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
  189. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
  190. data/lib/ruby_llm/provider.rb +87 -15
  191. data/lib/ruby_llm/providers/anthropic.rb +23 -0
  192. data/lib/ruby_llm/providers/azure/chat.rb +4 -0
  193. data/lib/ruby_llm/providers/azure/images.rb +2 -2
  194. data/lib/ruby_llm/providers/azure/media.rb +8 -3
  195. data/lib/ruby_llm/providers/azure/videos.rb +7 -2
  196. data/lib/ruby_llm/providers/azure.rb +24 -4
  197. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
  198. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
  199. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
  200. data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
  201. data/lib/ruby_llm/providers/bedrock.rb +38 -6
  202. data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
  203. data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
  204. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  205. data/lib/ruby_llm/providers/gemini.rb +4 -0
  206. data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
  207. data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
  208. data/lib/ruby_llm/providers/hetzner.rb +41 -0
  209. data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
  210. data/lib/ruby_llm/providers/mistral/media.rb +8 -4
  211. data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
  212. data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
  213. data/lib/ruby_llm/providers/ollama/media.rb +6 -2
  214. data/lib/ruby_llm/providers/ollama/models.rb +2 -0
  215. data/lib/ruby_llm/providers/ollama.rb +11 -0
  216. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
  217. data/lib/ruby_llm/providers/openai.rb +14 -0
  218. data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
  219. data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
  220. data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
  221. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
  222. data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
  223. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  224. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  225. data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
  226. data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
  227. data/lib/ruby_llm/providers/perplexity.rb +11 -0
  228. data/lib/ruby_llm/providers/typesafe.rb +32 -0
  229. data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
  230. data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
  231. data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
  232. data/lib/ruby_llm/providers/vertexai.rb +27 -28
  233. data/lib/ruby_llm/providers/xai/chat.rb +6 -0
  234. data/lib/ruby_llm/providers/xai/images.rb +9 -10
  235. data/lib/ruby_llm/providers/xai/responses.rb +10 -8
  236. data/lib/ruby_llm/providers/xai/videos.rb +11 -9
  237. data/lib/ruby_llm/providers/xai.rb +4 -0
  238. data/lib/ruby_llm/railtie.rb +11 -0
  239. data/lib/ruby_llm/request_shape/part.rb +117 -0
  240. data/lib/ruby_llm/request_shape/problem.rb +63 -0
  241. data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
  242. data/lib/ruby_llm/request_shape/turn.rb +54 -0
  243. data/lib/ruby_llm/request_shape.rb +160 -0
  244. data/lib/ruby_llm/rerank.rb +9 -2
  245. data/lib/ruby_llm/research_job.rb +21 -5
  246. data/lib/ruby_llm/score.rb +42 -0
  247. data/lib/ruby_llm/search_results.rb +2 -2
  248. data/lib/ruby_llm/server_tool_call.rb +10 -2
  249. data/lib/ruby_llm/speech.rb +8 -1
  250. data/lib/ruby_llm/support/cancellation.rb +39 -0
  251. data/lib/ruby_llm/support/inspectable.rb +3 -2
  252. data/lib/ruby_llm/support/instrumentation.rb +38 -1
  253. data/lib/ruby_llm/support/process_cache.rb +60 -0
  254. data/lib/ruby_llm/support/progress_reporter.rb +33 -0
  255. data/lib/ruby_llm/thinking.rb +2 -1
  256. data/lib/ruby_llm/tokens.rb +57 -14
  257. data/lib/ruby_llm/tool.rb +50 -3
  258. data/lib/ruby_llm/tool_call.rb +17 -2
  259. data/lib/ruby_llm/transcription.rb +10 -3
  260. data/lib/ruby_llm/transport/connection.rb +127 -44
  261. data/lib/ruby_llm/transport/error_middleware.rb +90 -24
  262. data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
  263. data/lib/ruby_llm/transport/json_response.rb +33 -0
  264. data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
  265. data/lib/ruby_llm/version.rb +1 -1
  266. data/lib/ruby_llm/video.rb +5 -3
  267. data/lib/ruby_llm/video_job.rb +48 -5
  268. data/lib/ruby_llm.rb +106 -5
  269. data/lib/tasks/ruby_llm/evaluations.rb +43 -0
  270. data/lib/tasks/ruby_llm.rake +6 -9
  271. data/skills/rubyllm/SKILL.md +33 -12
  272. metadata +95 -37
  273. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
  274. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
  275. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
  276. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
  277. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
  278. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
  279. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
  280. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
  281. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
  282. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
  283. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
  284. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A selected judgment option and its full probability distribution.
5
+ class Choice
6
+ include Support::Inspectable
7
+
8
+ # Returns the selected option, preserving its declared String or Symbol type.
9
+ attr_reader :choice
10
+
11
+ # Returns each declared option's probability.
12
+ attr_reader :probabilities
13
+
14
+ # Returns the provider's confidence in the distribution, between 0 and 1.
15
+ attr_reader :confidence
16
+
17
+ def initialize(choice:, probabilities:, confidence:) # :nodoc:
18
+ @choice = choice.is_a?(String) ? choice.dup.freeze : choice
19
+ @probabilities = probabilities.dup.freeze
20
+ @confidence = confidence
21
+ freeze
22
+ end
23
+
24
+ # Returns :choice.
25
+ def type
26
+ :choice
27
+ end
28
+
29
+ # Returns the answer as a Hash.
30
+ def to_h
31
+ { type:, choice:, probabilities:, confidence: }
32
+ end
33
+
34
+ def inspect_attributes # :nodoc:
35
+ { choice:, confidence: }
36
+ end
37
+ end
38
+ end
@@ -82,6 +82,13 @@ module RubyLLM
82
82
  # Default: <tt>'omni-moderation-latest'</tt>.
83
83
  option :default_moderation_model, 'omni-moderation-latest'
84
84
 
85
+ ##
86
+ # :attr_accessor: default_judgment_model
87
+ #
88
+ # The model id used by RubyLLM.judge and Judge when no model is given.
89
+ # Default: <tt>'jev-latest'</tt>.
90
+ option :default_judgment_model, 'jev-latest'
91
+
85
92
  ##
86
93
  # :attr_accessor: default_image_model
87
94
  #
@@ -152,6 +159,33 @@ module RubyLLM
152
159
  # Optional persistence adapter installed by the Rails integration.
153
160
  option :batch_store, nil # :nodoc:
154
161
 
162
+ ##
163
+ # :attr_accessor: mcp_credential_store
164
+ #
165
+ # Store for the OAuth credentials of MCP servers. Rails apps keep them
166
+ # in the +ruby_llm_mcp_credentials+ table automatically. A store
167
+ # responds to +read(key)+, +write(key, data, owner:)+, and
168
+ # +delete(key)+. A store that several processes share also responds
169
+ # to +synchronize(key)+, running the block while no other process
170
+ # holds +key+, so only one of them refreshes a token at a time.
171
+ # Default: +nil+ (keep them in memory).
172
+ option :mcp_credential_store, nil
173
+
174
+ ##
175
+ # :attr_accessor: mcp_client_name
176
+ #
177
+ # The client name RubyLLM gives when it registers with an MCP server's
178
+ # authorization server. Default: <tt>"RubyLLM"</tt>.
179
+ option :mcp_client_name, 'RubyLLM'
180
+
181
+ ##
182
+ # :attr_accessor: mcp_client_id
183
+ #
184
+ # The HTTPS URL of your OAuth client metadata document. Authorization
185
+ # servers that support client ID metadata documents use it instead of
186
+ # registering RubyLLM. Default: +nil+.
187
+ option :mcp_client_id, nil
188
+
155
189
  # Keeps custom 1.x initializers bootable long enough to run the 2.0
156
190
  # upgrade generator. The registry is always RubyLLM-owned in 2.0.
157
191
  def model_registry_class=(_value) # :nodoc:
@@ -288,9 +322,8 @@ module RubyLLM
288
322
  #
289
323
  # Timeout in seconds for the regular expressions that scrub logged
290
324
  # payloads. Defaults to the global <tt>Regexp.timeout</tt>, or 1.0
291
- # when none is set. Requires Ruby 3.2 or later; on older Rubies
292
- # setting a value logs a warning.
293
- option :log_regexp_timeout, -> { Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil }
325
+ # when none is set.
326
+ option :log_regexp_timeout, -> { Regexp.timeout || 1.0 }
294
327
 
295
328
  def initialize # :nodoc:
296
329
  self.class.send(:defaults).each do |key, default|
@@ -311,14 +344,6 @@ module RubyLLM
311
344
  "#<#{self.class.name} #{attributes.join(', ')}>"
312
345
  end
313
346
 
314
- remove_method :log_regexp_timeout=
315
- def log_regexp_timeout=(value) # :nodoc:
316
- if value && !Regexp.respond_to?(:timeout)
317
- RubyLLM.logger.warn("log_regexp_timeout is not supported on Ruby #{RUBY_VERSION}")
318
- end
319
- @log_regexp_timeout = value
320
- end
321
-
322
347
  private
323
348
 
324
349
  # Whether the +name+ environment variable is set to something that means
@@ -35,8 +35,8 @@ module RubyLLM
35
35
 
36
36
  # Creates a new Chat that uses this context's configuration.
37
37
  # Accepts the same arguments as RubyLLM.chat.
38
- def chat(*args, **kwargs, &)
39
- Chat.new(*args, **kwargs, context: self, &)
38
+ def chat(*, **, &)
39
+ Chat.new(*, **, context: self, &)
40
40
  end
41
41
 
42
42
  # Counts tokens using this context's configuration.
@@ -47,8 +47,8 @@ module RubyLLM
47
47
 
48
48
  # Tokenizes plain text using this context's configuration.
49
49
  # Accepts the same arguments as RubyLLM.tokenize.
50
- def tokenize(*args, **kwargs)
51
- Tokenization.tokenize(*args, **kwargs, context: self)
50
+ def tokenize(*, **)
51
+ Tokenization.tokenize(*, **, context: self)
52
52
  end
53
53
 
54
54
  # Runs a named workflow using this context's instrumenter. Accepts the same
@@ -59,8 +59,8 @@ module RubyLLM
59
59
 
60
60
  # Generates embeddings using this context's configuration.
61
61
  # Accepts the same arguments as RubyLLM.embed.
62
- def embed(*args, **kwargs, &)
63
- Embedding.embed(*args, **kwargs, context: self, &)
62
+ def embed(*, **, &)
63
+ Embedding.embed(*, **, context: self, &)
64
64
  end
65
65
 
66
66
  # Stages an embedding request using this context's configuration.
@@ -71,81 +71,94 @@ module RubyLLM
71
71
 
72
72
  # Generates an image using this context's configuration.
73
73
  # Accepts the same arguments as RubyLLM.paint.
74
- def paint(*args, **kwargs, &)
75
- Image.paint(*args, **kwargs, context: self, &)
74
+ def paint(*, **, &)
75
+ Image.paint(*, **, context: self, &)
76
76
  end
77
77
 
78
78
  # Generates a video using this context's configuration, blocking
79
79
  # until it is ready. Accepts the same arguments as RubyLLM.animate.
80
- def animate(*args, **kwargs, &)
81
- Video.animate(*args, **kwargs, context: self, &)
80
+ def animate(*, **, &)
81
+ Video.animate(*, **, context: self, &)
82
82
  end
83
83
 
84
84
  # Submits a video generation job using this context's configuration.
85
85
  # Accepts the same arguments as RubyLLM.animate_later.
86
- def animate_later(*args, **kwargs, &)
87
- VideoJob.animate_later(*args, **kwargs, context: self, &)
86
+ def animate_later(*, **, &)
87
+ VideoJob.animate_later(*, **, context: self, &)
88
88
  end
89
89
 
90
90
  # Runs content moderation using this context's configuration.
91
91
  # Accepts the same arguments as RubyLLM.moderate.
92
- def moderate(*args, **kwargs, &)
93
- Moderation.moderate(*args, **kwargs, context: self, &)
92
+ def moderate(*, **, &)
93
+ Moderation.moderate(*, **, context: self, &)
94
+ end
95
+
96
+ # Judges text, structured data, or images using this context's configuration.
97
+ # Accepts the same arguments as RubyLLM.judge.
98
+ def judge(*, **, &)
99
+ Judge.judge(*, **, context: self, &)
94
100
  end
95
101
 
96
102
  # Runs hosted research using this context's configuration.
97
103
  # Accepts the same arguments as RubyLLM.research.
98
- def research(*args, **kwargs)
99
- ResearchJob.research(*args, **kwargs, context: self)
104
+ def research(*, **)
105
+ ResearchJob.research(*, **, context: self)
100
106
  end
101
107
 
102
108
  # Submits hosted research using this context's configuration.
103
109
  # Accepts the same arguments as RubyLLM.research_later.
104
- def research_later(*args, **kwargs)
105
- ResearchJob.research_later(*args, **kwargs, context: self)
110
+ def research_later(*, **)
111
+ ResearchJob.research_later(*, **, context: self)
106
112
  end
107
113
 
108
114
  # Generates speech audio using this context's configuration. Given a
109
115
  # block, yields SpeechChunk objects and returns the complete Speech.
110
116
  # Accepts the same arguments as RubyLLM.speak.
111
- def speak(*args, **kwargs, &)
112
- Speech.speak(*args, **kwargs, context: self, &)
117
+ def speak(*, **, &)
118
+ Speech.speak(*, **, context: self, &)
113
119
  end
114
120
 
115
121
  # Transcribes audio using this context's configuration.
116
122
  # Accepts the same arguments as RubyLLM.transcribe.
117
- def transcribe(*args, **kwargs, &)
118
- Transcription.transcribe(*args, **kwargs, context: self, &)
123
+ def transcribe(*, **, &)
124
+ Transcription.transcribe(*, **, context: self, &)
119
125
  end
120
126
 
121
127
  # Extracts document text using this context's configuration.
122
128
  # Accepts the same arguments as RubyLLM.ocr.
123
- def ocr(*args, **kwargs, &)
124
- OCR.ocr(*args, **kwargs, context: self, &)
129
+ def ocr(*, **, &)
130
+ OCR.ocr(*, **, context: self, &)
125
131
  end
126
132
 
127
133
  # Ranks documents using this context's configuration.
128
134
  # Accepts the same arguments as RubyLLM.rerank.
129
- def rerank(*args, **kwargs, &)
130
- Rerank.rerank(*args, **kwargs, context: self, &)
135
+ def rerank(*, **, &)
136
+ Rerank.rerank(*, **, context: self, &)
131
137
  end
132
138
 
133
139
  # Uploads a file to a provider using this context's configuration.
134
140
  # Accepts the same arguments as RubyLLM.upload.
135
- def upload(*args, **kwargs, &)
136
- UploadedFile.upload(*args, **kwargs, context: self, &)
141
+ def upload(*, **, &)
142
+ UploadedFile.upload(*, **, context: self, &)
137
143
  end
138
144
 
139
145
  # Downloads a provider-hosted file using this context's configuration.
140
146
  # Accepts the same arguments as RubyLLM.download.
141
- def download(*args, **kwargs, &)
142
- UploadedFile.download(*args, **kwargs, context: self, &)
147
+ def download(*, **, &)
148
+ UploadedFile.download(*, **, context: self, &)
143
149
  end
144
150
 
145
151
  # Creates a provider-side prompt cache using this context's
146
152
  # configuration. Accepts the same arguments as RubyLLM.cache.
147
- def cache(*args, **kwargs, &)
148
- CachedContent.create(*args, **kwargs, context: self, &)
153
+ def cache(*, **, &)
154
+ CachedContent.create(*, **, context: self, &)
155
+ end
156
+
157
+ # Connects to an MCP server using this context's configuration, so its
158
+ # connection settings apply to the server's requests and to its OAuth
159
+ # requests. Accepts the same arguments as RubyLLM.mcp.
160
+ def mcp(...)
161
+ MCP.define(...).new(context: self)
149
162
  end
150
163
  end
151
164
  end
data/lib/ruby_llm/cost.rb CHANGED
@@ -130,7 +130,9 @@ module RubyLLM
130
130
  end
131
131
 
132
132
  # Returns the cost of cache-write input tokens in US dollars, or +nil+
133
- # when the token count or its pricing is unavailable.
133
+ # when the token count or its pricing is unavailable. Writes with a
134
+ # longer lifetime, reported in Tokens#cache_write_by_ttl, are priced at
135
+ # the provider's rate for that lifetime.
134
136
  def cache_write
135
137
  @amounts[:cache_write]
136
138
  end
@@ -199,6 +201,7 @@ module RubyLLM
199
201
 
200
202
  def missing_component?(component)
201
203
  return image_input_missing? if component == :input && detailed_image_input?
204
+ return cache_write_missing? if component == :cache_write && repriced_cache_writes?
202
205
  return false if component == :thinking && !thinking_priced_separately?
203
206
 
204
207
  tokens_for(component).to_i.positive? && price_for(component).nil?
@@ -206,6 +209,7 @@ module RubyLLM
206
209
 
207
210
  def amount_for(component)
208
211
  return image_input_amount if component == :input && detailed_image_input?
212
+ return cache_write_amount if component == :cache_write && repriced_cache_writes?
209
213
 
210
214
  token_count = tokens_for(component)
211
215
  return nil if token_count.nil?
@@ -299,6 +303,43 @@ module RubyLLM
299
303
  pricing.public_send(@category)
300
304
  end
301
305
 
306
+ def repriced_cache_writes?
307
+ cache_write_multipliers.any?
308
+ end
309
+
310
+ def cache_write_amount
311
+ cache_write_parts.sum { |count, price| count * price / PER_MILLION } unless cache_write_missing?
312
+ end
313
+
314
+ def cache_write_missing?
315
+ cache_write_parts.any? { |_, price| price.nil? }
316
+ end
317
+
318
+ # Cache writes as [count, price] pairs: lifetimes the provider prices
319
+ # from the input price, then the rest at the registry's cache-write price.
320
+ def cache_write_parts
321
+ parts = cache_write_multipliers.map do |ttl, multiplier|
322
+ [cache_write_by_ttl[ttl], input_price && (input_price * multiplier)]
323
+ end
324
+ standard = @tokens.cache_write.to_i - parts.sum(&:first)
325
+ parts << [standard, price_for(:cache_write)] if standard.positive?
326
+ parts
327
+ end
328
+
329
+ def cache_write_multipliers
330
+ @cache_write_multipliers ||= begin
331
+ provider = @model.provider_class if @model.respond_to?(:provider_class)
332
+ cache_write_by_ttl.filter_map do |ttl, _|
333
+ multiplier = provider&.cache_write_input_multiplier(@model.id, ttl)
334
+ [ttl, multiplier] if multiplier
335
+ end.to_h
336
+ end
337
+ end
338
+
339
+ def cache_write_by_ttl
340
+ (@tokens.cache_write_by_ttl if @tokens.respond_to?(:cache_write_by_ttl)) || {}
341
+ end
342
+
302
343
  def image_cost?
303
344
  %i[image images].include?(@category)
304
345
  end
@@ -54,6 +54,8 @@ module RubyLLM
54
54
  Cost.new(tokens:, model: model_info, category: :embeddings)
55
55
  end
56
56
 
57
+ attr_writer :model_info # :nodoc:
58
+
57
59
  def model_info # :nodoc:
58
60
  @model_info ||= RubyLLM.models.find(model)
59
61
  rescue ModelNotFoundError
@@ -97,6 +99,8 @@ module RubyLLM
97
99
  # in the provider's request vocabulary and merges them into the
98
100
  # request as-is. +metadata:+ is not sent to the provider; it is
99
101
  # attached to the emitted +embedding.ruby_llm+ instrumentation event.
102
+ # +owner:+ attributes the usage to a record, such as a user, and wins
103
+ # over RubyLLM.with_usage_owner.
100
104
  def self.embed(text,
101
105
  model: nil,
102
106
  provider: nil,
@@ -107,7 +111,8 @@ module RubyLLM
107
111
  title: nil,
108
112
  with: nil,
109
113
  provider_options: {},
110
- metadata: nil)
114
+ metadata: nil,
115
+ owner: nil)
111
116
  config = context&.config || RubyLLM.config
112
117
  model ||= config.default_embedding_model
113
118
  model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
@@ -132,7 +137,9 @@ module RubyLLM
132
137
  }
133
138
 
134
139
  RubyLLM.instrument('embedding.ruby_llm', payload, config: config) do |event|
135
- result = provider_instance.embed(text, model:, dimensions:, task_type:, title:, with:, provider_options:)
140
+ result = Accounting::Usage.owned_by(owner) do
141
+ provider_instance.embed(text, model:, dimensions:, task_type:, title:, with:, provider_options:)
142
+ end
136
143
  event[:result] = result
137
144
  event[:response_model] = result.model
138
145
  event[:tokens] = result.tokens
@@ -4,8 +4,9 @@ module RubyLLM
4
4
  # Error is the base class for provider-operation errors raised by
5
5
  # RubyLLM, including API, network, capability, and provider response-shape
6
6
  # failures. When an HTTP response is available, it wraps that response and
7
- # normalizes the message across providers. Subclasses map common HTTP
8
- # status codes: BadRequestError (400), UnauthorizedError (401),
7
+ # normalizes the message across providers. When the provider refuses a
8
+ # conversation, #request_shape describes what it was sent. Subclasses map
9
+ # common HTTP status codes: BadRequestError (400), UnauthorizedError (401),
9
10
  # PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
10
11
  # ServerError (500), ServiceUnavailableError (502 to 504), and
11
12
  # OverloadedError (529).
@@ -27,6 +28,13 @@ module RubyLLM
27
28
  # available. Its +status+ and +body+ carry the provider's reply.
28
29
  attr_reader :response
29
30
 
31
+ # The Protocol that rendered the request the error answered, which
32
+ # reads its payload back for #request_shape.
33
+ attr_accessor :request_protocol # :nodoc:
34
+
35
+ # The payload request_protocol rendered for the request.
36
+ attr_accessor :request_payload # :nodoc:
37
+
30
38
  def self.default_message # :nodoc:
31
39
  nil
32
40
  end
@@ -37,6 +45,24 @@ module RubyLLM
37
45
  @response = response
38
46
  super(message || response&.body || self.class.default_message)
39
47
  end
48
+
49
+ # Returns a RequestShape describing the conversation request the error
50
+ # answered: each turn's parts and their sizes, never their contents,
51
+ # and the problems providers are known to refuse. Returns +nil+ for
52
+ # errors raised before a request goes out, such as an unsupported
53
+ # attachment, and for operations that send no conversation, such as
54
+ # embeddings.
55
+ #
56
+ # rescue RubyLLM::BadRequestError => e
57
+ # Rails.logger.error("#{e.message}\n#{e.request_shape}")
58
+ # Rails.error.report(e, context: { request_shape: e.request_shape&.to_h })
59
+ #
60
+ def request_shape
61
+ return unless request_protocol
62
+ return @request_shape if defined?(@request_shape)
63
+
64
+ @request_shape = request_protocol.request_shape(request_payload)
65
+ end
40
66
  end
41
67
 
42
68
  # Raised when a deprecated API is used and
@@ -137,6 +163,12 @@ module RubyLLM
137
163
  end
138
164
  end
139
165
 
166
+ # Raised when the provider's content filters block a transcription and
167
+ # no transcript comes back. The message names the provider's reason,
168
+ # such as +SAFETY+. Chat responses report a block through
169
+ # Message#content_filtered? instead of raising.
170
+ class ContentFilterError < Error; end
171
+
140
172
  # Raised for HTTP 529 responses when the provider is temporarily
141
173
  # overloaded.
142
174
  class OverloadedError < Error
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Evaluation
5
+ class Assertions # :nodoc:
6
+ METHOD = /\A(?:assert|refute)(?:_|\z)/
7
+
8
+ # Matches Minitest assertion failures without loading Minitest.
9
+ module Failure # :nodoc:
10
+ def self.===(error)
11
+ defined?(::Minitest::Assertion) && error.is_a?(::Minitest::Assertion)
12
+ end
13
+ end
14
+
15
+ attr_accessor :assertions
16
+
17
+ class << self
18
+ def method?(name)
19
+ METHOD.match?(name) && available? && ::Minitest::Assertions.method_defined?(name)
20
+ end
21
+
22
+ def available?
23
+ require 'minitest'
24
+ true
25
+ rescue LoadError
26
+ false
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ raise LoadError, "Evaluation assertions require the 'minitest' gem. Add it to your Gemfile." unless
32
+ self.class.available?
33
+
34
+ extend ::Minitest::Assertions
35
+
36
+ @assertions = 0
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Evaluation
5
+ # One dataset example, with application inputs and optional reference evidence.
6
+ class Case
7
+ include Support::Inspectable
8
+
9
+ # Returns the stable case name used to compare runs.
10
+ attr_reader :name
11
+ # Returns the value passed to Evaluation#perform.
12
+ attr_reader :inputs
13
+ # Returns the reference answer, or nil when none was supplied.
14
+ attr_reader :expected_output
15
+ # Returns application-defined reference evidence and category labels.
16
+ attr_reader :metadata
17
+
18
+ # Creates a case. Inputs and reference evidence must be JSON-compatible.
19
+ def initialize(name:, inputs:, metadata: {}, **reference)
20
+ unless (reference.keys - [:expected_output]).empty?
21
+ raise ArgumentError,
22
+ "Unknown case fields: #{reference.keys - [:expected_output]}"
23
+ end
24
+ raise ArgumentError, 'A case name cannot be empty' if name.to_s.empty?
25
+ raise ArgumentError, 'Case metadata must be a Hash' unless metadata.is_a?(Hash)
26
+
27
+ @name = name.to_s.dup.freeze
28
+ @inputs = Judge::Data.copy(inputs)
29
+ @metadata = Judge::Data.copy(metadata)
30
+ @expected_output = Judge::Data.copy(reference[:expected_output])
31
+ @reference_supplied = reference.key?(:expected_output)
32
+ freeze
33
+ end
34
+
35
+ # Returns whether a reference was supplied, including an explicit nil.
36
+ def expected_output?
37
+ @reference_supplied
38
+ end
39
+
40
+ # Returns the portable dataset representation of this case.
41
+ def to_h
42
+ data = { name:, inputs:, metadata: }
43
+ data[:expected_output] = expected_output if expected_output?
44
+ data
45
+ end
46
+
47
+ def inspect_attributes # :nodoc:
48
+ { name: }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'pathname'
5
+
6
+ module RubyLLM
7
+ class Evaluation
8
+ module Dataset # :nodoc:
9
+ module_function
10
+
11
+ def load(source, name:)
12
+ source = rows(source, name:)
13
+ cases = source.map do |row|
14
+ row.is_a?(Case) ? row : Case.new(**row.transform_keys(&:to_sym))
15
+ end
16
+ raise ArgumentError, 'A dataset cannot be empty' if cases.empty?
17
+ raise ArgumentError, 'Dataset case names must be unique' unless cases.map(&:name).uniq.size == cases.size
18
+
19
+ cases
20
+ end
21
+
22
+ def rows(source, name:)
23
+ source = source.call if source.respond_to?(:call)
24
+ source ||= discover(name)
25
+ source = read(source) if source.is_a?(String) || source.is_a?(Pathname)
26
+ source = cases_from_hash(source) if source.is_a?(Hash)
27
+ raise ArgumentError, 'A dataset must contain enumerable cases' unless source.respond_to?(:map)
28
+
29
+ source
30
+ end
31
+
32
+ def cases_from_hash(source)
33
+ unknown = source.keys.map(&:to_s) - %w[name cases]
34
+ if unknown.any?
35
+ raise ArgumentError,
36
+ "Unsupported dataset fields: #{unknown.join(', ')}; declare evaluators in Ruby"
37
+ end
38
+
39
+ source.fetch('cases') { source.fetch(:cases) }
40
+ end
41
+
42
+ def discover(name)
43
+ raise ArgumentError, 'An anonymous evaluation needs an explicit dataset' unless name
44
+
45
+ filename = name.gsub('::', '/').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
46
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
47
+ root = Prompt.root.parent.join('evals')
48
+ paths = %w[yml yaml json jsonl].map { |extension| root.join("#{filename}.#{extension}") }.select(&:file?)
49
+ raise ArgumentError, "Dataset not found: #{root.join(filename)}.{yml,yaml,json,jsonl}" if paths.empty?
50
+ raise ArgumentError, "Ambiguous dataset: #{paths.join(', ')}" if paths.size > 1
51
+
52
+ paths.first
53
+ end
54
+
55
+ def read(path)
56
+ case File.extname(path)
57
+ when '.yml', '.yaml' then YAML.safe_load_file(path, aliases: false)
58
+ when '.json' then JSON.parse(File.read(path))
59
+ when '.jsonl' then File.foreach(path).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) }
60
+ else raise ArgumentError, "Unsupported dataset format: #{path}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end