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,461 @@
1
+ # frozen_string_literal: true
2
+ <% if copy_mode? -%>
3
+
4
+ require 'generators/ruby_llm/upgrade/upgrade_migration'
5
+ <% end -%>
6
+
7
+ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
8
+ disable_ddl_transaction!
9
+
10
+ BATCH_SIZE = 10_000
11
+ PROGRESS_TABLE = :ruby_llm_v2_backfills
12
+
13
+ def up
14
+ <% if copy_mode? -%>
15
+ RubyLLM::Generators::UpgradeMigration.new.verify_settings(<%= copy_upgrade_settings.inspect %>)
16
+ <% else -%>
17
+ raise 'Generate this migration with --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
18
+ <% end -%>
19
+ verify_upgrade_in_progress
20
+ create_progress_table
21
+ backfill_required_defaults
22
+ <% if copy_mode? -%>
23
+ RubyLLM::Generators::UpgradeMigration.new.backfill
24
+ %w[message_content tool_results usages].each { |task| mark_completed(task) }
25
+ <% else -%>
26
+ backfill_message_content
27
+ backfill_tool_results
28
+ backfill_usages
29
+ <% end -%>
30
+ verify_backfills
31
+ end
32
+
33
+ def down
34
+ raise ActiveRecord::IrreversibleMigration, 'RubyLLM 2.0 changes persisted record ownership'
35
+ end
36
+
37
+ private
38
+
39
+ def verify_upgrade_in_progress
40
+ legacy_columns = %i[content_raw input_tokens output_tokens <%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>]
41
+ unless legacy_columns.any? { |column| column_exists?(:<%= message_table_name %>, column) }
42
+ raise 'The RubyLLM 2.0 schema has already been cleaned. Do not run the backfill again.'
43
+ end
44
+ return unless table_exists?(PROGRESS_TABLE)
45
+ return unless progress_records.where(task: 'finished', completed: true).exists?
46
+
47
+ raise 'The RubyLLM 2.0 upgrade is already finished. Do not run the backfill again.'
48
+ end
49
+
50
+ def backfill_required_defaults
51
+ {
52
+ <%= chat_table_name %>: {cancelled: false},
53
+ <%= message_table_name %>: {cache_until_here: false},
54
+ ruby_llm_tool_calls: {message_type: '<%= message_model_name %>'}
55
+ }.each do |table, attributes|
56
+ column, value = attributes.first
57
+ migration_record(table).where(column => nil).in_batches(of: BATCH_SIZE) do |batch|
58
+ batch.update_all(column => value)
59
+ end
60
+ end
61
+ end
62
+
63
+ def backfill_message_content
64
+ return mark_completed('message_content') unless column_exists?(:<%= message_table_name %>, :content_raw)
65
+
66
+ each_message_range('message_content') do |range|
67
+ content_raw = message_value(:content_raw)
68
+ raw_content = message_value(:raw_content)
69
+ content = message_value(:content)
70
+ <% if postgresql? -%>
71
+ structured_content = "#{content_raw}::jsonb"
72
+ rendered_content = "#{content_raw}::text"
73
+ <% elsif mysql? -%>
74
+ structured_content = content_raw
75
+ rendered_content = "CAST(#{content_raw} AS CHAR)"
76
+ <% else -%>
77
+ structured_content = content_raw
78
+ rendered_content = content_raw
79
+ <% end -%>
80
+ execute <<~SQL
81
+ UPDATE #{quote_table(:<%= message_table_name %>)} AS legacy_messages
82
+ SET #{quote_column(:raw_content)} = COALESCE(#{raw_content}, #{structured_content}),
83
+ #{quote_column(:content)} = COALESCE(#{content}, #{rendered_content})
84
+ WHERE #{range}
85
+ AND #{content_raw} IS NOT NULL
86
+ SQL
87
+ end
88
+ end
89
+
90
+ def backfill_tool_results
91
+ result_reference = :<%= v1_tool_call_foreign_key %>
92
+ return mark_completed('tool_results') unless column_exists?(:<%= message_table_name %>, result_reference)
93
+
94
+ each_message_range('tool_results') do |range|
95
+ messages = quote_table(:<%= message_table_name %>)
96
+ tool_calls = quote_table(:ruby_llm_tool_calls)
97
+ message_id = quoted_primary_key(:<%= message_table_name %>)
98
+ tool_call_id = quoted_primary_key(:ruby_llm_tool_calls)
99
+ result_id = message_value(result_reference)
100
+ result_type = connection.quote('<%= message_model_name %>')
101
+ <% if mysql? -%>
102
+ execute <<~SQL
103
+ UPDATE #{tool_calls}
104
+ INNER JOIN (
105
+ SELECT #{result_id} AS matched_tool_call_id,
106
+ MIN(legacy_messages.#{message_id}) AS matched_result_id
107
+ FROM #{messages} legacy_messages
108
+ WHERE #{range}
109
+ AND #{result_id} IS NOT NULL
110
+ GROUP BY #{result_id}
111
+ ) matches ON matches.matched_tool_call_id = #{tool_calls}.#{tool_call_id}
112
+ SET #{tool_calls}.result_id = matches.matched_result_id,
113
+ #{tool_calls}.result_type = #{result_type}
114
+ SQL
115
+ <% elsif postgresql? -%>
116
+ execute <<~SQL
117
+ UPDATE #{tool_calls}
118
+ SET result_id = legacy_messages.#{message_id},
119
+ result_type = #{result_type}
120
+ FROM #{messages} legacy_messages
121
+ WHERE #{result_id} = #{tool_calls}.#{tool_call_id}
122
+ AND #{range}
123
+ AND #{result_id} IS NOT NULL
124
+ SQL
125
+ <% else -%>
126
+ execute <<~SQL
127
+ UPDATE #{tool_calls}
128
+ SET result_id = (
129
+ SELECT legacy_messages.#{message_id}
130
+ FROM #{messages} legacy_messages
131
+ WHERE #{range}
132
+ AND #{result_id} = #{tool_calls}.#{tool_call_id}
133
+ ORDER BY legacy_messages.#{message_id}
134
+ LIMIT 1
135
+ ),
136
+ result_type = #{result_type}
137
+ WHERE EXISTS (
138
+ SELECT 1
139
+ FROM #{messages} legacy_messages
140
+ WHERE #{range}
141
+ AND #{result_id} = #{tool_calls}.#{tool_call_id}
142
+ )
143
+ SQL
144
+ <% end -%>
145
+ end
146
+ end
147
+
148
+ def backfill_usages
149
+ joins, provider, model = usage_identity_sql
150
+ conditions = legacy_usage_conditions
151
+ return mark_completed('usages') if conditions.empty?
152
+
153
+ each_message_range('usages') do |range|
154
+ candidate = usage_candidate_sql(conditions, joins, range)
155
+ execute usage_insert_sql(candidate, provider, model)
156
+ end
157
+ end
158
+
159
+ def each_message_range(task)
160
+ return if completed?(task)
161
+
162
+ primary_key = connection.primary_key(:<%= message_table_name %>)
163
+ records = migration_record(:<%= message_table_name %>)
164
+ last_id = progress_value(task)
165
+ loop do
166
+ relation = records.order(primary_key => :asc)
167
+ relation = relation.where("#{quote_column(primary_key)} > ?", last_id) if last_id
168
+ ids = relation.limit(BATCH_SIZE).pluck(primary_key)
169
+ break mark_completed(task) if ids.empty?
170
+
171
+ upper_id = ids.last
172
+ range = message_range(primary_key, last_id, upper_id)
173
+ records.transaction do
174
+ with_upgrade_safety { yield range }
175
+ record_progress(task, upper_id)
176
+ end
177
+ say "#{task}: migrated through #{primary_key}=#{upper_id} (#{ids.length} messages)", true
178
+ last_id = upper_id
179
+ end
180
+ end
181
+
182
+ def message_range(primary_key, lower_id, upper_id)
183
+ upper = "#{message_value(primary_key)} <= #{connection.quote(upper_id)}"
184
+ return upper unless lower_id
185
+
186
+ "#{message_value(primary_key)} > #{connection.quote(lower_id)} AND #{upper}"
187
+ end
188
+
189
+ def verify_backfills
190
+ verify_message_content
191
+ verify_tool_results
192
+ verify_usages
193
+ end
194
+
195
+ def verify_message_content
196
+ return unless column_exists?(:<%= message_table_name %>, :content_raw)
197
+
198
+ missing = migration_record(:<%= message_table_name %>)
199
+ .where.not(content_raw: nil)
200
+ .where('raw_content IS NULL OR content IS NULL')
201
+ .limit(1)
202
+ .pick(connection.primary_key(:<%= message_table_name %>))
203
+ raise "Message #{missing} did not preserve content_raw" if missing
204
+ end
205
+
206
+ def verify_tool_results
207
+ result_reference = :<%= v1_tool_call_foreign_key %>
208
+ return unless column_exists?(:<%= message_table_name %>, result_reference)
209
+
210
+ messages = quote_table(:<%= message_table_name %>)
211
+ tool_calls = quote_table(:ruby_llm_tool_calls)
212
+ missing = select_value(<<~SQL)
213
+ SELECT legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
214
+ FROM #{messages} legacy_messages
215
+ <% if copy_mode? -%>
216
+ LEFT JOIN #{quote_table(:<%= v1_tool_call_table_name %>)} legacy_tool_calls
217
+ ON #{message_value(result_reference)} = legacy_tool_calls.#{quoted_primary_key(:<%= v1_tool_call_table_name %>)}
218
+ LEFT JOIN #{tool_calls} migrated_tool_calls
219
+ ON legacy_tool_calls.tool_call_id = migrated_tool_calls.tool_call_id
220
+ AND legacy_tool_calls.#{quote_column(:<%= message_foreign_key %>)} = migrated_tool_calls.message_id
221
+ <% else -%>
222
+ LEFT JOIN #{tool_calls} migrated_tool_calls
223
+ ON #{message_value(result_reference)} = migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)}
224
+ <% end -%>
225
+ AND migrated_tool_calls.result_type = #{connection.quote('<%= message_model_name %>')}
226
+ AND migrated_tool_calls.result_id = legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
227
+ WHERE #{message_value(result_reference)} IS NOT NULL
228
+ AND migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)} IS NULL
229
+ LIMIT 1
230
+ SQL
231
+ raise "Message #{missing} did not preserve its tool result association" if missing
232
+ end
233
+
234
+ def verify_usages
235
+ joins, provider, model = usage_identity_sql
236
+ conditions = legacy_usage_conditions
237
+ return if conditions.empty?
238
+
239
+ candidate = usage_candidate_sql(conditions, joins, '1 = 1')
240
+ missing = select_value(<<~SQL)
241
+ SELECT #{message_value(connection.primary_key(:<%= message_table_name %>))}
242
+ #{candidate}
243
+ LIMIT 1
244
+ SQL
245
+ return unless missing
246
+
247
+ raise "Message #{missing} did not receive its RubyLLM usage entry (provider: #{provider}, model: #{model})"
248
+ end
249
+
250
+ def usage_identity_sql
251
+ providers = []
252
+ models = []
253
+ joins = []
254
+ registry = quote_table(:ruby_llm_models)
255
+ registry_key = quoted_primary_key(:ruby_llm_models)
256
+
257
+ if column_exists?(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
258
+ explicit_model = column_definition(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
259
+ if explicit_model.type == :string
260
+ models << message_value(:<%= v1_model_foreign_key %>)
261
+ providers << message_value(:provider) if column_exists?(:<%= message_table_name %>, :provider)
262
+ else
263
+ joins << "LEFT JOIN #{registry} legacy_message_models " \
264
+ "ON #{message_value(:<%= v1_model_foreign_key %>)} = legacy_message_models.#{registry_key}"
265
+ providers << "legacy_message_models.#{quote_column(:provider)}"
266
+ models << "legacy_message_models.#{quote_column(:model_id)}"
267
+ end
268
+ end
269
+
270
+ if column_exists?(:<%= chat_table_name %>, :ruby_llm_model_id)
271
+ chats = quote_table(:<%= chat_table_name %>)
272
+ joins << "LEFT JOIN #{chats} legacy_chats " \
273
+ "ON #{message_value(:<%= chat_foreign_key %>)} = legacy_chats.#{quoted_primary_key(:<%= chat_table_name %>)}"
274
+ joins << "LEFT JOIN #{registry} legacy_chat_models " \
275
+ "ON legacy_chats.#{quote_column(:ruby_llm_model_id)} = legacy_chat_models.#{registry_key}"
276
+ providers << "legacy_chat_models.#{quote_column(:provider)}"
277
+ models << "legacy_chat_models.#{quote_column(:model_id)}"
278
+ end
279
+
280
+ [joins.join("\n"), coalesce_sql(providers), coalesce_sql(models)]
281
+ end
282
+
283
+ def legacy_usage_conditions
284
+ token_columns = %i[
285
+ input_tokens output_tokens cached_tokens cache_creation_tokens cache_read_tokens cache_write_tokens
286
+ thinking_tokens total_cost
287
+ ]
288
+ conditions = token_columns.filter_map do |column|
289
+ "#{message_value(column)} IS NOT NULL" if column_exists?(:<%= message_table_name %>, column)
290
+ end
291
+ if column_exists?(:<%= message_table_name %>, :cost_details)
292
+ costs = %w[input output cache_read cache_write thinking total].map do |key|
293
+ "#{legacy_cost_detail(key)} IS NOT NULL"
294
+ end
295
+ conditions << "(#{costs.join(' OR ')})"
296
+ end
297
+ conditions << "#{message_value(:role)} = 'assistant'" if column_exists?(:<%= message_table_name %>, :role)
298
+ conditions
299
+ end
300
+
301
+ def usage_candidate_sql(conditions, joins, range)
302
+ usages = quote_table(:ruby_llm_usages)
303
+ <<~SQL
304
+ FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
305
+ #{joins}
306
+ WHERE (#{conditions.join(' OR ')})
307
+ AND #{range}
308
+ AND NOT EXISTS (
309
+ SELECT 1
310
+ FROM #{usages} existing_usages
311
+ WHERE existing_usages.message_type = #{connection.quote('<%= message_model_name %>')}
312
+ AND existing_usages.message_id = #{message_value(connection.primary_key(:<%= message_table_name %>))}
313
+ AND existing_usages.operation = 'chat'
314
+ AND existing_usages.status = 'succeeded'
315
+ )
316
+ SQL
317
+ end
318
+
319
+ def usage_insert_sql(candidate, provider, model)
320
+ columns = %i[
321
+ chat_type chat_id message_type message_id operation provider model status
322
+ input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens
323
+ input_cost output_cost cache_read_cost cache_write_cost thinking_cost total_cost
324
+ created_at updated_at
325
+ ].map { |column| quote_column(column) }.join(', ')
326
+ values = [
327
+ connection.quote('<%= chat_model_name %>'),
328
+ message_value(:<%= chat_foreign_key %>),
329
+ connection.quote('<%= message_model_name %>'),
330
+ message_value(connection.primary_key(:<%= message_table_name %>)),
331
+ connection.quote('chat'),
332
+ provider,
333
+ model,
334
+ connection.quote('succeeded'),
335
+ legacy_message_value(:input_tokens),
336
+ legacy_message_value(:output_tokens),
337
+ preferred_message_value(:cache_read_tokens, :cached_tokens),
338
+ preferred_message_value(:cache_write_tokens, :cache_creation_tokens),
339
+ legacy_message_value(:thinking_tokens),
340
+ legacy_cost_detail('input'),
341
+ legacy_cost_detail('output'),
342
+ legacy_cost_detail('cache_read'),
343
+ legacy_cost_detail('cache_write'),
344
+ legacy_cost_detail('thinking'),
345
+ legacy_total_cost,
346
+ legacy_message_value(:created_at, fallback: 'CURRENT_TIMESTAMP'),
347
+ legacy_message_value(:updated_at, fallback: 'CURRENT_TIMESTAMP')
348
+ ].join(', ')
349
+ <<~SQL
350
+ INSERT INTO #{quote_table(:ruby_llm_usages)} (#{columns})
351
+ SELECT #{values}
352
+ #{candidate}
353
+ SQL
354
+ end
355
+
356
+ def preferred_message_value(preferred, fallback)
357
+ return message_value(preferred) if column_exists?(:<%= message_table_name %>, preferred)
358
+
359
+ legacy_message_value(fallback)
360
+ end
361
+
362
+ def legacy_message_value(column, fallback: 'NULL')
363
+ return fallback unless column_exists?(:<%= message_table_name %>, column)
364
+
365
+ message_value(column)
366
+ end
367
+
368
+ def legacy_cost_detail(key)
369
+ return 'NULL' unless column_exists?(:<%= message_table_name %>, :cost_details)
370
+
371
+ details = message_value(:cost_details)
372
+ <% if postgresql? -%>
373
+ "NULLIF(#{details}::jsonb ->> '#{key}', '')::numeric"
374
+ <% elsif mysql? -%>
375
+ "JSON_UNQUOTE(JSON_EXTRACT(#{details}, '$.#{key}'))"
376
+ <% else -%>
377
+ "json_extract(#{details}, '$.#{key}')"
378
+ <% end -%>
379
+ end
380
+
381
+ def legacy_total_cost
382
+ direct = legacy_message_value(:total_cost)
383
+ detailed = legacy_cost_detail('total')
384
+ return 'NULL' if direct == 'NULL' && detailed == 'NULL'
385
+ return detailed if direct == 'NULL'
386
+ return direct if detailed == 'NULL'
387
+
388
+ "COALESCE(#{direct}, #{detailed})"
389
+ end
390
+
391
+ def create_progress_table
392
+ return if table_exists?(PROGRESS_TABLE)
393
+
394
+ create_table PROGRESS_TABLE, id: false do |table|
395
+ table.string :task, null: false
396
+ table.column :last_id, reference_type_for(:<%= message_table_name %>)
397
+ table.boolean :completed, null: false, default: false
398
+ table.index :task, unique: true
399
+ end
400
+ end
401
+
402
+ def progress_value(task)
403
+ progress_records.where(task: task).pick(:last_id)
404
+ end
405
+
406
+ def completed?(task)
407
+ progress_records.where(task: task, completed: true).exists?
408
+ end
409
+
410
+ def record_progress(task, last_id)
411
+ row = progress_records.where(task: task)
412
+ return row.update_all(last_id: last_id, completed: false) if row.exists?
413
+
414
+ progress_records.insert_all!([{ task: task, last_id: last_id, completed: false }], returning: false)
415
+ end
416
+
417
+ def mark_completed(task)
418
+ row = progress_records.where(task: task)
419
+ return row.update_all(completed: true) if row.exists?
420
+
421
+ progress_records.insert_all!([{ task: task, completed: true }], returning: false)
422
+ end
423
+
424
+ def coalesce_sql(values)
425
+ values = Array(values).compact
426
+ return 'NULL' if values.empty?
427
+ return values.first if values.one?
428
+
429
+ "COALESCE(#{values.join(', ')})"
430
+ end
431
+
432
+ def message_value(column) = "legacy_messages.#{quote_column(column)}"
433
+ def column_definition(table, column) = connection.columns(table).find { |candidate| candidate.name == column.to_s }
434
+ def quoted_primary_key(table) = quote_column(connection.primary_key(table))
435
+ def quote_table(table) = connection.quote_table_name(table)
436
+ def quote_column(column) = connection.quote_column_name(column)
437
+
438
+ def reference_type_for(table)
439
+ column = column_definition(table, connection.primary_key(table))
440
+ raise "Could not determine the primary key type for #{table}" unless column
441
+
442
+ return :bigint if column.type == :integer && column.limit == 8
443
+
444
+ column.type
445
+ end
446
+
447
+ def with_upgrade_safety(&)
448
+ return safety_assured(&) if respond_to?(:safety_assured, true)
449
+
450
+ yield
451
+ end
452
+
453
+ def progress_records = migration_record(PROGRESS_TABLE)
454
+
455
+ def migration_record(table)
456
+ Class.new(ActiveRecord::Base) do
457
+ self.table_name = table.to_s
458
+ self.inheritance_column = :_type_disabled
459
+ end
460
+ end
461
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+ <% if copy_mode? -%>
3
+
4
+ require 'generators/ruby_llm/upgrade/upgrade_migration'
5
+ <% end -%>
6
+
7
+ class <%= cleanup_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
8
+ disable_ddl_transaction!
9
+
10
+ PROGRESS_TABLE = :ruby_llm_v2_backfills
11
+
12
+ def up
13
+ <% if copy_mode? -%>
14
+ RubyLLM::Generators::UpgradeMigration.new.verify_settings(<%= copy_upgrade_settings.inspect %>)
15
+ RubyLLM::Generators::UpgradeMigration.new.verify_cleanup
16
+ <% else -%>
17
+ raise 'Generate cleanup with --mode copy for this database' if table_exists?(:ruby_llm_v2_upgrades)
18
+ <% end -%>
19
+ unless table_exists?(PROGRESS_TABLE)
20
+ <% if copy_mode? -%>
21
+ remove_legacy_message_columns
22
+ RubyLLM::Generators::UpgradeMigration.new.cleanup
23
+ return
24
+ <% else -%>
25
+ return unless legacy_columns.any?
26
+
27
+ raise 'RubyLLM 2.0 upgrade progress is missing. Run the finish migration before cleanup.'
28
+ <% end -%>
29
+ end
30
+ unless migration_record(PROGRESS_TABLE).where(task: 'finished', completed: true).exists?
31
+ raise 'Run <%= finish_migration_class_name %> before removing the legacy message columns'
32
+ end
33
+
34
+ remove_legacy_message_columns
35
+ <% if copy_mode? -%>
36
+ RubyLLM::Generators::UpgradeMigration.new.cleanup
37
+ <% end -%>
38
+ drop_table PROGRESS_TABLE
39
+ end
40
+
41
+ def down
42
+ raise ActiveRecord::IrreversibleMigration, 'RubyLLM 2.0 removes legacy message data'
43
+ end
44
+
45
+ private
46
+
47
+ def remove_legacy_message_columns
48
+ table = :<%= message_table_name %>
49
+ remove_matching_indexes(table, :role)
50
+ reference_columns = %i[<%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>]
51
+ reference_columns.each do |column|
52
+ remove_column_foreign_keys(table, column)
53
+ remove_matching_indexes(table, column)
54
+ end
55
+
56
+ columns = legacy_columns
57
+ with_upgrade_safety { remove_columns table, *columns } if columns.any?
58
+ end
59
+
60
+ def remove_column_foreign_keys(table, column)
61
+ connection.foreign_keys(table).each do |key|
62
+ remove_foreign_key(table, key.to_table, column: column) if key.column == column.to_s
63
+ end
64
+ end
65
+
66
+ def remove_matching_indexes(table, columns)
67
+ connection.indexes(table).each do |index|
68
+ remove_upgrade_index(table, index.name) if index.columns == Array(columns).map(&:to_s)
69
+ end
70
+ end
71
+
72
+ def remove_upgrade_index(table, name)
73
+ options = { name: name }
74
+ <% if postgresql? -%>
75
+ options[:algorithm] = :concurrently
76
+ <% end -%>
77
+ remove_index table, **options
78
+ end
79
+
80
+ def with_upgrade_safety(&)
81
+ return safety_assured(&) if respond_to?(:safety_assured, true)
82
+
83
+ yield
84
+ end
85
+
86
+ def legacy_columns
87
+ candidates = %i[
88
+ <%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>
89
+ content_raw input_tokens output_tokens cached_tokens cache_creation_tokens cache_read_tokens
90
+ cache_write_tokens thinking_tokens total_cost cost_details
91
+ ]
92
+ candidates.uniq.select { |column| column_exists?(:<%= message_table_name %>, column) }
93
+ end
94
+
95
+ def migration_record(table)
96
+ Class.new(ActiveRecord::Base) do
97
+ self.table_name = table.to_s
98
+ self.inheritance_column = :_type_disabled
99
+ end
100
+ end
101
+ end