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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42dd9dfdb910f8e5df4eb6114ed59586a13e4a51797173d3f3bdb13b6d59c28b
4
- data.tar.gz: fa4e0aa374c915ab5e97146bcacb438fc2c3fca0c029ed51c1aea6c934769da8
3
+ metadata.gz: 2f0478372c3c7c58cd1452546f21068161f553949b064377738c28e74b67da9d
4
+ data.tar.gz: 2f8f63b90a885326904724a95074934877251b589b41ba00d9366dd72d5b3d69
5
5
  SHA512:
6
- metadata.gz: 3e5489b6fdd071de5c4281c9cc8eff9d345dd10876cb6773144873d2e58c3ec400109d208706d4c04c12d93328f5864f839907521fd751accc29aa9a5a753602
7
- data.tar.gz: 19c33af936f0626262ad3e469c955b98aee8599b15fe088ba745e31584f91ebbf166cabc981bef4bef526f6e6c96800aebe07dc133ca732313ca58598aa6273f
6
+ metadata.gz: 4028eea4e3515ffa779ada1d1464ab5ed4fa4c21418c1ebaa8fe09bb0da11fd6998f6b0132dde1cd59f876b54a43c03f89507be73e7d3e39b3b3794fbb33f3c5
7
+ data.tar.gz: e57218438a1479c2d4974926d4afdf509fa18f2c57ffe0524e116feea919d1fb337d80f883dca30205a442ed754faafafb076bcc780eceb2f86b4c6372a47321
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <p>RubyLLM is the Ruby-native AI framework. Work with models, tools, and agents through one consistent API, in plain Ruby or Rails.</p>
11
11
 
