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,662 @@
1
+ # frozen_string_literal: true
2
+ <% if copy_mode? -%>
3
+
4
+ require 'generators/ruby_llm/upgrade/upgrade_migration'
5
+ <% end -%>
6
+
7
+ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
8
+ disable_ddl_transaction!
9
+
10
+ def up
11
+ <% if copy_mode? -%>
12
+ RubyLLM::Generators::UpgradeMigration.for.prepare(<%= copy_upgrade_settings.inspect %>) { prepare_schema }
13
+ <% else -%>
14
+ raise 'This database uses --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
15
+ prepare_schema
16
+ <% end -%>
17
+ end
18
+
19
+ def down
20
+ raise ActiveRecord::IrreversibleMigration, 'RubyLLM 2.0 changes persisted record ownership'
21
+ end
22
+
23
+ private
24
+
25
+ def prepare_schema
26
+ validate_upgrade
27
+ move_models
28
+ add_chat_and_message_columns
29
+ move_tool_calls
30
+ create_usages
31
+ create_batches
32
+ end
33
+
34
+ def validate_upgrade
35
+ %i[<%= chat_table_name %> <%= message_table_name %>].each do |table|
36
+ raise "Expected #{table} to exist before upgrading RubyLLM" unless table_exists?(table)
37
+ end
38
+ validate_legacy_message_columns
39
+ validate_table_move(:<%= v1_model_table_name %>, :ruby_llm_models)
40
+ validate_table_move(:<%= v1_tool_call_table_name %>, :ruby_llm_tool_calls)
41
+
42
+ models = existing_table(:<%= v1_model_table_name %>, :ruby_llm_models)
43
+ raise 'Expected the RubyLLM 1.16 model table to exist' unless models
44
+
45
+ validate_table_shape(models, %i[model_id name provider], 'model')
46
+ validate_unique_values(models, %i[provider model_id])
47
+ validate_chat_model_references(models)
48
+
49
+ tool_calls = existing_table(:<%= v1_tool_call_table_name %>, :ruby_llm_tool_calls)
50
+ raise 'Expected the RubyLLM 1.16 tool-call table to exist' unless tool_calls
51
+
52
+ validate_table_shape(tool_calls, %i[tool_call_id name arguments created_at updated_at], 'tool-call')
53
+ <% if copy_mode? -%>
54
+ validate_unique_values(tool_calls, %i[tool_call_id])
55
+ <% end -%>
56
+ unless column_exists?(tool_calls, :<%= message_foreign_key %>) || column_exists?(tool_calls, :message_id)
57
+ raise "#{tool_calls} does not have a message foreign key"
58
+ end
59
+ if migration_record(tool_calls).where(tool_call_id: nil).exists?
60
+ raise "#{tool_calls} contains a NULL tool_call_id. Set it before retrying."
61
+ end
62
+ validate_unique_tool_results
63
+
64
+ validate_usage_identities(models)
65
+ validate_existing_table(
66
+ :ruby_llm_usages,
67
+ %i[
68
+ chat_type chat_id message_type message_id operation provider model status
69
+ input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens
70
+ input_cost output_cost cache_read_cost cache_write_cost thinking_cost total_cost created_at updated_at
71
+ ],
72
+ 'usage'
73
+ )
74
+ validate_existing_table(
75
+ :ruby_llm_batches,
76
+ %i[
77
+ provider_batch_id provider status raw_status completed chat_type batch_protocol
78
+ chat_ids request_counts created_at updated_at
79
+ ],
80
+ 'batch'
81
+ )
82
+ validate_unique_values(:ruby_llm_batches, %i[provider provider_batch_id]) if table_exists?(:ruby_llm_batches)
83
+ end
84
+
85
+ # A schema the finish migration already cleaned has none of these columns;
86
+ # running the backfill again there would invent usage rows.
87
+ def validate_legacy_message_columns
88
+ if table_exists?(:ruby_llm_v2_backfills) &&
89
+ migration_record(:ruby_llm_v2_backfills).where(task: 'finished', completed: true).exists?
90
+ raise 'The RubyLLM 2.0 upgrade is already finished. Generate --phase cleanup to remove the legacy columns.'
91
+ end
92
+
93
+ legacy_columns = %i[content_raw input_tokens output_tokens <%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>]
94
+ return if legacy_columns.any? { |column| column_exists?(:<%= message_table_name %>, column) }
95
+ return unless table_exists?(:ruby_llm_usages)
96
+
97
+ raise '<%= message_table_name %> already has the RubyLLM 2.0 schema. ' \
98
+ 'Remove the generated upgrade migrations instead of running them again.'
99
+ end
100
+
101
+ def validate_table_move(source, target)
102
+ <% if copy_mode? -%>
103
+ raise 'Copy mode needs distinct legacy and RubyLLM table names' if source == target
104
+ raise "Expected legacy table #{source}" unless table_exists?(source)
105
+ <% else -%>
106
+ return if source == target || !table_exists?(source) || !table_exists?(target)
107
+
108
+ raise "Both #{source} and #{target} exist. Merge or remove one before running this migration."
109
+ <% end -%>
110
+ end
111
+
112
+ def existing_table(source, target)
113
+ return source if table_exists?(source)
114
+
115
+ target if table_exists?(target)
116
+ end
117
+
118
+ def validate_existing_table(table, columns, label)
119
+ validate_table_shape(table, columns, label) if table_exists?(table)
120
+ end
121
+
122
+ def validate_table_shape(table, columns, label)
123
+ primary_key = connection.primary_key(table)
124
+ unless primary_key.is_a?(String) && column_exists?(table, primary_key)
125
+ raise "#{table} does not look like a RubyLLM #{label} table; expected one primary key column"
126
+ end
127
+
128
+ missing = columns.reject { |column| column_exists?(table, column) }
129
+ return if missing.empty?
130
+
131
+ raise "#{table} does not look like a RubyLLM #{label} table; missing columns: #{missing.join(', ')}"
132
+ end
133
+
134
+ def validate_unique_values(table, columns)
135
+ duplicates = migration_record(table).group(*columns).having('COUNT(*) > 1').limit(5).pluck(*columns)
136
+ return if duplicates.empty?
137
+
138
+ raise "Cannot add a unique index to #{table} on #{columns.join(', ')}; " \
139
+ "duplicate values include #{duplicates.inspect}. Reconcile them before retrying."
140
+ end
141
+
142
+ def validate_chat_model_references(models)
143
+ chats = :<%= chat_table_name %>
144
+ model_column = first_existing_column(chats, :<%= v1_model_foreign_key %>, :ruby_llm_model_id)
145
+ return unless model_column
146
+
147
+ ensure_reference_types_match(chats, model_column, models)
148
+ orphan = select_value(<<~SQL)
149
+ SELECT legacy_chats.#{quoted_primary_key(chats)}
150
+ FROM #{quote_table(chats)} legacy_chats
151
+ LEFT JOIN #{quote_table(models)} legacy_models
152
+ ON legacy_chats.#{quote_column(model_column)} = legacy_models.#{quoted_primary_key(models)}
153
+ WHERE legacy_chats.#{quote_column(model_column)} IS NOT NULL
154
+ AND legacy_models.#{quoted_primary_key(models)} IS NULL
155
+ LIMIT 1
156
+ SQL
157
+ return unless orphan
158
+
159
+ raise "Could not find #{models} record for #{chats} id #{orphan}. Repair its model reference before retrying."
160
+ end
161
+
162
+ def validate_usage_identities(models)
163
+ conditions = legacy_usage_conditions
164
+ return if conditions.empty?
165
+
166
+ joins, provider, model = usage_identity_sql(models)
167
+ message_id = "legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}"
168
+ missing = select_value(<<~SQL)
169
+ SELECT #{message_id}
170
+ FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
171
+ #{joins}
172
+ WHERE (#{conditions.join(' OR ')})
173
+ AND (#{provider} IS NULL OR #{model} IS NULL)
174
+ LIMIT 1
175
+ SQL
176
+ return unless missing
177
+
178
+ raise "Could not determine provider and model for <%= message_table_name %> id #{missing}. " \
179
+ 'Set its model reference, or its chat model reference, before retrying.'
180
+ end
181
+
182
+ def validate_unique_tool_results
183
+ column = :<%= v1_tool_call_foreign_key %>
184
+ return unless column_exists?(:<%= message_table_name %>, column)
185
+
186
+ duplicates = migration_record(:<%= message_table_name %>)
187
+ .where.not(column => nil)
188
+ .group(column)
189
+ .having('COUNT(*) > 1')
190
+ .limit(5)
191
+ .pluck(column)
192
+ return if duplicates.empty?
193
+
194
+ raise "Multiple messages reference the same tool call: #{duplicates.inspect}. " \
195
+ 'Keep one result message per tool call before retrying.'
196
+ end
197
+
198
+ def move_models
199
+ move_table(:<%= v1_model_table_name %>, :ruby_llm_models)
200
+ <% if postgresql? -%>
201
+ normalize_postgresql_json_columns(:ruby_llm_models, %i[modalities capabilities pricing metadata])
202
+ <% end -%>
203
+ <% unless mysql? -%>
204
+ normalize_json_defaults(:ruby_llm_models, modalities: {}, capabilities: [], pricing: {}, metadata: {})
205
+ <% end -%>
206
+ with_upgrade_safety do
207
+ change_table :ruby_llm_models, bulk: true do |table|
208
+ table.string :family unless column_exists?(:ruby_llm_models, :family)
209
+ table.datetime :model_created_at unless column_exists?(:ruby_llm_models, :model_created_at)
210
+ table.integer :context_window unless column_exists?(:ruby_llm_models, :context_window)
211
+ table.integer :max_output_tokens unless column_exists?(:ruby_llm_models, :max_output_tokens)
212
+ table.date :knowledge_cutoff unless column_exists?(:ruby_llm_models, :knowledge_cutoff)
213
+ table.datetime :unlisted_at unless column_exists?(:ruby_llm_models, :unlisted_at)
214
+ <% if postgresql? -%>
215
+ table.jsonb :modalities, default: {} unless column_exists?(:ruby_llm_models, :modalities)
216
+ table.jsonb :capabilities, default: [] unless column_exists?(:ruby_llm_models, :capabilities)
217
+ table.jsonb :pricing, default: {} unless column_exists?(:ruby_llm_models, :pricing)
218
+ table.jsonb :metadata, default: {} unless column_exists?(:ruby_llm_models, :metadata)
219
+ <% elsif mysql? -%>
220
+ table.json :modalities unless column_exists?(:ruby_llm_models, :modalities)
221
+ table.json :capabilities unless column_exists?(:ruby_llm_models, :capabilities)
222
+ table.json :pricing unless column_exists?(:ruby_llm_models, :pricing)
223
+ table.json :metadata unless column_exists?(:ruby_llm_models, :metadata)
224
+ <% else -%>
225
+ table.json :modalities, default: {} unless column_exists?(:ruby_llm_models, :modalities)
226
+ table.json :capabilities, default: [] unless column_exists?(:ruby_llm_models, :capabilities)
227
+ table.json :pricing, default: {} unless column_exists?(:ruby_llm_models, :pricing)
228
+ table.json :metadata, default: {} unless column_exists?(:ruby_llm_models, :metadata)
229
+ <% end -%>
230
+ table.datetime :created_at unless column_exists?(:ruby_llm_models, :created_at)
231
+ table.datetime :updated_at unless column_exists?(:ruby_llm_models, :updated_at)
232
+ end
233
+ end
234
+ ensure_unique_index(:ruby_llm_models, %i[provider model_id])
235
+ add_upgrade_index(:ruby_llm_models, :provider) unless valid_index_exists?(:ruby_llm_models, :provider)
236
+ add_upgrade_index(:ruby_llm_models, :family) unless valid_index_exists?(:ruby_llm_models, :family)
237
+ <% if postgresql? -%>
238
+ ensure_index_method(:ruby_llm_models, :capabilities, using: :gin)
239
+ ensure_index_method(:ruby_llm_models, :modalities, using: :gin)
240
+ <% end -%>
241
+ normalize_chat_model_reference
242
+ end
243
+
244
+ def normalize_chat_model_reference
245
+ table = :<%= chat_table_name %>
246
+ column = first_existing_column(table, :<%= v1_model_foreign_key %>, :ruby_llm_model_id)
247
+ return unless column
248
+
249
+ ensure_reference_types_match(table, column, :ruby_llm_models)
250
+ <% if copy_mode? -%>
251
+ unless column_exists?(table, :ruby_llm_model_id)
252
+ add_column table, :ruby_llm_model_id, reference_type_for(:ruby_llm_models)
253
+ end
254
+ RubyLLM::Generators::UpgradeMigration.for.copy_model_references
255
+ <% else -%>
256
+ with_upgrade_safety { rename_column table, column, :ruby_llm_model_id } if column != :ruby_llm_model_id
257
+ <% end -%>
258
+ add_upgrade_index(table, :ruby_llm_model_id) unless valid_index_exists?(table, :ruby_llm_model_id)
259
+ return if foreign_key_exists?(table, :ruby_llm_models, column: :ruby_llm_model_id)
260
+
261
+ add_upgrade_foreign_key(table, :ruby_llm_models, column: :ruby_llm_model_id)
262
+ end
263
+
264
+ def add_chat_and_message_columns
265
+ <% if copy_mode? -%>
266
+ unless column_exists?(:<%= chat_table_name %>, :ruby_llm_version)
267
+ add_column :<%= chat_table_name %>, :ruby_llm_version, :integer, null: false, default: 1
268
+ end
269
+ <% end -%>
270
+ ensure_boolean_column(:<%= chat_table_name %>, :cancelled)
271
+ ensure_boolean_column(:<%= message_table_name %>, :cache_until_here)
272
+ with_upgrade_safety do
273
+ change_table :<%= message_table_name %>, bulk: true do |table|
274
+ table.string :finish_reason unless column_exists?(:<%= message_table_name %>, :finish_reason)
275
+ <% if postgresql? -%>
276
+ table.jsonb :citations unless column_exists?(:<%= message_table_name %>, :citations)
277
+ table.jsonb :server_tool_calls unless column_exists?(:<%= message_table_name %>, :server_tool_calls)
278
+ table.jsonb :raw_content unless column_exists?(:<%= message_table_name %>, :raw_content)
279
+ table.jsonb :raw_reasoning unless column_exists?(:<%= message_table_name %>, :raw_reasoning)
280
+ <% else -%>
281
+ table.json :citations unless column_exists?(:<%= message_table_name %>, :citations)
282
+ table.json :server_tool_calls unless column_exists?(:<%= message_table_name %>, :server_tool_calls)
283
+ table.json :raw_content unless column_exists?(:<%= message_table_name %>, :raw_content)
284
+ table.json :raw_reasoning unless column_exists?(:<%= message_table_name %>, :raw_reasoning)
285
+ <% end -%>
286
+ end
287
+ end
288
+ <% if postgresql? -%>
289
+ normalize_postgresql_json_columns(:<%= message_table_name %>, %i[citations server_tool_calls raw_content raw_reasoning])
290
+ <% end -%>
291
+ end
292
+
293
+ def ensure_boolean_column(table, column)
294
+ unless column_exists?(table, column)
295
+ with_upgrade_safety { add_column table, column, :boolean, default: false }
296
+ end
297
+ return if ActiveRecord::Type::Boolean.new.cast(column_definition(table, column).default) == false
298
+
299
+ with_upgrade_safety { change_column_default table, column, false }
300
+ end
301
+
302
+ def move_tool_calls
303
+ move_table(:<%= v1_tool_call_table_name %>, :ruby_llm_tool_calls)
304
+ table = :ruby_llm_tool_calls
305
+ <% if postgresql? -%>
306
+ normalize_postgresql_json_columns(table, %i[arguments])
307
+ <% end -%>
308
+ <% unless mysql? -%>
309
+ normalize_json_defaults(table, arguments: {})
310
+ <% end -%>
311
+ legacy_message_column = :<%= message_foreign_key %>
312
+ if legacy_message_column != :message_id && column_exists?(table, legacy_message_column)
313
+ remove_column_foreign_keys(table, legacy_message_column)
314
+ remove_matching_indexes(table, legacy_message_column)
315
+ with_upgrade_safety { rename_column table, legacy_message_column, :message_id }
316
+ end
317
+ remove_column_foreign_keys(table, :message_id)
318
+ remove_matching_indexes(table, :message_id)
319
+
320
+ if column_exists?(table, :message_type)
321
+ unless column_definition(table, :message_type).default.nil?
322
+ with_upgrade_safety { change_column_default table, :message_type, nil }
323
+ end
324
+ else
325
+ add_column table, :message_type, :string
326
+ end
327
+ with_upgrade_safety do
328
+ change_table table, bulk: true do |definition|
329
+ definition.string :result_type unless column_exists?(table, :result_type)
330
+ definition.column :result_id, reference_type_for(:<%= message_table_name %>) unless column_exists?(table, :result_id)
331
+ definition.text :thought_signature unless column_exists?(table, :thought_signature)
332
+ definition.string :approval unless column_exists?(table, :approval)
333
+ definition.boolean :remote, default: false, null: false unless column_exists?(table, :remote)
334
+ end
335
+ end
336
+ if column_exists?(table, :thought_signature, :string)
337
+ with_upgrade_safety { change_column table, :thought_signature, :text }
338
+ end
339
+ normalize_tool_call_indexes
340
+ end
341
+
342
+ def normalize_tool_call_indexes
343
+ table = :ruby_llm_tool_calls
344
+ indexes = []
345
+ indexes << [%i[message_type message_id], {}] unless valid_index_exists?(table, %i[message_type message_id])
346
+ indexes << [%i[result_type result_id], {}] unless valid_index_exists?(table, %i[result_type result_id])
347
+ unless valid_index_exists?(table, :tool_call_id, unique: true)
348
+ deduplicate_tool_call_ids
349
+ remove_matching_indexes(table, :tool_call_id)
350
+ indexes << [:tool_call_id, { unique: true }]
351
+ end
352
+ indexes << [:name, {}] unless valid_index_exists?(table, :name)
353
+ add_upgrade_indexes(table, indexes)
354
+ end
355
+
356
+ def deduplicate_tool_call_ids
357
+ records = migration_record(:ruby_llm_tool_calls)
358
+ loop do
359
+ duplicate_ids = records.group(:tool_call_id).having('COUNT(*) > 1').limit(1_000).pluck(:tool_call_id)
360
+ break if duplicate_ids.empty?
361
+
362
+ duplicate_ids.each do |tool_call_id|
363
+ matches = records.where(tool_call_id: tool_call_id)
364
+ keeper = matches.order(records.primary_key).pick(records.primary_key)
365
+ matches.where.not(records.primary_key => keeper).find_each(batch_size: 1_000) do |record|
366
+ record.update!(tool_call_id: available_tool_call_id(records, record))
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ def available_tool_call_id(records, record)
373
+ counter = nil
374
+ loop do
375
+ suffix = "-migrated-#{record.id}#{"-#{counter}" if counter}"
376
+ candidate = "#{record.tool_call_id.to_s.slice(0, 255 - suffix.length)}#{suffix}"
377
+ return candidate unless records.where(tool_call_id: candidate).exists?
378
+
379
+ counter = counter.to_i + 1
380
+ end
381
+ end
382
+
383
+ def create_usages
384
+ unless table_exists?(:ruby_llm_usages)
385
+ create_table :ruby_llm_usages do |table|
386
+ table.references :chat, polymorphic: true, null: false,
387
+ type: reference_type_for(:<%= chat_table_name %>), index: false
388
+ table.references :message, polymorphic: true,
389
+ type: reference_type_for(:<%= message_table_name %>), index: false
390
+ table.string :operation, null: false
391
+ table.string :provider, null: false
392
+ table.string :model, null: false
393
+ table.string :status, null: false
394
+ table.integer :input_tokens
395
+ table.integer :output_tokens
396
+ table.integer :cache_read_tokens
397
+ table.integer :cache_write_tokens
398
+ table.integer :thinking_tokens
399
+ table.decimal :input_cost, precision: 16, scale: 10
400
+ table.decimal :output_cost, precision: 16, scale: 10
401
+ table.decimal :cache_read_cost, precision: 16, scale: 10
402
+ table.decimal :cache_write_cost, precision: 16, scale: 10
403
+ table.decimal :thinking_cost, precision: 16, scale: 10
404
+ table.decimal :total_cost, precision: 16, scale: 10
405
+ table.timestamps
406
+ table.check_constraint "operation IN (<%= usage_operations_sql %>)"
407
+ table.check_constraint "status IN (<%= usage_statuses_sql %>)"
408
+ end
409
+ end
410
+ model_column = connection.columns(:ruby_llm_usages).find { |column| column.name == 'model' }
411
+ with_upgrade_safety { change_column_null :ruby_llm_usages, :model, false } if model_column.null
412
+ indexes = [%i[chat_type chat_id], %i[message_type message_id], :status]
413
+ missing = indexes.reject { |columns| valid_index_exists?(:ruby_llm_usages, columns) }
414
+ .map { |columns| [columns, {}] }
415
+ add_upgrade_indexes(:ruby_llm_usages, missing)
416
+ end
417
+
418
+ def create_batches
419
+ unless table_exists?(:ruby_llm_batches)
420
+ create_table :ruby_llm_batches do |table|
421
+ table.string :provider_batch_id, null: false
422
+ table.string :provider, null: false
423
+ table.string :status, null: false
424
+ table.string :raw_status
425
+ table.boolean :completed, null: false, default: false
426
+ table.string :chat_type
427
+ table.string :batch_protocol
428
+ <% if postgresql? -%>
429
+ table.jsonb :chat_ids, default: []
430
+ table.jsonb :request_counts
431
+ table.jsonb :reported_cost
432
+ <% elsif mysql? -%>
433
+ table.json :chat_ids
434
+ table.json :request_counts
435
+ table.json :reported_cost
436
+ <% else -%>
437
+ table.json :chat_ids, default: []
438
+ table.json :request_counts
439
+ table.json :reported_cost
440
+ <% end -%>
441
+ table.timestamps
442
+ end
443
+ end
444
+ add_column :ruby_llm_batches, :raw_status, :string unless column_exists?(:ruby_llm_batches, :raw_status)
445
+ add_column :ruby_llm_batches, :chat_type, :string unless column_exists?(:ruby_llm_batches, :chat_type)
446
+ add_column :ruby_llm_batches, :batch_protocol, :string unless column_exists?(:ruby_llm_batches, :batch_protocol)
447
+ <% if postgresql? -%>
448
+ add_column :ruby_llm_batches, :request_counts, :jsonb unless column_exists?(:ruby_llm_batches, :request_counts)
449
+ add_column :ruby_llm_batches, :reported_cost, :jsonb unless column_exists?(:ruby_llm_batches, :reported_cost)
450
+ normalize_postgresql_json_columns(:ruby_llm_batches, %i[chat_ids request_counts reported_cost])
451
+ <% else -%>
452
+ add_column :ruby_llm_batches, :request_counts, :json unless column_exists?(:ruby_llm_batches, :request_counts)
453
+ add_column :ruby_llm_batches, :reported_cost, :json unless column_exists?(:ruby_llm_batches, :reported_cost)
454
+ <% end -%>
455
+ <% unless mysql? -%>
456
+ normalize_json_defaults(:ruby_llm_batches, chat_ids: [])
457
+ <% end -%>
458
+ ensure_unique_index(:ruby_llm_batches, %i[provider provider_batch_id])
459
+ add_upgrade_index(:ruby_llm_batches, :status) unless valid_index_exists?(:ruby_llm_batches, :status)
460
+ end
461
+
462
+ def usage_identity_sql(models)
463
+ providers = []
464
+ model_ids = []
465
+ joins = []
466
+ if column_exists?(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
467
+ model_column = column_definition(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
468
+ if model_column.type == :string
469
+ providers << message_value(:provider) if column_exists?(:<%= message_table_name %>, :provider)
470
+ model_ids << message_value(:<%= v1_model_foreign_key %>)
471
+ else
472
+ joins << "LEFT JOIN #{quote_table(models)} legacy_message_models " \
473
+ "ON #{message_value(:<%= v1_model_foreign_key %>)} = legacy_message_models.#{quoted_primary_key(models)}"
474
+ providers << "legacy_message_models.#{quote_column(:provider)}"
475
+ model_ids << "legacy_message_models.#{quote_column(:model_id)}"
476
+ end
477
+ end
478
+
479
+ chat_model_column = first_existing_column(:<%= chat_table_name %>, :<%= v1_model_foreign_key %>, :ruby_llm_model_id)
480
+ if chat_model_column
481
+ joins << "LEFT JOIN #{quote_table(:<%= chat_table_name %>)} legacy_chats " \
482
+ "ON #{message_value(:<%= chat_foreign_key %>)} = legacy_chats.#{quoted_primary_key(:<%= chat_table_name %>)}"
483
+ joins << "LEFT JOIN #{quote_table(models)} legacy_chat_models " \
484
+ "ON legacy_chats.#{quote_column(chat_model_column)} = legacy_chat_models.#{quoted_primary_key(models)}"
485
+ providers << "legacy_chat_models.#{quote_column(:provider)}"
486
+ model_ids << "legacy_chat_models.#{quote_column(:model_id)}"
487
+ end
488
+
489
+ [joins.join("\n"), coalesce_sql(providers), coalesce_sql(model_ids)]
490
+ end
491
+
492
+ def legacy_usage_conditions
493
+ columns = %i[
494
+ input_tokens output_tokens cached_tokens cache_creation_tokens cache_read_tokens cache_write_tokens
495
+ thinking_tokens total_cost cost_details
496
+ ]
497
+ conditions = columns.filter_map do |column|
498
+ "#{message_value(column)} IS NOT NULL" if column_exists?(:<%= message_table_name %>, column)
499
+ end
500
+ conditions << "#{message_value(:role)} = 'assistant'" if column_exists?(:<%= message_table_name %>, :role)
501
+ conditions
502
+ end
503
+
504
+ def coalesce_sql(values)
505
+ values = Array(values).compact
506
+ return 'NULL' if values.empty?
507
+ return values.first if values.one?
508
+
509
+ "COALESCE(#{values.join(', ')})"
510
+ end
511
+
512
+ def message_value(column) = "legacy_messages.#{quote_column(column)}"
513
+ def first_existing_column(table, *columns) = columns.find { |column| column_exists?(table, column) }
514
+ def column_definition(table, column) = connection.columns(table).find { |candidate| candidate.name == column.to_s }
515
+ def quoted_primary_key(table) = quote_column(connection.primary_key(table))
516
+ def quote_table(table) = connection.quote_table_name(table)
517
+ def quote_column(column) = connection.quote_column_name(column)
518
+
519
+ def ensure_reference_types_match(source, column, target)
520
+ source_column = column_definition(source, column)
521
+ target_column = column_definition(target, connection.primary_key(target))
522
+ return if source_column.type == target_column.type && source_column.limit == target_column.limit
523
+
524
+ raise "Expected #{source}.#{column} to match #{target}.#{connection.primary_key(target)}"
525
+ end
526
+
527
+ def move_table(source, target)
528
+ <% if copy_mode? -%>
529
+ with_upgrade_safety { RubyLLM::Generators::UpgradeMigration.for.copy_table(source, target) }
530
+ <% else -%>
531
+ return if source == target || !table_exists?(source)
532
+
533
+ raise "Both #{source} and #{target} exist. Merge or remove one before retrying." if table_exists?(target)
534
+
535
+ with_upgrade_safety { rename_table source, target }
536
+ <% end -%>
537
+ end
538
+
539
+ <% if postgresql? -%>
540
+ def normalize_postgresql_json_columns(table, columns)
541
+ columns.each do |column_name|
542
+ column = column_definition(table, column_name)
543
+ next unless column&.type == :json
544
+
545
+ with_upgrade_safety do
546
+ change_column table, column_name, :jsonb, using: "#{quote_column(column_name)}::jsonb"
547
+ end
548
+ end
549
+ end
550
+
551
+ <% end -%>
552
+ <% unless mysql? -%>
553
+ def normalize_json_defaults(table, defaults)
554
+ defaults.each do |column_name, value|
555
+ column = column_definition(table, column_name)
556
+ next unless column&.default.nil?
557
+
558
+ with_upgrade_safety { change_column_default table, column_name, from: nil, to: value }
559
+ end
560
+ end
561
+
562
+ <% end -%>
563
+ def ensure_unique_index(table, columns)
564
+ return if valid_index_exists?(table, columns, unique: true)
565
+
566
+ validate_unique_values(table, columns)
567
+ remove_matching_indexes(table, columns)
568
+ add_upgrade_index(table, columns, unique: true)
569
+ end
570
+
571
+ def ensure_index_method(table, columns, using:)
572
+ return if valid_index_exists?(table, columns, using: using)
573
+
574
+ remove_matching_indexes(table, columns)
575
+ add_upgrade_index(table, columns, using: using)
576
+ end
577
+
578
+ def remove_matching_indexes(table, columns)
579
+ connection.indexes(table).each do |index|
580
+ remove_upgrade_index(table, index.name) if index.columns == Array(columns).map(&:to_s)
581
+ end
582
+ end
583
+
584
+ def remove_column_foreign_keys(table, column)
585
+ connection.foreign_keys(table).each do |key|
586
+ remove_foreign_key(table, key.to_table, column: column) if key.column == column.to_s
587
+ end
588
+ end
589
+
590
+ def valid_index_exists?(table, columns, unique: nil, using: nil)
591
+ matching = connection.indexes(table).select do |index|
592
+ index.columns == Array(columns).map(&:to_s) &&
593
+ (unique.nil? || index.unique == unique) &&
594
+ (using.nil? || index.using.to_s == using.to_s)
595
+ end
596
+ valid, invalid = matching.partition { |index| !index.respond_to?(:valid?) || index.valid? }
597
+ invalid.each { |index| remove_upgrade_index(table, index.name) }
598
+ valid.any?
599
+ end
600
+
601
+ def add_upgrade_index(table, columns, **options)
602
+ <% if postgresql? -%>
603
+ options[:algorithm] = :concurrently
604
+ <% end -%>
605
+ add_index table, columns, **options
606
+ end
607
+
608
+ def add_upgrade_indexes(table, indexes)
609
+ return if indexes.empty?
610
+
611
+ <% if mysql? -%>
612
+ with_upgrade_safety do
613
+ change_table table, bulk: true do |definition|
614
+ indexes.each { |columns, options| definition.index(columns, **options) }
615
+ end
616
+ end
617
+ <% else -%>
618
+ indexes.each { |columns, options| add_upgrade_index(table, columns, **options) }
619
+ <% end -%>
620
+ end
621
+
622
+ def remove_upgrade_index(table, name)
623
+ options = { name: name }
624
+ <% if postgresql? -%>
625
+ options[:algorithm] = :concurrently
626
+ <% end -%>
627
+ remove_index table, **options
628
+ end
629
+
630
+ def add_upgrade_foreign_key(table, target, column:)
631
+ <% if postgresql? -%>
632
+ add_foreign_key table, target, column: column, validate: false
633
+ validate_foreign_key table, target, column: column
634
+ <% elsif mysql? -%>
635
+ connection.disable_referential_integrity { add_foreign_key table, target, column: column }
636
+ <% else -%>
637
+ add_foreign_key table, target, column: column
638
+ <% end -%>
639
+ end
640
+
641
+ def reference_type_for(table)
642
+ column = column_definition(table, connection.primary_key(table))
643
+ raise "Could not determine the primary key type for #{table}" unless column
644
+
645
+ return :bigint if column.type == :integer && column.limit == 8
646
+
647
+ column.type
648
+ end
649
+
650
+ def with_upgrade_safety(&)
651
+ return safety_assured(&) if respond_to?(:safety_assured, true)
652
+
653
+ yield
654
+ end
655
+
656
+ def migration_record(table)
657
+ Class.new(ActiveRecord::Base) do
658
+ self.table_name = table.to_s
659
+ self.inheritance_column = :_type_disabled
660
+ end
661
+ end
662
+ end