ruby_llm 1.16.0 → 2.0.0.rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +242 -54
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +662 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +126 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,468 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% if copy_mode? -%>
4
+ require 'generators/ruby_llm/upgrade/upgrade_migration'
5
+ <% else -%>
6
+ require 'generators/ruby_llm/upgrade/legacy_content_sql'
7
+ <% end -%>
8
+
9
+ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
10
+ disable_ddl_transaction!
11
+
12
+ <% unless copy_mode? -%>
13
+ BATCH_SIZE = 10_000
14
+ <% end -%>
15
+ PROGRESS_TABLE = :ruby_llm_v2_backfills
16
+
17
+ def up
18
+ <% if copy_mode? -%>
19
+ RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
20
+ verify_upgrade_in_progress
21
+ RubyLLM::Generators::UpgradeMigration.for.backfill
22
+ <% else -%>
23
+ raise 'Generate this migration with --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
24
+ verify_upgrade_in_progress
25
+ create_progress_table
26
+ backfill_required_defaults
27
+ backfill_message_content
28
+ backfill_tool_results
29
+ backfill_usages
30
+ verify_backfills
31
+ <% end -%>
32
+ end
33
+
34
+ def down
35
+ raise ActiveRecord::IrreversibleMigration, 'RubyLLM 2.0 changes persisted record ownership'
36
+ end
37
+
38
+ private
39
+
40
+ def verify_upgrade_in_progress
41
+ legacy_columns = %i[content_raw input_tokens output_tokens <%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>]
42
+ unless legacy_columns.any? { |column| column_exists?(:<%= message_table_name %>, column) }
43
+ raise 'The RubyLLM 2.0 schema has already been cleaned. Do not run the backfill again.'
44
+ end
45
+ return unless table_exists?(PROGRESS_TABLE)
46
+ return unless progress_records.where(task: 'finished', completed: true).exists?
47
+
48
+ raise 'The RubyLLM 2.0 upgrade is already finished. Do not run the backfill again.'
49
+ end
50
+
51
+ <% unless copy_mode? -%>
52
+ def backfill_required_defaults
53
+ {
54
+ <%= chat_table_name %>: {cancelled: false},
55
+ <%= message_table_name %>: {cache_until_here: false},
56
+ ruby_llm_tool_calls: {message_type: '<%= message_model_name %>'}
57
+ }.each do |table, attributes|
58
+ column, value = attributes.first
59
+ migration_record(table).where(column => nil).in_batches(of: BATCH_SIZE) do |batch|
60
+ batch.update_all(column => value)
61
+ end
62
+ end
63
+ end
64
+
65
+ def backfill_message_content
66
+ return mark_completed('message_content') unless column_exists?(:<%= message_table_name %>, :content_raw)
67
+
68
+ each_message_range('message_content') do |range|
69
+ content_raw = message_value(:content_raw)
70
+ raw_content = message_value(:raw_content)
71
+ content = message_value(:content)
72
+ <% if postgresql? -%>
73
+ structured_content = "#{content_raw}::jsonb"
74
+ <% elsif mysql? -%>
75
+ structured_content = content_raw
76
+ <% else -%>
77
+ structured_content = content_raw
78
+ <% end -%>
79
+ rendered_content = RubyLLM::Generators::LegacyContentSQL.new(connection).render(content: content, raw: content_raw)
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)} = #{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
+ <% if postgresql? -%>
191
+ tables = %i[<%= chat_table_name %> <%= message_table_name %> ruby_llm_models ruby_llm_tool_calls ruby_llm_usages]
192
+ with_upgrade_safety { execute "ANALYZE #{tables.map { |table| quote_table(table) }.join(', ')}" }
193
+ <% end -%>
194
+ verify_message_content
195
+ verify_tool_results
196
+ verify_usages
197
+ end
198
+
199
+ def verify_message_content
200
+ return unless column_exists?(:<%= message_table_name %>, :content_raw)
201
+
202
+ missing = migration_record(:<%= message_table_name %>)
203
+ .where.not(content_raw: nil)
204
+ .where('raw_content IS NULL OR content IS NULL')
205
+ .limit(1)
206
+ .pick(connection.primary_key(:<%= message_table_name %>))
207
+ raise "Message #{missing} did not preserve content_raw" if missing
208
+ end
209
+
210
+ def verify_tool_results
211
+ result_reference = :<%= v1_tool_call_foreign_key %>
212
+ return unless column_exists?(:<%= message_table_name %>, result_reference)
213
+
214
+ messages = quote_table(:<%= message_table_name %>)
215
+ tool_calls = quote_table(:ruby_llm_tool_calls)
216
+ missing = select_value(<<~SQL)
217
+ SELECT legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
218
+ FROM #{messages} legacy_messages
219
+ <% if copy_mode? -%>
220
+ LEFT JOIN #{quote_table(:<%= v1_tool_call_table_name %>)} legacy_tool_calls
221
+ ON #{message_value(result_reference)} = legacy_tool_calls.#{quoted_primary_key(:<%= v1_tool_call_table_name %>)}
222
+ LEFT JOIN #{tool_calls} migrated_tool_calls
223
+ ON legacy_tool_calls.tool_call_id = migrated_tool_calls.tool_call_id
224
+ AND legacy_tool_calls.#{quote_column(:<%= message_foreign_key %>)} = migrated_tool_calls.message_id
225
+ <% else -%>
226
+ LEFT JOIN #{tool_calls} migrated_tool_calls
227
+ ON #{message_value(result_reference)} = migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)}
228
+ <% end -%>
229
+ AND migrated_tool_calls.result_type = #{connection.quote('<%= message_model_name %>')}
230
+ AND migrated_tool_calls.result_id = legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
231
+ WHERE #{message_value(result_reference)} IS NOT NULL
232
+ AND migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)} IS NULL
233
+ LIMIT 1
234
+ SQL
235
+ raise "Message #{missing} did not preserve its tool result association" if missing
236
+ end
237
+
238
+ def verify_usages
239
+ joins, provider, model = usage_identity_sql
240
+ conditions = legacy_usage_conditions
241
+ return if conditions.empty?
242
+
243
+ candidate = usage_candidate_sql(conditions, joins, '1 = 1')
244
+ missing = select_value(<<~SQL)
245
+ SELECT #{message_value(connection.primary_key(:<%= message_table_name %>))}
246
+ #{candidate}
247
+ LIMIT 1
248
+ SQL
249
+ return unless missing
250
+
251
+ raise "Message #{missing} did not receive its RubyLLM usage entry (provider: #{provider}, model: #{model})"
252
+ end
253
+
254
+ def usage_identity_sql
255
+ providers = []
256
+ models = []
257
+ joins = []
258
+ registry = quote_table(:ruby_llm_models)
259
+ registry_key = quoted_primary_key(:ruby_llm_models)
260
+
261
+ if column_exists?(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
262
+ explicit_model = column_definition(:<%= message_table_name %>, :<%= v1_model_foreign_key %>)
263
+ if explicit_model.type == :string
264
+ models << message_value(:<%= v1_model_foreign_key %>)
265
+ providers << message_value(:provider) if column_exists?(:<%= message_table_name %>, :provider)
266
+ else
267
+ joins << "LEFT JOIN #{registry} legacy_message_models " \
268
+ "ON #{message_value(:<%= v1_model_foreign_key %>)} = legacy_message_models.#{registry_key}"
269
+ providers << "legacy_message_models.#{quote_column(:provider)}"
270
+ models << "legacy_message_models.#{quote_column(:model_id)}"
271
+ end
272
+ end
273
+
274
+ if column_exists?(:<%= chat_table_name %>, :ruby_llm_model_id)
275
+ chats = quote_table(:<%= chat_table_name %>)
276
+ joins << "LEFT JOIN #{chats} legacy_chats " \
277
+ "ON #{message_value(:<%= chat_foreign_key %>)} = legacy_chats.#{quoted_primary_key(:<%= chat_table_name %>)}"
278
+ joins << "LEFT JOIN #{registry} legacy_chat_models " \
279
+ "ON legacy_chats.#{quote_column(:ruby_llm_model_id)} = legacy_chat_models.#{registry_key}"
280
+ providers << "legacy_chat_models.#{quote_column(:provider)}"
281
+ models << "legacy_chat_models.#{quote_column(:model_id)}"
282
+ end
283
+
284
+ [joins.join("\n"), coalesce_sql(providers), coalesce_sql(models)]
285
+ end
286
+
287
+ def legacy_usage_conditions
288
+ token_columns = %i[
289
+ input_tokens output_tokens cached_tokens cache_creation_tokens cache_read_tokens cache_write_tokens
290
+ thinking_tokens total_cost
291
+ ]
292
+ conditions = token_columns.filter_map do |column|
293
+ "#{message_value(column)} IS NOT NULL" if column_exists?(:<%= message_table_name %>, column)
294
+ end
295
+ if column_exists?(:<%= message_table_name %>, :cost_details)
296
+ costs = %w[input output cache_read cache_write thinking total].map do |key|
297
+ "#{legacy_cost_detail(key)} IS NOT NULL"
298
+ end
299
+ conditions << "(#{costs.join(' OR ')})"
300
+ end
301
+ conditions << "#{message_value(:role)} = 'assistant'" if column_exists?(:<%= message_table_name %>, :role)
302
+ conditions
303
+ end
304
+
305
+ def usage_candidate_sql(conditions, joins, range)
306
+ usages = quote_table(:ruby_llm_usages)
307
+ usage_range = range.gsub(message_value(connection.primary_key(:<%= message_table_name %>)), 'existing_usages.message_id')
308
+ <<~SQL
309
+ FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
310
+ #{joins}
311
+ WHERE (#{conditions.join(' OR ')})
312
+ AND #{range}
313
+ AND NOT EXISTS (
314
+ SELECT 1
315
+ FROM #{usages} existing_usages
316
+ WHERE existing_usages.message_type = #{connection.quote('<%= message_model_name %>')}
317
+ AND (#{usage_range})
318
+ AND existing_usages.message_id = #{message_value(connection.primary_key(:<%= message_table_name %>))}
319
+ AND existing_usages.operation = 'chat'
320
+ AND existing_usages.status = 'succeeded'
321
+ )
322
+ SQL
323
+ end
324
+
325
+ def usage_insert_sql(candidate, provider, model)
326
+ columns = %i[
327
+ chat_type chat_id message_type message_id operation provider model status
328
+ input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens
329
+ input_cost output_cost cache_read_cost cache_write_cost thinking_cost total_cost
330
+ created_at updated_at
331
+ ].map { |column| quote_column(column) }.join(', ')
332
+ values = [
333
+ connection.quote('<%= chat_model_name %>'),
334
+ message_value(:<%= chat_foreign_key %>),
335
+ connection.quote('<%= message_model_name %>'),
336
+ message_value(connection.primary_key(:<%= message_table_name %>)),
337
+ connection.quote('chat'),
338
+ provider,
339
+ model,
340
+ connection.quote('succeeded'),
341
+ legacy_message_value(:input_tokens),
342
+ legacy_message_value(:output_tokens),
343
+ preferred_message_value(:cache_read_tokens, :cached_tokens),
344
+ preferred_message_value(:cache_write_tokens, :cache_creation_tokens),
345
+ legacy_message_value(:thinking_tokens),
346
+ legacy_cost_detail('input'),
347
+ legacy_cost_detail('output'),
348
+ legacy_cost_detail('cache_read'),
349
+ legacy_cost_detail('cache_write'),
350
+ legacy_cost_detail('thinking'),
351
+ legacy_total_cost,
352
+ legacy_message_value(:created_at, fallback: 'CURRENT_TIMESTAMP'),
353
+ legacy_message_value(:updated_at, fallback: 'CURRENT_TIMESTAMP')
354
+ ].join(', ')
355
+ <<~SQL
356
+ INSERT INTO #{quote_table(:ruby_llm_usages)} (#{columns})
357
+ SELECT #{values}
358
+ #{candidate}
359
+ SQL
360
+ end
361
+
362
+ def preferred_message_value(preferred, fallback)
363
+ return message_value(preferred) if column_exists?(:<%= message_table_name %>, preferred)
364
+
365
+ legacy_message_value(fallback)
366
+ end
367
+
368
+ def legacy_message_value(column, fallback: 'NULL')
369
+ return fallback unless column_exists?(:<%= message_table_name %>, column)
370
+
371
+ message_value(column)
372
+ end
373
+
374
+ def legacy_cost_detail(key)
375
+ return 'NULL' unless column_exists?(:<%= message_table_name %>, :cost_details)
376
+
377
+ details = message_value(:cost_details)
378
+ <% if postgresql? -%>
379
+ "NULLIF(#{details}::jsonb ->> '#{key}', '')::numeric"
380
+ <% elsif mysql? -%>
381
+ "JSON_UNQUOTE(JSON_EXTRACT(#{details}, '$.#{key}'))"
382
+ <% else -%>
383
+ "json_extract(#{details}, '$.#{key}')"
384
+ <% end -%>
385
+ end
386
+
387
+ def legacy_total_cost
388
+ direct = legacy_message_value(:total_cost)
389
+ detailed = legacy_cost_detail('total')
390
+ return 'NULL' if direct == 'NULL' && detailed == 'NULL'
391
+ return detailed if direct == 'NULL'
392
+ return direct if detailed == 'NULL'
393
+
394
+ "COALESCE(#{direct}, #{detailed})"
395
+ end
396
+
397
+ def create_progress_table
398
+ return if table_exists?(PROGRESS_TABLE)
399
+
400
+ create_table PROGRESS_TABLE, id: false do |table|
401
+ table.string :task, null: false
402
+ table.column :last_id, reference_type_for(:<%= message_table_name %>)
403
+ table.boolean :completed, null: false, default: false
404
+ table.index :task, unique: true
405
+ end
406
+ end
407
+
408
+ def progress_value(task)
409
+ progress_records.where(task: task).pick(:last_id)
410
+ end
411
+
412
+ def completed?(task)
413
+ progress_records.where(task: task, completed: true).exists?
414
+ end
415
+
416
+ def record_progress(task, last_id)
417
+ row = progress_records.where(task: task)
418
+ return row.update_all(last_id: last_id, completed: false) if row.exists?
419
+
420
+ progress_records.insert_all!([{ task: task, last_id: last_id, completed: false }], returning: false)
421
+ end
422
+
423
+ def mark_completed(task)
424
+ row = progress_records.where(task: task)
425
+ return row.update_all(completed: true) if row.exists?
426
+
427
+ progress_records.insert_all!([{ task: task, completed: true }], returning: false)
428
+ end
429
+
430
+ def coalesce_sql(values)
431
+ values = Array(values).compact
432
+ return 'NULL' if values.empty?
433
+ return values.first if values.one?
434
+
435
+ "COALESCE(#{values.join(', ')})"
436
+ end
437
+
438
+ def message_value(column) = "legacy_messages.#{quote_column(column)}"
439
+ def column_definition(table, column) = connection.columns(table).find { |candidate| candidate.name == column.to_s }
440
+ def quoted_primary_key(table) = quote_column(connection.primary_key(table))
441
+ def quote_table(table) = connection.quote_table_name(table)
442
+ def quote_column(column) = connection.quote_column_name(column)
443
+
444
+ def reference_type_for(table)
445
+ column = column_definition(table, connection.primary_key(table))
446
+ raise "Could not determine the primary key type for #{table}" unless column
447
+
448
+ return :bigint if column.type == :integer && column.limit == 8
449
+
450
+ column.type
451
+ end
452
+
453
+ def with_upgrade_safety(&)
454
+ return safety_assured(&) if respond_to?(:safety_assured, true)
455
+
456
+ yield
457
+ end
458
+
459
+ <% end -%>
460
+ def progress_records = migration_record(PROGRESS_TABLE)
461
+
462
+ def migration_record(table)
463
+ Class.new(ActiveRecord::Base) do
464
+ self.table_name = table.to_s
465
+ self.inheritance_column = :_type_disabled
466
+ end
467
+ end
468
+ 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.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
15
+ RubyLLM::Generators::UpgradeMigration.for.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
+ RubyLLM::Generators::UpgradeMigration.for.cleanup { remove_legacy_message_columns }
22
+ return
23
+ <% else -%>
24
+ return unless legacy_columns.any?
25
+
26
+ raise 'RubyLLM 2.0 upgrade progress is missing. Run the finish migration before cleanup.'
27
+ <% end -%>
28
+ end
29
+ unless migration_record(PROGRESS_TABLE).where(task: 'finished', completed: true).exists?
30
+ raise 'Run <%= finish_migration_class_name %> before removing the legacy message columns'
31
+ end
32
+
33
+ <% if copy_mode? -%>
34
+ RubyLLM::Generators::UpgradeMigration.for.cleanup { remove_legacy_message_columns }
35
+ <% else -%>
36
+ remove_legacy_message_columns
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