12
- [Website](https://rubyllm.com/) · [Getting Started](https://rubyllm.com/getting-started/) · [What's New in 2.0](https://rubyllm.com/whats-new-in-2-0/)
12
+ [Website](https://rubyllm.com/) · [Getting Started](https://rubyllm.com/getting-started/) · [What's New in 2.1](https://rubyllm.com/whats-new-in-2-1/)
13
13
 
14
14
  Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) - *Fully private work AI*
15
15
 
@@ -26,7 +26,7 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
26
26
 
27
27
  ---
28
28
 
29
- ## 17 providers. One Ruby API.
29
+ ## 19 providers. One Ruby API.
30
30
 
31
31
  Build with the models you want. Move between hosted and local providers without rewriting your application, or connect an OpenAI-compatible endpoint.
32
32
 
@@ -34,7 +34,7 @@ Build with the models you want. Move between hosted and local providers without
34
34
 
35
35
  ## Start with one line. Add files, tools, and agents
36
36
 
37
- These examples use **2.0.0**. Follow [Getting Started](https://rubyllm.com/getting-started/) to install it and configure the providers you want to try. For 1.x, use the [1.x docs](https://rubyllm.com/v1/).
37
+ These examples use **2.1.0**. Follow [Getting Started](https://rubyllm.com/getting-started/) to install it and configure the providers you want to try. For 1.x, use the [1.x docs](https://rubyllm.com/v1/).
38
38
 
39
39
  ```ruby
40
40
  # Just ask
@@ -111,6 +111,15 @@ puts document.markdown
111
111
  RubyLLM.moderate("Some user-generated content").flagged?
112
112
  ```
113
113
 
114
+ ```ruby
115
+ # Ask a typed question about your data
116
+ class Urgency < RubyLLM::Judge
117
+ probability :urgent, "Does this need attention today?"
118
+ end
119
+
120
+ Urgency.judge("Please refund my duplicate charge today.").urgent.probability
121
+ ```
122
+
114
123
  ```ruby
115
124
  # Let AI use your code
116
125
  class Weather < RubyLLM::Tool
@@ -150,9 +159,55 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
150
159
  response.parsed
151
160
  ```
152
161
 
162
+ ## Evaluate your agents
163
+
164
+ Keep questions and expected answers in a dataset. RubyLLM checks correctness by default. Run the same evaluation from Ruby, RSpec, Minitest, or Rake.
165
+
166
+ Evaluations are available on `main`. Follow the [development installation guide](https://rubyllm.com/next/getting-started/) to try them.
167
+
168
+ `app/agents/support_agent.rb`
169
+
170
+ ```ruby
171
+ class SupportAgent < RubyLLM::Agent
172
+ instructions "Accept returns of unopened items within 30 days."
173
+ end
174
+ ```
175
+
176
+ `app/evals/support_evaluation.rb`
177
+
178
+ ```ruby
179
+ class SupportEvaluation < RubyLLM::Evaluation
180
+ def perform(input)
181
+ SupportAgent.new.ask(input)
182
+ end
183
+ end
184
+ ```
185
+
186
+ `app/evals/support_evaluation.yml`
187
+
188
+ ```yaml
189
+ cases:
190
+ - name: within_return_window
191
+ inputs: Can I return an unopened item after 14 days?
192
+ expected_output: Yes, unopened items can be returned within 30 days.
193
+ - name: past_return_window
194
+ inputs: Can I return an unopened item after 45 days?
195
+ expected_output: No, the 30-day return window has passed.
196
+ ```
197
+
198
+ Run the evaluation:
199
+
200
+ ```ruby
201
+ report = SupportEvaluation.run
202
+ report.passed?
203
+ report.cost.total
204
+ ```
205
+
206
+ The model evaluator checks meaning, so answers can use different wording. Declare your own criteria, add Ruby assertions, or choose a configured evaluator agent or judgment model. [Read the evaluation guides](https://rubyllm.com/next/evaluations/).
207
+
153
208
  ## A complete AI framework for Ruby
154
209
 
155
- Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking and Rails integration to bring them into your app.
210
+ Agents, evaluations, workflows, RAG, images, audio, and video. Built in, with usage tracking and Rails integration to bring them into your app.
156
211
 
157
212
  * **Chat:** Conversational AI with `RubyLLM.chat`
158
213
  * **Vision:** Analyze images and videos
@@ -164,11 +219,13 @@ Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking
164
219
  * **Embeddings:** Generate embeddings with `RubyLLM.embed`
165
220
  * **Reranking:** Order retrieval candidates by relevance with `RubyLLM.rerank`
166
221
  * **Moderation:** Content flags, categories, and scores with `RubyLLM.moderate`
222
+ * **Judgments:** Probabilities, choices, and scores with `RubyLLM::Judge` and `RubyLLM.judge`
167
223
  * **Tools:** Let AI call your Ruby methods
168
224
  * **Tool approval:** Park a run until a human approves with `requires_approval`
169
225
  * **The agentic loop:** Drive it yourself with `ask_later`, `step`, and `complete?`
170
226
  * **Provider tools:** Web search, code execution, and MCP connectors with `with_provider_tools`
171
227
  * **Agents:** Reusable assistants with `RubyLLM::Agent`
228
+ * **Evaluations (main):** Reusable datasets, model criteria, and Ruby assertions with [`RubyLLM::Evaluation`](https://rubyllm.com/next/evaluations/)
172
229
  * **Prompt templates:** ERB prompts in `app/prompts`, rendered with `RubyLLM.render_prompt`
173
230
  * **Workflows:** Correlate multi-agent runs in your telemetry with `RubyLLM.workflow`
174
231
  * **Structured output:** Define a Ruby schema and read the result with `response.parsed`
@@ -185,14 +242,14 @@ Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking
185
242
  * **Batches:** Provider-side batch processing with provider-specific discounts via `RubyLLM.batch`
186
243
  * **Compaction:** Let providers condense long conversations with `with_compaction`
187
244
  * **Token counting:** Count a request before you send it with `count_tokens`
188
- * **Providers:** OpenAI, Azure, xAI, Anthropic, Gemini, VertexAI, Bedrock, Cohere, DeepSeek, Mistral, Ollama, Ollama Cloud, OpenRouter, Perplexity, GPUStack, ElevenLabs, Deepgram, and any OpenAI-compatible API
245
+ * **Providers:** OpenAI, Azure, xAI, Anthropic, Gemini, VertexAI, Bedrock, Cohere, DeepSeek, Hetzner, Mistral, Ollama, Ollama Cloud, OpenRouter, Perplexity, GPUStack, ElevenLabs, Deepgram, TypeSafe, and any OpenAI-compatible API
189
246
 
190
247
  ## Installation
191
248
 
192
- Install RubyLLM 2.0:
249
+ Install RubyLLM 2.1:
193
250
 
194
251
  ```bash
195
- bundle add ruby_llm --version 2.0.0
252
+ bundle add ruby_llm --version 2.1.0
196
253
  ```
197
254
 
198
255
  Configure a provider in your script, or in `config/initializers/ruby_llm.rb` in Rails:
@@ -205,7 +262,7 @@ RubyLLM.configure do |config|
205
262
  end
206
263
  ```
207
264
 
208
- Configure the other providers used by the examples as needed: Gemini for files, xAI for video, Mistral for OCR, and Cohere for reranking. [Getting Started](https://rubyllm.com/getting-started/) shows each setup beside its example. If your app uses 1.16, follow the [upgrade guide](https://rubyllm.com/upgrading/) before deploying 2.0.
265
+ Configure the other providers used by the examples as needed: Gemini for files, xAI for video, Mistral for OCR, and Cohere for reranking. [Getting Started](https://rubyllm.com/getting-started/) shows each setup beside its example. Upgrading an existing app? Follow the [upgrade guide](https://rubyllm.com/upgrading/).
209
266
 
210
267
  ## Feels at home in Rails
211
268
 
@@ -149,15 +149,11 @@ module RubyLLM
149
149
  end
150
150
 
151
151
  def postgresql?
152
- ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
153
- rescue StandardError
154
- false
152
+ adapter_built_on?('ActiveRecord::ConnectionAdapters::PostgreSQLAdapter')
155
153
  end
156
154
 
157
155
  def mysql?
158
- ::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
159
- rescue StandardError
160
- false
156
+ adapter_built_on?('ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter')
161
157
  end
162
158
 
163
159
  def table_exists?(table_name)
@@ -202,8 +198,74 @@ module RubyLLM
202
198
  end
203
199
  end
204
200
 
201
+ module ClassMethods # :nodoc:
202
+ def start(given_args = ARGV, config = {})
203
+ super(GeneratorHelpers.reorder_arguments(given_args, class_options), config)
204
+ end
205
+ end
206
+
207
+ def self.included(base)
208
+ base.extend(ClassMethods)
209
+ end
210
+
211
+ def self.reorder_arguments(args, options)
212
+ mappings = []
213
+ switches = []
214
+ index = 0
215
+
216
+ while index < args.length
217
+ argument = args[index]
218
+ return mappings + switches + args[index..] if argument == '--'
219
+
220
+ unless argument.is_a?(String) && argument.start_with?('-')
221
+ mappings << argument
222
+ index += 1
223
+ next
224
+ end
225
+
226
+ switches << argument
227
+ index += 1
228
+ next if argument.include?('=')
229
+
230
+ value = args[index]
231
+ next unless takes_value?(options, argument, value)
232
+
233
+ switches << value
234
+ index += 1
235
+ end
236
+
237
+ mappings + switches
238
+ end
239
+
240
+ def self.takes_value?(options, argument, value)
241
+ return false if value.nil? || value.to_s.start_with?('-')
242
+
243
+ option = option_for(options, argument)
244
+ return false unless option
245
+ return %w[true TRUE t T false FALSE f F].include?(value) if option.type == :boolean
246
+
247
+ !argument.tr('_', '-').match?(/\A--(?:no|skip)-/)
248
+ end
249
+
250
+ def self.option_for(options, argument)
251
+ return option_for(options, "-#{argument[-1]}") if argument.match?(/\A-[a-z]{2,}\z/i)
252
+
253
+ name = argument.sub(/\A--?/, '').tr('-', '_')
254
+ options[name.to_sym] || options[name] ||
255
+ options.values.find { |candidate| candidate.aliases.include?(argument) } ||
256
+ options[name.sub(/\A(?:no|skip)_/, '').to_sym] || options[name.sub(/\A(?:no|skip)_/, '')]
257
+ end
258
+
205
259
  private
206
260
 
261
+ # Compares class names because referencing an adapter constant loads
262
+ # its database driver, which the application may not have installed.
263
+ def adapter_built_on?(adapter_class_name)
264
+ ::ActiveRecord::Base.connection.class.ancestors.any? { |ancestor| ancestor.name == adapter_class_name }
265
+ rescue StandardError
266
+ false
267
+ end
268
+
207
269
  def add_association_params(params, default_assoc, table_name, model_name,
208
270
  owner_table:, owner_model_name:, plural: false)
209
271
  assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
@@ -5,6 +5,7 @@ class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Mig
5
5
  t.string :role, null: false
6
6
  t.text :content
7
7
  t.boolean :cache_until_here, null: false, default: false
8
+ t.string :cache_ttl
8
9
  t.text :thinking_text
9
10
  t.text :thinking_signature
10
11
  <% if postgresql? %>
@@ -47,10 +47,16 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
47
47
  t.boolean :remote, default: false, null: false
48
48
  <% if postgresql? %>
49
49
  t.jsonb :arguments, default: {}
50
+ t.jsonb :mcp_state
51
+ t.jsonb :mcp_result
50
52
  <% elsif mysql? %>
51
53
  t.json :arguments
54
+ t.json :mcp_state
55
+ t.json :mcp_result
52
56
  <% else %>
53
57
  t.json :arguments, default: {}
58
+ t.json :mcp_state
59
+ t.json :mcp_result
54
60
  <% end %>
55
61
  t.timestamps
56
62
 
@@ -60,9 +66,19 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
60
66
  t.index :name
61
67
  end
62
68
 
69
+ create_table :ruby_llm_mcp_credentials, id: :<%= reference_type %> do |t|
70
+ t.references :owner, polymorphic: true, type: :<%= reference_type %>
71
+ t.string :key, null: false
72
+ t.text :data
73
+ t.timestamps
74
+
75
+ t.index :key, unique: true
76
+ end
77
+
63
78
  create_table :ruby_llm_usages, id: :<%= reference_type %> do |t|
64
- t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
79
+ t.references :chat, polymorphic: true, type: :<%= reference_type %>, index: false
65
80
  t.references :message, polymorphic: true, type: :<%= reference_type %>, index: false
81
+ t.references :owner, polymorphic: true, type: :<%= reference_type %>, index: false
66
82
  t.string :operation, null: false
67
83
  t.string :provider, null: false
68
84
  t.string :model, null: false
@@ -72,6 +88,11 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
72
88
  t.integer :cache_read_tokens
73
89
  t.integer :cache_write_tokens
74
90
  t.integer :thinking_tokens
91
+ <% if postgresql? %>
92
+ t.jsonb :server_tool_use
93
+ <% else %>
94
+ t.json :server_tool_use
95
+ <% end %>
75
96
  t.decimal :input_cost, precision: 16, scale: 10
76
97
  t.decimal :output_cost, precision: 16, scale: 10
77
98
  t.decimal :cache_read_cost, precision: 16, scale: 10
@@ -82,9 +103,10 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
82
103
 
83
104
  t.index [ :chat_type, :chat_id ]
84
105
  t.index [ :message_type, :message_id ]
106
+ t.index [ :owner_type, :owner_id ]
85
107
  t.index :status
86
- t.check_constraint "operation IN (<%= usage_operations_sql %>)"
87
- t.check_constraint "status IN (<%= usage_statuses_sql %>)"
108
+ t.check_constraint "operation<%= '::text' if postgresql? %> IN (<%= usage_operations_sql %>)"
109
+ t.check_constraint "status<%= '::text' if postgresql? %> IN (<%= usage_statuses_sql %>)"
88
110
  end
89
111
 
90
112
  create_table :ruby_llm_batches, id: :<%= reference_type %> do |t|
@@ -113,5 +135,16 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
113
135
  t.index [ :provider, :provider_batch_id ], unique: true
114
136
  t.index :status
115
137
  end
138
+
139
+ create_table :ruby_llm_provider_files, id: :<%= reference_type %> do |t|
140
+ t.string :blob_key, null: false
141
+ t.string :provider, null: false
142
+ t.string :account, null: false
143
+ t.text :file_id, null: false
144
+ t.datetime :expires_at
145
+ t.timestamps
146
+
147
+ t.index [ :blob_key, :provider, :account ], unique: true, name: "index_ruby_llm_provider_files_uniqueness"
148
+ end
116
149
  end
117
150
  end
@@ -11,6 +11,9 @@ module RubyLLM
11
11
  # Generates first-party providers and standalone provider gems.
12
12
  class Scaffold # :nodoc: all
13
13
  SUPPORTED_MODES = %w[core gem].freeze
14
+ HASH_ENTRY_LINE = /^\s+'[^']+' => '[^']+',?$/
15
+ ENV_LINE = /^#?\s*[A-Z][A-Z0-9_]*=/
16
+ SPEC_CONFIG_LINE = /^ {6}config\.[a-z0-9_]+ = /
14
17
  SUPPORTED_DIALECTS = %w[chat_completions responses anthropic gemini converse ollama].freeze
15
18
  TEMPLATE_ROOT = File.expand_path('templates', __dir__)
16
19
 
@@ -157,7 +160,7 @@ module RubyLLM
157
160
  path = file_path('lib/ruby_llm.rb')
158
161
  return unless File.exist?(path)
159
162
 
160
- insert_sorted_line(path, " '#{slug}' => '#{class_name}',", /^\s+'[^']+' => '[^']+',?$/)
163
+ insert_sorted_entry(path, " '#{slug}' => '#{class_name}'")
161
164
  insert_sorted_line(
162
165
  path,
163
166
  "RubyLLM::Provider.register :#{slug}, RubyLLM::Providers::#{class_name}",
@@ -169,8 +172,8 @@ module RubyLLM
169
172
  path = file_path('.env.example')
170
173
  return unless File.exist?(path)
171
174
 
172
- append_unique_line(path, "#{api_key_env}=$(op read \"op://RubyLLM/#{class_name}/credential\")")
173
- append_unique_line(path, "#{api_base_env}=#{api_base}")
175
+ insert_sorted_line(path, "#{api_key_env}=$(op read \"op://RubyLLM/#{class_name}/credential\")", ENV_LINE)
176
+ insert_sorted_line(path, "#{api_base_env}=#{api_base}", ENV_LINE)
174
177
  end
175
178
 
176
179
  def update_core_spec_configuration
@@ -178,9 +181,9 @@ module RubyLLM
178
181
  return unless File.exist?(path)
179
182
 
180
183
  insert_sorted_line(path, " config.#{api_base_config} = ENV.fetch('#{api_base_env}', '#{api_base}')",
181
- /^\s+config\.[a-z0-9_]+_api_base =/)
184
+ SPEC_CONFIG_LINE)
182
185
  insert_sorted_line(path, " config.#{api_key_config} = ENV.fetch('#{api_key_env}', 'test')",
183
- /^\s+config\.[a-z0-9_]+_api_key =/)
186
+ SPEC_CONFIG_LINE)
184
187
  end
185
188
 
186
189
  def update_core_vcr_configuration
@@ -196,7 +199,7 @@ module RubyLLM
196
199
  path = file_path('lib/ruby_llm/models.rb')
197
200
  return unless File.exist?(path)
198
201
 
199
- insert_before_first(path, " '#{models_dev_provider}' => '#{slug}',", /^ \}\.freeze$/)
202
+ insert_sorted_entry(path, " '#{models_dev_provider}' => '#{slug}'")
200
203
  end
201
204
 
202
205
  def write_template(template, relative_path, executable: false)
@@ -218,38 +221,45 @@ module RubyLLM
218
221
  record(existed ? :updated : :written, relative_path)
219
222
  end
220
223
 
221
- def append_unique_line(path, line)
222
- content = File.exist?(path) ? File.read(path) : ''
223
- return record(:skipped, relative_path(path)) if content.include?(line)
224
-
225
- FileUtils.mkdir_p(File.dirname(path))
226
- File.write(path, "#{content.chomp}\n#{line}\n")
227
- record(:updated, relative_path(path))
228
- end
229
-
230
224
  def insert_sorted_line(path, line, matcher)
231
225
  content = File.read(path)
232
226
  return record(:skipped, relative_path(path)) if content.include?(line)
233
227
 
234
228
  lines = content.lines
235
- indexes = lines.each_index.select { |index| lines[index].match?(matcher) }
236
- insert_at = indexes.find { |index| line < lines[index].chomp } || indexes.last&.+(1) || lines.length
237
- lines.insert(insert_at, "#{line}\n")
229
+ lines.insert(sorted_line_index(lines, line, matcher), "#{line}\n")
238
230
  File.write(path, lines.join)
239
231
  record(:updated, relative_path(path))
240
232
  end
241
233
 
242
- def insert_before_first(path, line, matcher)
234
+ # Lands after the last smaller line, above any comment that describes the next one.
235
+ def sorted_line_index(lines, line, matcher)
236
+ indexes = lines.each_index.select { |index| lines[index].match?(matcher) }
237
+ index = indexes.find { |candidate| line < lines[candidate].chomp } || indexes.last&.+(1) || lines.length
238
+ index -= 1 while index.positive? && lines[index - 1].strip.start_with?('#')
239
+ index
240
+ end
241
+
242
+ # Inserts a `'key' => 'value'` entry into the first multiline hash literal of
243
+ # such entries, keeping it sorted and every entry but the last comma-terminated.
244
+ def insert_sorted_entry(path, entry)
243
245
  content = File.read(path)
244
- return record(:skipped, relative_path(path)) if content.include?(line)
246
+ return record(:skipped, relative_path(path)) if content.match?(/^#{Regexp.escape(entry)},?$/)
245
247
 
246
248
  lines = content.lines
247
- insert_at = lines.index { |candidate| candidate.match?(matcher) } || lines.length
248
- lines.insert(insert_at, "#{line}\n")
249
+ block = hash_entry_block(lines)
250
+ insert_at = block.find { |index| entry < lines[index].chomp.delete_suffix(',') } || (block.last + 1)
251
+ lines.insert(insert_at, "#{entry}\n")
252
+ hash_entry_block(lines)[0...-1].each { |index| lines[index] = "#{lines[index].chomp.delete_suffix(',')},\n" }
249
253
  File.write(path, lines.join)
250
254
  record(:updated, relative_path(path))
251
255
  end
252
256
 
257
+ def hash_entry_block(lines)
258
+ first = lines.index { |line| line.match?(HASH_ENTRY_LINE) }
259
+ last = (first...lines.length).take_while { |index| lines[index].match?(HASH_ENTRY_LINE) }.last
260
+ (first..last).to_a
261
+ end
262
+
253
263
  def record(action, path)
254
264
  @result.public_send(action) << path
255
265
  @result.actions << [action, path]
@@ -16,7 +16,7 @@ jobs:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- ruby-version: ["3.1", "3.2", "3.3", "3.4", "4.0"]
19
+ ruby-version: ["3.2", "3.3", "3.4", "4.0"]
20
20
  steps:
21
21
  - uses: actions/checkout@v6
22
22
 
@@ -26,7 +26,3 @@ jobs:
26
26
  bundler-cache: true
27
27
 
28
28
  - run: bundle exec rake
29
- if: matrix.ruby-version != '3.1'
30
-
31
- - run: bundle exec rake rubocop flay spec
32
- if: matrix.ruby-version == '3.1'
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development do
8
- gem 'archspec' if RUBY_ENGINE == 'ruby' && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
8
+ gem 'archspec' if RUBY_ENGINE == 'ruby'
9
9
  gem 'bundler', '>= 2.0'
10
10
  gem 'dotenv'
11
11
  gem 'flay'
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = 'Adds <%= class_name %> provider support to RubyLLM.'
11
11
  spec.homepage = 'https://github.com/<%= github_owner %>/<%= gem_name %>'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = '>= 3.1'
13
+ spec.required_ruby_version = '>= 3.2'
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = spec.homepage
@@ -5,7 +5,7 @@ plugins:
5
5
 
6
6
  AllCops:
7
7
  NewCops: enable
8
- TargetRubyVersion: 3.1
8
+ TargetRubyVersion: 3.2
9
9
  Exclude:
10
10
  - Archspec.rb
11
11
  - vendor/**/*
@@ -0,0 +1,78 @@
1
+ class <%= 'upgrade_ruby_llm_to_2_1'.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ unless table_exists?(:ruby_llm_mcp_credentials)
4
+ create_table :ruby_llm_mcp_credentials, id: :<%= reference_type %> do |t|
5
+ t.references :owner, polymorphic: true, type: :<%= reference_type %>
6
+ t.string :key, null: false
7
+ t.text :data
8
+ t.timestamps
9
+
10
+ t.index :key, unique: true
11
+ end
12
+ end
13
+
14
+ unless column_exists?(:ruby_llm_tool_calls, :mcp_state)
15
+ if column_exists?(:ruby_llm_tool_calls, :pending_input)
16
+ rename_column :ruby_llm_tool_calls, :pending_input, :mcp_state
17
+ else
18
+ <% if postgresql? -%>
19
+ add_column :ruby_llm_tool_calls, :mcp_state, :jsonb
20
+ <% else -%>
21
+ add_column :ruby_llm_tool_calls, :mcp_state, :json
22
+ <% end -%>
23
+ end
24
+ end
25
+
26
+ unless column_exists?(:ruby_llm_tool_calls, :mcp_result)
27
+ <% if postgresql? -%>
28
+ add_column :ruby_llm_tool_calls, :mcp_result, :jsonb
29
+ <% else -%>
30
+ add_column :ruby_llm_tool_calls, :mcp_result, :json
31
+ <% end -%>
32
+ end
33
+
34
+ unless column_exists?(:ruby_llm_usages, :server_tool_use)
35
+ <% if postgresql? -%>
36
+ add_column :ruby_llm_usages, :server_tool_use, :jsonb
37
+ <% else -%>
38
+ add_column :ruby_llm_usages, :server_tool_use, :json
39
+ <% end -%>
40
+ end
41
+
42
+ if columns(:ruby_llm_usages).any? { |column| column.name == "chat_id" && !column.null }
43
+ change_column_null :ruby_llm_usages, :chat_type, true
44
+ change_column_null :ruby_llm_usages, :chat_id, true
45
+ end
46
+
47
+ unless column_exists?(:ruby_llm_usages, :owner_id)
48
+ add_reference :ruby_llm_usages, :owner, polymorphic: true, type: :<%= reference_type %>, index: true
49
+ end
50
+
51
+ reversible do |direction|
52
+ direction.up do
53
+ operations = check_constraints(:ruby_llm_usages).find { |constraint| constraint.expression.include?("operation") }
54
+ if operations && [<%= usage_operations_sql %>].any? { |operation| !operations.expression.include?("'#{operation}'") }
55
+ remove_check_constraint :ruby_llm_usages, name: operations.name
56
+ add_check_constraint :ruby_llm_usages, "operation<%= '::text' if postgresql? %> IN (<%= usage_operations_sql %>)"
57
+ end
58
+ end
59
+ end
60
+
61
+ unless column_exists?(:<%= message_table_name %>, :cache_ttl)
62
+ add_column :<%= message_table_name %>, :cache_ttl, :string
63
+ end
64
+
65
+ unless table_exists?(:ruby_llm_provider_files)
66
+ create_table :ruby_llm_provider_files, id: :<%= reference_type %> do |t|
67
+ t.string :blob_key, null: false
68
+ t.string :provider, null: false
69
+ t.string :account, null: false
70
+ t.text :file_id, null: false
71
+ t.datetime :expires_at
72
+ t.timestamps
73
+
74
+ t.index [ :blob_key, :provider, :account ], unique: true, name: "index_ruby_llm_provider_files_uniqueness"
75
+ end
76
+ end
77
+ end
78
+ end