ruby_llm 1.16.0 → 2.0.0.rc4

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 (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +242 -54
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +662 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +126 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLMUpgrade # :nodoc: all
4
+ TABLE = 'ruby_llm_v2_upgrades'
5
+ VERSION_COLUMN = 'ruby_llm_version'
6
+ OPERATIONS = %i[ask say ask_later add_message generate compact step complete run_tools cancel resume
7
+ approve deny with_model with_instructions cache_until_here reset_messages!].freeze
8
+ READ_OPERATIONS = %i[reload to_llm count_tokens].freeze
9
+
10
+ module Records
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ class_attribute :ruby_llm_upgrade_kind, instance_writer: false
15
+ self.enumerate_columns_in_select_statements = true
16
+ default_scope { RubyLLMUpgrade.visible(self) }
17
+ after_initialize { @ruby_llm_upgrade_epoch = RubyLLMUpgrade.epoch }
18
+ around_save :guard_ruby_llm_upgrade
19
+ around_destroy :guard_ruby_llm_upgrade, prepend: true
20
+ before_save { RubyLLMUpgrade.prepare_model(self) if ruby_llm_upgrade_kind == :chat }
21
+ end
22
+
23
+ private
24
+
25
+ def guard_ruby_llm_upgrade(&)
26
+ RubyLLMUpgrade.guard(self, &)
27
+ end
28
+ end
29
+
30
+ class << self
31
+ def install(chat:, message:, tool_call:)
32
+ if version == 2 && message.table_exists? && message.column_names.include?('ruby_llm_content')
33
+ message.alias_attribute :content, :ruby_llm_content
34
+ end
35
+ { chat => :chat, message => :message, tool_call => :tool_call }.each do |model, kind|
36
+ next if model < Records
37
+
38
+ model.include Records
39
+ model.ruby_llm_upgrade_kind = kind
40
+ model.prepend guarded_operations(model, READ_OPERATIONS, claim: false)
41
+ end
42
+ return if chat.instance_variable_defined?(:@ruby_llm_upgrade_operations)
43
+
44
+ operations = guarded_operations(chat, OPERATIONS, claim: true)
45
+ %i[persist_usage_entry persist_new_message persist_message_completion].each do |name|
46
+ next unless chat.private_method_defined?(name)
47
+
48
+ operations.define_method(name) do |*args, **options, &block|
49
+ RubyLLMUpgrade.guard(self) { super(*args, **options, &block) }
50
+ end
51
+ operations.send(:private, name)
52
+ end
53
+ chat.prepend operations
54
+ chat.instance_variable_set(:@ruby_llm_upgrade_operations, operations)
55
+ end
56
+
57
+ def visible(model)
58
+ return model.where(nil) unless available?
59
+
60
+ state = current_state
61
+ check_version(state)
62
+ return model.where(nil) if version == 2
63
+
64
+ settings = state.settings
65
+ return model.where(nil) unless ownership_column?(settings)
66
+
67
+ chats = records(settings.fetch('chat_table')).where(VERSION_COLUMN => 1)
68
+ case model.ruby_llm_upgrade_kind
69
+ when :chat
70
+ model.where(VERSION_COLUMN => 1)
71
+ when :message
72
+ model.where(settings.fetch('chat_foreign_key') => chats.select(chats.primary_key))
73
+ when :tool_call
74
+ messages = records(settings.fetch('message_table'))
75
+ .where(settings.fetch('chat_foreign_key') => chats.select(chats.primary_key))
76
+ model.where(settings.fetch('message_foreign_key') => messages.select(messages.primary_key))
77
+ end
78
+ end
79
+
80
+ def prepare_model(chat)
81
+ return unless available?
82
+
83
+ state = current_state
84
+ settings = state.settings
85
+ return if version == 1 && settings['online'] && state.status == 'preparing'
86
+ if version == 1
87
+ return unless ActiveRecord::Base.connection.column_exists?(settings.fetch('chat_table'), :ruby_llm_model_id)
88
+ return unless ActiveRecord::Base.connection.table_exists?('ruby_llm_models')
89
+ end
90
+ ensure_model_reference(chat, settings)
91
+ end
92
+
93
+ def guard(record, claim: true)
94
+ return yield unless available?
95
+
96
+ states = records(TABLE)
97
+ states.uncached do
98
+ states.transaction do
99
+ if ActiveRecord::Base.connection.adapter_name == 'SQLite' && claim
100
+ states.update_all('epoch = epoch')
101
+ end
102
+ state = states.lock(lock_clause).first!
103
+ check_version(state)
104
+ protect(record, state, claim: claim)
105
+ if record.instance_variable_get(:@ruby_llm_upgrade_epoch) != state[:epoch].to_i
106
+ raise ActiveRecord::ReadOnlyRecord, 'Reload the conversation after switching RubyLLM versions'
107
+ end
108
+ yield
109
+ end
110
+ end
111
+ end
112
+
113
+ def epoch
114
+ available? ? current_state[:epoch].to_i : 0
115
+ end
116
+
117
+ private
118
+
119
+ def lock_clause
120
+ ActiveRecord::Base.connection.adapter_name == 'Mysql2' ? 'LOCK IN SHARE MODE' : 'FOR SHARE'
121
+ end
122
+
123
+ def guarded_operations(model, names, claim:)
124
+ Module.new do
125
+ names.each do |name|
126
+ next unless model.method_defined?(name)
127
+
128
+ define_method(name) do |*args, **options, &block|
129
+ RubyLLMUpgrade.guard(self, claim: claim) { nil }
130
+ super(*args, **options, &block)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def protect(record, state, claim:)
137
+ return if state.status == 'finalized'
138
+
139
+ settings = state.settings
140
+ return if version == 1 && !ownership_column?(settings)
141
+
142
+ owners(record, settings).each { |chat| protect_owner(chat, claim: claim) }
143
+ return unless claim && version == 2 && record.class.ruby_llm_upgrade_kind == :chat
144
+
145
+ record[VERSION_COLUMN] = 2
146
+ end
147
+
148
+ def protect_owner(chat, claim:)
149
+ chat.lock!
150
+ if version == 1 && chat[VERSION_COLUMN] != 1
151
+ raise ActiveRecord::ReadOnlyRecord, 'This conversation contains RubyLLM 2.0 changes'
152
+ end
153
+
154
+ chat.update!(VERSION_COLUMN => 2) if claim && version == 2 && chat[VERSION_COLUMN] != 2
155
+ end
156
+
157
+ def version
158
+ Gem::Version.new(RubyLLM::VERSION).segments.first >= 2 ? 2 : 1
159
+ end
160
+
161
+ def available?
162
+ ActiveRecord::Base.connection.table_exists?(TABLE)
163
+ end
164
+
165
+ def records(table)
166
+ @records ||= {}
167
+ model = @records[table] ||= Class.new(ActiveRecord::Base) do
168
+ self.table_name = table
169
+ self.inheritance_column = :_type_disabled
170
+ end
171
+ if !model.column_names.include?(VERSION_COLUMN) &&
172
+ ActiveRecord::Base.connection.column_exists?(table, VERSION_COLUMN)
173
+ model.reset_column_information
174
+ end
175
+ model
176
+ end
177
+
178
+ def current_state
179
+ records(TABLE).uncached { records(TABLE).first! }
180
+ end
181
+
182
+ def ownership_column?(settings)
183
+ ActiveRecord::Base.connection.column_exists?(settings.fetch('chat_table'), VERSION_COLUMN)
184
+ end
185
+
186
+ def check_version(state)
187
+ return if version == 1 && state.active_version == 1 && state.settings['online'] && state.status == 'preparing'
188
+ return if state.active_version == version && %w[active finalized].include?(state.status)
189
+
190
+ raise ActiveRecord::ReadOnlyRecord,
191
+ "RubyLLM #{version} cannot access conversations while the copy upgrade is " \
192
+ "#{state.status} on #{state.active_version}"
193
+ end
194
+
195
+ def ensure_model_reference(chat, settings)
196
+ if version == 1
197
+ legacy_id = chat[settings.fetch('model_foreign_key')]
198
+ legacy_models = records(settings.fetch('model_table'))
199
+ legacy = legacy_models.find_by(legacy_models.primary_key => legacy_id)
200
+ unless legacy
201
+ chat.errors.add(:model, 'must identify an existing legacy model')
202
+ raise ActiveRecord::RecordInvalid, chat
203
+ end
204
+
205
+ target = records('ruby_llm_models')
206
+ model = target.find_by(provider: legacy.provider, model_id: legacy.model_id) ||
207
+ target.create!(legacy.attributes.slice(*target.column_names).except(target.primary_key))
208
+ chat[:ruby_llm_model_id] = model.id
209
+ end
210
+ return if chat[:ruby_llm_model_id]
211
+
212
+ chat.errors.add(:model, 'must exist before saving the conversation')
213
+ raise ActiveRecord::RecordInvalid, chat
214
+ end
215
+
216
+ def owners(record, settings)
217
+ chats = records(settings.fetch('chat_table'))
218
+ return chat_owner(record, chats) if record.class.ruby_llm_upgrade_kind == :chat
219
+
220
+ values = owner_attributes(record, settings)
221
+ ids = values.filter_map { |value| value[settings.fetch('chat_foreign_key')] }
222
+ raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists' if ids.empty?
223
+
224
+ found = chats.where(chats.primary_key => ids).to_a
225
+ raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists' unless found.length == ids.uniq.length
226
+
227
+ found
228
+ end
229
+
230
+ def chat_owner(record, chats)
231
+ found = chats.where(chats.primary_key => record.id).to_a
232
+ return found unless record.persisted? && found.empty?
233
+
234
+ raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists'
235
+ end
236
+
237
+ def owner_attributes(record, settings)
238
+ values = [record.attributes]
239
+ if record.persisted?
240
+ stored = records(record.class.table_name).find(record.id)
241
+ values << stored.attributes
242
+ end
243
+ if record.class.ruby_llm_upgrade_kind == :tool_call
244
+ message_key = version == 1 ? settings.fetch('message_foreign_key') : 'message_id'
245
+ messages = records(settings.fetch('message_table'))
246
+ values = messages.where(messages.primary_key => values.map { |value| value[message_key] }).map(&:attributes)
247
+ end
248
+ values
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require Rails.root.join('app/models/concerns/ruby_llm_upgrade')
4
+
5
+ Rails.application.config.to_prepare do
6
+ require 'active_record/base'
7
+ tool_call = if Gem::Version.new(RubyLLM::VERSION).segments.first >= 2
8
+ RubyLLM::ActiveRecord::ToolCall
9
+ else
10
+ <%= v1_model_name(:tool_call) %>
11
+ end
12
+ RubyLLMUpgrade.install(chat: <%= chat_model_name %>, message: <%= message_model_name %>, tool_call: tool_call)
13
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ module Generators
9
+ # Upgrades the schema generated by RubyLLM 1.16 to RubyLLM 2.0.
10
+ class UpgradeGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ MODEL_MAPPING_DEFAULTS = {
15
+ chat: 'Chat',
16
+ message: 'Message',
17
+ model: 'Model',
18
+ tool_call: 'ToolCall'
19
+ }.freeze
20
+ MODEL_MAPPING_TYPES = MODEL_MAPPING_DEFAULTS.keys.map(&:to_s).freeze
21
+
22
+ namespace 'ruby_llm:upgrade'
23
+ source_root File.expand_path('templates', __dir__)
24
+
25
+ class_option :phase, type: :string, enum: %w[prepare backfill finish cleanup],
26
+ desc: 'Generate one upgrade phase; cleanup runs in a later deployment'
27
+ class_option :mode, type: :string, enum: %w[rename copy], default: 'rename',
28
+ desc: 'Copy prepares and backfills online, with protected 1.16 rollback until cleanup'
29
+ class_option :discard_incomplete_tool_calls, type: :boolean, default: false,
30
+ desc: 'Discard all incomplete legacy tool calls at copy finish'
31
+
32
+ argument :model_mappings,
33
+ type: :array,
34
+ default: [],
35
+ banner: 'chat:ChatName message:MessageName model:ModelName tool_call:ToolCallName'
36
+
37
+ desc 'Upgrades a RubyLLM 1.16 Rails schema to 2.0'
38
+
39
+ def self.next_migration_number(dirname)
40
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
41
+ end
42
+
43
+ def create_migration_files
44
+ validate_cleanup_options
45
+ parse_model_mappings(allowed_types: MODEL_MAPPING_TYPES, defaults: MODEL_MAPPING_DEFAULTS)
46
+ say_status :models, resolved_model_mappings
47
+
48
+ if copy_mode? && !options[:phase].in?(%w[backfill finish cleanup])
49
+ template 'ruby_llm_upgrade.rb.tt', 'app/models/concerns/ruby_llm_upgrade.rb'
50
+ template 'upgrade_initializer.rb.tt', 'config/initializers/ruby_llm_upgrade.rb'
51
+ end
52
+
53
+ phases = options[:phase] ? [options[:phase]] : %w[prepare backfill finish]
54
+ phases.each do |phase|
55
+ template = phase == 'backfill' ? 'backfill_v2_data' : "#{phase}_v2_upgrade"
56
+ migration = phase == 'backfill' ? 'backfill_ruby_llm_v2_data' : "#{phase}_ruby_llm_v2_upgrade"
57
+ migration_template "#{template}.rb.tt", "db/migrate/#{migration}.rb"
58
+ end
59
+ end
60
+
61
+ def show_next_steps
62
+ say_status :success, 'Upgrade prepared!', :green
63
+ if copy_mode?
64
+ say <<~INSTRUCTIONS
65
+
66
+ Copy mode retains the legacy tables and a separate chat model reference.
67
+ Install the generated RubyLLMUpgrade concern and initializer in BOTH the
68
+ running 1.16 build and the 2.0 build BEFORE running prepare. Restart
69
+ all affected 1.16 processes so they load the guards and explicit column
70
+ selects. The guards protect whole conversations
71
+ changed by 2.0. Direct SQL, bulk updates/deletes and attachment purges
72
+ bypass these guards; review those application paths before upgrading.
73
+
74
+ Rehearse prepare, backfill and finish on a production database copy.
75
+ Run prepare and backfill from the 2.0 build while 1.16 serves traffic.
76
+ Stop affected traffic, workers, scheduled jobs and retries BEFORE finish.
77
+ Finish catches up intervening writes, validates and activates 2.0.
78
+ Then load models and restart the 2.0 application.
79
+ Keep the old model/tool-call classes in the 1.16 build.
80
+
81
+ PostgreSQL, MySQL and SQLite support this workflow. Schema changes can
82
+ block writes; SQLite also serializes backfill and app writes.
83
+ Use a direct connection or session-mode pool for copy migrations.
84
+ db:migrate runs ALL pending phases. Stop at the backfill timestamp to
85
+ defer finish, or pause AI before running all three together.
86
+
87
+ With all affected processes stopped, use the 2.0 build to run:
88
+ bin/rails ruby_llm:upgrade:rollback # activates the protected 1.16 view
89
+ bin/rails ruby_llm:upgrade:resume # reconciles 1.16 writes, activates 2.0
90
+
91
+ Finish pending tool calls, approvals and batches before switching.
92
+ Once rollback is no longer needed, run ruby_llm:upgrade:finalize, then
93
+ generate --mode copy --phase cleanup in a later deployment. Keep the
94
+ compatibility files until cleanup succeeds; remove them afterward.
95
+
96
+ Pass the same --mode and model mappings to every generator invocation.
97
+ Full guide: https://rubyllm.com/next/upgrading/
98
+
99
+ INSTRUCTIONS
100
+ return
101
+ end
102
+ say <<~INSTRUCTIONS
103
+
104
+ These migrations expect the schema generated by RubyLLM 1.16. They move
105
+ RubyLLM's model and tool-call records, preserve message content and usage,
106
+ and retain the 1.x message columns until a later cleanup deployment.
107
+
108
+ Next steps:
109
+ 1. Review the generated migrations
110
+ 2. Rehearse them on a recent production snapshot
111
+ 3. Stop affected web processes, workers, scheduled jobs, and retries
112
+ 4. Run preparation, backfill, and finish before reopening traffic
113
+ 5. Run: bin/rails ruby_llm:load_models
114
+ 6. Keep only the current declarations in your application models:
115
+ #{chat_model_path}: #{acts_as_chat_declaration}
116
+ #{message_model_path}: #{acts_as_message_declaration}
117
+ 7. Remove the 1.x model and tool-call classes, config.model_registry_class,
118
+ and config.use_new_acts_as
119
+ 8. Update 1.x API calls using the upgrade guide
120
+
121
+ Generate one phase with --phase prepare, backfill, or finish when the
122
+ backfill needs to run outside your deployment's release phase.
123
+ After verifying the upgrade, generate --phase cleanup in a later release.
124
+ Pass the same model mappings to every invocation.
125
+
126
+ The migrations are irreversible. Retained columns do not make a code
127
+ rollback safe; recovery requires the previous database and application.
128
+
129
+ Full upgrade guide:
130
+ https://rubyllm.com/next/upgrading/
131
+
132
+ INSTRUCTIONS
133
+ end
134
+
135
+ private
136
+
137
+ def copy_mode? = options[:mode] == 'copy'
138
+ def discard_incomplete_tool_calls? = options[:discard_incomplete_tool_calls]
139
+
140
+ def validate_cleanup_options
141
+ return unless discard_incomplete_tool_calls?
142
+ return if copy_mode? && options[:phase].in?([nil, 'finish'])
143
+
144
+ raise Thor::Error, 'Discard incomplete tool calls only with --mode copy and the finish phase'
145
+ end
146
+
147
+ def copy_upgrade_settings
148
+ {
149
+ chat_table: chat_table_name, message_table: message_table_name,
150
+ model_table: v1_model_table_name, tool_call_table: v1_tool_call_table_name,
151
+ chat_class: chat_model_name, message_class: message_model_name,
152
+ model_foreign_key: v1_model_foreign_key, tool_call_foreign_key: v1_tool_call_foreign_key,
153
+ chat_foreign_key: chat_foreign_key, message_foreign_key: message_foreign_key
154
+ }
155
+ end
156
+
157
+ def prepare_migration_class_name = 'prepare_ruby_llm_v2_upgrade'.camelize
158
+ def backfill_migration_class_name = 'backfill_ruby_llm_v2_data'.camelize
159
+ def finish_migration_class_name = 'finish_ruby_llm_v2_upgrade'.camelize
160
+ def cleanup_migration_class_name = 'cleanup_ruby_llm_v2_upgrade'.camelize
161
+
162
+ def resolved_model_mappings
163
+ @model_names.map do |type, model_name|
164
+ "#{type}:#{model_name} -> #{table_name_for(model_name)}"
165
+ end.join(', ')
166
+ end
167
+
168
+ def v1_model_name(type)
169
+ @model_names.fetch(type)
170
+ end
171
+
172
+ def v1_model_table_name = table_name_for(v1_model_name(:model))
173
+ def v1_tool_call_table_name = table_name_for(v1_model_name(:tool_call))
174
+ def v1_model_foreign_key = "#{v1_model_table_name.singularize}_id"
175
+ def v1_tool_call_foreign_key = "#{v1_tool_call_table_name.singularize}_id"
176
+ def message_foreign_key = "#{message_table_name.singularize}_id"
177
+ def chat_foreign_key = "#{chat_table_name.singularize}_id"
178
+
179
+ def chat_model_path
180
+ "app/models/#{chat_model_name.underscore}.rb"
181
+ end
182
+
183
+ def message_model_path
184
+ "app/models/#{message_model_name.underscore}.rb"
185
+ end
186
+ end
187
+ end
188
+ end