ruby_llm 1.16.0 → 2.0.0.rc2

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