ruby_llm 2.0.0 → 2.1.0

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 (284) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -8
  3. data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
  5. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
  6. data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
  7. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
  8. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
  9. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
  10. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
  11. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
  13. data/lib/ruby_llm/accounting/usage.rb +86 -49
  14. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  15. data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
  16. data/lib/ruby_llm/active_record/batch.rb +1 -1
  17. data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
  18. data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
  19. data/lib/ruby_llm/active_record/message_methods.rb +32 -7
  20. data/lib/ruby_llm/active_record/model.rb +1 -1
  21. data/lib/ruby_llm/active_record/provider_file.rb +64 -0
  22. data/lib/ruby_llm/active_record/record.rb +19 -0
  23. data/lib/ruby_llm/active_record/tool_call.rb +1 -1
  24. data/lib/ruby_llm/active_record/usage.rb +53 -3
  25. data/lib/ruby_llm/agent.rb +198 -33
  26. data/lib/ruby_llm/aliases.json +66 -8
  27. data/lib/ruby_llm/attachment.rb +54 -10
  28. data/lib/ruby_llm/batch.rb +26 -3
  29. data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
  30. data/lib/ruby_llm/chat/tool_search.rb +26 -0
  31. data/lib/ruby_llm/chat.rb +488 -82
  32. data/lib/ruby_llm/choice.rb +38 -0
  33. data/lib/ruby_llm/configuration.rb +36 -11
  34. data/lib/ruby_llm/context.rb +45 -32
  35. data/lib/ruby_llm/cost.rb +42 -1
  36. data/lib/ruby_llm/embedding.rb +9 -2
  37. data/lib/ruby_llm/error.rb +34 -2
  38. data/lib/ruby_llm/evaluation/assertions.rb +40 -0
  39. data/lib/ruby_llm/evaluation/case.rb +52 -0
  40. data/lib/ruby_llm/evaluation/dataset.rb +65 -0
  41. data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
  42. data/lib/ruby_llm/evaluation/evidence.rb +125 -0
  43. data/lib/ruby_llm/evaluation/minitest.rb +26 -0
  44. data/lib/ruby_llm/evaluation/report.rb +103 -0
  45. data/lib/ruby_llm/evaluation/result.rb +75 -0
  46. data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
  47. data/lib/ruby_llm/evaluation/rspec.rb +23 -0
  48. data/lib/ruby_llm/evaluation/runner.rb +93 -0
  49. data/lib/ruby_llm/evaluation/trial.rb +101 -0
  50. data/lib/ruby_llm/evaluation.rb +289 -0
  51. data/lib/ruby_llm/image.rb +12 -4
  52. data/lib/ruby_llm/judge/builder.rb +51 -0
  53. data/lib/ruby_llm/judge/data.rb +43 -0
  54. data/lib/ruby_llm/judge/question.rb +101 -0
  55. data/lib/ruby_llm/judge.rb +179 -0
  56. data/lib/ruby_llm/judgment.rb +125 -0
  57. data/lib/ruby_llm/mcp/apps.rb +25 -0
  58. data/lib/ruby_llm/mcp/client.rb +213 -0
  59. data/lib/ruby_llm/mcp/collection.rb +56 -0
  60. data/lib/ruby_llm/mcp/content.rb +44 -0
  61. data/lib/ruby_llm/mcp/error.rb +27 -0
  62. data/lib/ruby_llm/mcp/http.rb +379 -0
  63. data/lib/ruby_llm/mcp/input_request.rb +118 -0
  64. data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
  65. data/lib/ruby_llm/mcp/listener.rb +168 -0
  66. data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
  67. data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
  68. data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
  69. data/lib/ruby_llm/mcp/oauth.rb +653 -0
  70. data/lib/ruby_llm/mcp/param_headers.rb +83 -0
  71. data/lib/ruby_llm/mcp/prompt.rb +71 -0
  72. data/lib/ruby_llm/mcp/resource.rb +84 -0
  73. data/lib/ruby_llm/mcp/resource_template.rb +99 -0
  74. data/lib/ruby_llm/mcp/result.rb +76 -0
  75. data/lib/ruby_llm/mcp/session_expired.rb +10 -0
  76. data/lib/ruby_llm/mcp/stdio.rb +250 -0
  77. data/lib/ruby_llm/mcp/task.rb +151 -0
  78. data/lib/ruby_llm/mcp/tool.rb +130 -0
  79. data/lib/ruby_llm/mcp.rb +1101 -0
  80. data/lib/ruby_llm/message.rb +41 -6
  81. data/lib/ruby_llm/model.rb +2 -1
  82. data/lib/ruby_llm/models/aliases.rb +5 -2
  83. data/lib/ruby_llm/models/lookup.rb +80 -0
  84. data/lib/ruby_llm/models/registry.rb +2 -2
  85. data/lib/ruby_llm/models/schema.rb +1 -1
  86. data/lib/ruby_llm/models.json +23082 -10158
  87. data/lib/ruby_llm/models.rb +20 -69
  88. data/lib/ruby_llm/moderation.rb +7 -2
  89. data/lib/ruby_llm/ocr.rb +7 -2
  90. data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
  91. data/lib/ruby_llm/open_telemetry.rb +82 -0
  92. data/lib/ruby_llm/probability.rb +30 -0
  93. data/lib/ruby_llm/progress.rb +44 -0
  94. data/lib/ruby_llm/prompt/context.rb +55 -0
  95. data/lib/ruby_llm/prompt.rb +1 -1
  96. data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
  97. data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
  98. data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
  99. data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
  100. data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
  101. data/lib/ruby_llm/protocol/streaming.rb +38 -31
  102. data/lib/ruby_llm/protocol.rb +192 -40
  103. data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
  104. data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
  105. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
  106. data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
  107. data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
  108. data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
  109. data/lib/ruby_llm/protocols/anthropic.rb +20 -1
  110. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
  111. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
  112. data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
  113. data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
  114. data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
  115. data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
  116. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
  117. data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
  118. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
  119. data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
  120. data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
  121. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
  122. data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
  123. data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
  124. data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
  125. data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
  126. data/lib/ruby_llm/protocols/cohere.rb +1 -0
  127. data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
  128. data/lib/ruby_llm/protocols/converse/media.rb +16 -2
  129. data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
  130. data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
  131. data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
  132. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
  133. data/lib/ruby_llm/protocols/converse.rb +3 -5
  134. data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
  135. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
  136. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
  137. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
  138. data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
  139. data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
  140. data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
  141. data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
  142. data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
  143. data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
  144. data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
  145. data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
  146. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
  147. data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
  148. data/lib/ruby_llm/protocols/gemini.rb +1 -0
  149. data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
  150. data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
  151. data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
  152. data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
  153. data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
  154. data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
  155. data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
  156. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  157. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
  158. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
  159. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
  160. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
  161. data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
  162. data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
  163. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
  164. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
  165. data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
  166. data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
  167. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
  168. data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
  169. data/lib/ruby_llm/protocols/openai/files.rb +0 -7
  170. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
  171. data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
  172. data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
  173. data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
  174. data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
  175. data/lib/ruby_llm/protocols/responses/media.rb +11 -7
  176. data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
  177. data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
  178. data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
  179. data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
  180. data/lib/ruby_llm/protocols/responses.rb +1 -0
  181. data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
  182. data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
  183. data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
  184. data/lib/ruby_llm/protocols/system_one.rb +27 -0
  185. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
  186. data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
  187. data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
  188. data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
  189. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
  190. data/lib/ruby_llm/provider.rb +87 -15
  191. data/lib/ruby_llm/providers/anthropic.rb +23 -0
  192. data/lib/ruby_llm/providers/azure/chat.rb +4 -0
  193. data/lib/ruby_llm/providers/azure/images.rb +2 -2
  194. data/lib/ruby_llm/providers/azure/media.rb +8 -3
  195. data/lib/ruby_llm/providers/azure/videos.rb +7 -2
  196. data/lib/ruby_llm/providers/azure.rb +24 -4
  197. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
  198. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
  199. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
  200. data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
  201. data/lib/ruby_llm/providers/bedrock.rb +38 -6
  202. data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
  203. data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
  204. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  205. data/lib/ruby_llm/providers/gemini.rb +4 -0
  206. data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
  207. data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
  208. data/lib/ruby_llm/providers/hetzner.rb +41 -0
  209. data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
  210. data/lib/ruby_llm/providers/mistral/media.rb +8 -4
  211. data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
  212. data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
  213. data/lib/ruby_llm/providers/ollama/media.rb +6 -2
  214. data/lib/ruby_llm/providers/ollama/models.rb +2 -0
  215. data/lib/ruby_llm/providers/ollama.rb +11 -0
  216. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
  217. data/lib/ruby_llm/providers/openai.rb +14 -0
  218. data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
  219. data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
  220. data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
  221. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
  222. data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
  223. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  224. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  225. data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
  226. data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
  227. data/lib/ruby_llm/providers/perplexity.rb +11 -0
  228. data/lib/ruby_llm/providers/typesafe.rb +32 -0
  229. data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
  230. data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
  231. data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
  232. data/lib/ruby_llm/providers/vertexai.rb +27 -28
  233. data/lib/ruby_llm/providers/xai/chat.rb +6 -0
  234. data/lib/ruby_llm/providers/xai/images.rb +9 -10
  235. data/lib/ruby_llm/providers/xai/responses.rb +10 -8
  236. data/lib/ruby_llm/providers/xai/videos.rb +11 -9
  237. data/lib/ruby_llm/providers/xai.rb +4 -0
  238. data/lib/ruby_llm/railtie.rb +11 -0
  239. data/lib/ruby_llm/request_shape/part.rb +117 -0
  240. data/lib/ruby_llm/request_shape/problem.rb +63 -0
  241. data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
  242. data/lib/ruby_llm/request_shape/turn.rb +54 -0
  243. data/lib/ruby_llm/request_shape.rb +160 -0
  244. data/lib/ruby_llm/rerank.rb +9 -2
  245. data/lib/ruby_llm/research_job.rb +21 -5
  246. data/lib/ruby_llm/score.rb +42 -0
  247. data/lib/ruby_llm/search_results.rb +2 -2
  248. data/lib/ruby_llm/server_tool_call.rb +10 -2
  249. data/lib/ruby_llm/speech.rb +8 -1
  250. data/lib/ruby_llm/support/cancellation.rb +39 -0
  251. data/lib/ruby_llm/support/inspectable.rb +3 -2
  252. data/lib/ruby_llm/support/instrumentation.rb +38 -1
  253. data/lib/ruby_llm/support/process_cache.rb +60 -0
  254. data/lib/ruby_llm/support/progress_reporter.rb +33 -0
  255. data/lib/ruby_llm/thinking.rb +2 -1
  256. data/lib/ruby_llm/tokens.rb +57 -14
  257. data/lib/ruby_llm/tool.rb +50 -3
  258. data/lib/ruby_llm/tool_call.rb +17 -2
  259. data/lib/ruby_llm/transcription.rb +10 -3
  260. data/lib/ruby_llm/transport/connection.rb +127 -44
  261. data/lib/ruby_llm/transport/error_middleware.rb +90 -24
  262. data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
  263. data/lib/ruby_llm/transport/json_response.rb +33 -0
  264. data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
  265. data/lib/ruby_llm/version.rb +1 -1
  266. data/lib/ruby_llm/video.rb +5 -3
  267. data/lib/ruby_llm/video_job.rb +48 -5
  268. data/lib/ruby_llm.rb +106 -5
  269. data/lib/tasks/ruby_llm/evaluations.rb +43 -0
  270. data/lib/tasks/ruby_llm.rake +6 -9
  271. data/skills/rubyllm/SKILL.md +33 -12
  272. metadata +95 -37
  273. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
  274. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
  275. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
  276. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
  277. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
  278. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
  279. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
  280. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
  281. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
  282. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
  283. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
  284. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
@@ -1,247 +0,0 @@
1
- # frozen_string_literal: true
2
- <% if copy_mode? -%>
3
-
4
- require 'generators/ruby_llm/upgrade/upgrade_migration'
5
- <% end -%>
6
-
7
- class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
8
- disable_ddl_transaction!
9
-
10
- PROGRESS_TABLE = :ruby_llm_v2_backfills
11
- BACKFILL_TASKS = %w[message_content tool_results usages].freeze
12
-
13
- def up
14
- <% if copy_mode? -%>
15
- RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
16
- RubyLLM::Generators::UpgradeMigration.for.finish<% if discard_incomplete_tool_calls? %>(discard_incomplete_tool_calls: true)<% end %> do
17
- verify_completed_backfills
18
- enforce_required_defaults
19
- relax_legacy_message_constraints
20
- mark_finished
21
- end
22
- <% else -%>
23
- raise 'Generate this migration with --mode copy' if table_exists?(:ruby_llm_v2_upgrades)
24
- verify_completed_backfills
25
- <% if postgresql? -%>
26
- tables = %i[<%= chat_table_name %> <%= message_table_name %> ruby_llm_models ruby_llm_tool_calls ruby_llm_usages]
27
- with_upgrade_safety { execute "ANALYZE #{tables.map { |table| quote_table(table) }.join(', ')}" }
28
- <% end -%>
29
- verify_message_content
30
- verify_tool_results
31
- verify_usages
32
- enforce_required_defaults
33
- relax_legacy_message_constraints
34
- mark_finished
35
- <% end -%>
36
- end
37
-
38
- def down
39
- raise ActiveRecord::IrreversibleMigration, 'RubyLLM 2.0 changes persisted record ownership'
40
- end
41
-
42
- private
43
-
44
- def relax_legacy_message_constraints
45
- <% if copy_mode? -%>
46
- model_column = :<%= v1_model_foreign_key %>
47
- if column_definition(:<%= chat_table_name %>, model_column)&.null == false
48
- with_upgrade_safety { change_column_null :<%= chat_table_name %>, model_column, true }
49
- end
50
- connection.foreign_keys(:<%= v1_tool_call_table_name %>).each do |key|
51
- next unless key.column == '<%= message_foreign_key %>'
52
-
53
- remove_foreign_key :<%= v1_tool_call_table_name %>, column: key.column
54
- end
55
- <% end -%>
56
- %i[<%= v1_model_foreign_key %> <%= v1_tool_call_foreign_key %>].each do |column|
57
- next unless column_definition(:<%= message_table_name %>, column)&.null == false
58
-
59
- with_upgrade_safety { change_column_null :<%= message_table_name %>, column, true }
60
- end
61
- end
62
-
63
- def mark_finished
64
- progress = migration_record(PROGRESS_TABLE)
65
- return if progress.where(task: 'finished', completed: true).exists?
66
-
67
- progress.insert_all!([{ task: 'finished', completed: true }], returning: false)
68
- end
69
-
70
- def enforce_required_defaults
71
- {
72
- <%= chat_table_name %>: {cancelled: false},
73
- <%= message_table_name %>: {cache_until_here: false},
74
- ruby_llm_tool_calls: {message_type: '<%= message_model_name %>'}
75
- }.each do |table, attributes|
76
- migration_record(table).where(attributes.transform_values { nil }).in_batches(of: 10_000) do |batch|
77
- batch.update_all(attributes)
78
- end
79
- end
80
- <% if copy_mode? -%>
81
- enforce_not_null(:<%= chat_table_name %>, :ruby_llm_model_id)
82
- <% end -%>
83
- enforce_not_null(:<%= chat_table_name %>, :cancelled)
84
- enforce_not_null(:<%= message_table_name %>, :cache_until_here)
85
- enforce_not_null(:ruby_llm_tool_calls, :message_type)
86
- end
87
-
88
- def enforce_not_null(table, column)
89
- missing = migration_record(table).where(column => nil).limit(1).pick(connection.primary_key(table))
90
- raise "#{table} id #{missing} has a NULL #{column}" if missing
91
- return unless column_definition(table, column)&.null
92
-
93
- <% if postgresql? -%>
94
- constraint = "chk_ruby_llm_v2_#{column}_not_null"
95
- unless check_constraint_exists?(table, name: constraint)
96
- add_check_constraint table, "#{quote_column(column)} IS NOT NULL", name: constraint, validate: false
97
- end
98
- validate_check_constraint table, name: constraint
99
- with_upgrade_safety { change_column_null table, column, false }
100
- remove_check_constraint table, name: constraint
101
- <% else -%>
102
- with_upgrade_safety { change_column_null table, column, false }
103
- <% end -%>
104
- end
105
-
106
- def verify_completed_backfills
107
- raise "Run <%= backfill_migration_class_name %> before this migration" unless table_exists?(PROGRESS_TABLE)
108
-
109
- completed = migration_record(PROGRESS_TABLE).where(completed: true).pluck(:task)
110
- missing = BACKFILL_TASKS - completed
111
- return if missing.empty?
112
-
113
- raise "RubyLLM 2.0 backfills are incomplete: #{missing.join(', ')}"
114
- end
115
-
116
- <% unless copy_mode? -%>
117
- def verify_message_content
118
- return unless column_exists?(:<%= message_table_name %>, :content_raw)
119
-
120
- missing = migration_record(:<%= message_table_name %>)
121
- .where.not(content_raw: nil)
122
- .where('raw_content IS NULL OR content IS NULL')
123
- .limit(1)
124
- .pick(connection.primary_key(:<%= message_table_name %>))
125
- raise "Message #{missing} did not preserve content_raw" if missing
126
- end
127
-
128
- def verify_tool_results
129
- result_reference = :<%= v1_tool_call_foreign_key %>
130
- return unless column_exists?(:<%= message_table_name %>, result_reference)
131
-
132
- missing = select_value(<<~SQL)
133
- SELECT legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
134
- FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
135
- <% if copy_mode? -%>
136
- LEFT JOIN #{quote_table(:<%= v1_tool_call_table_name %>)} legacy_tool_calls
137
- ON #{message_value(result_reference)} = legacy_tool_calls.#{quoted_primary_key(:<%= v1_tool_call_table_name %>)}
138
- LEFT JOIN #{quote_table(:ruby_llm_tool_calls)} migrated_tool_calls
139
- ON legacy_tool_calls.tool_call_id = migrated_tool_calls.tool_call_id
140
- AND legacy_tool_calls.#{quote_column(:<%= message_foreign_key %>)} = migrated_tool_calls.message_id
141
- <% else -%>
142
- LEFT JOIN #{quote_table(:ruby_llm_tool_calls)} migrated_tool_calls
143
- ON #{message_value(result_reference)} = migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)}
144
- <% end -%>
145
- AND migrated_tool_calls.result_type = #{connection.quote('<%= message_model_name %>')}
146
- AND migrated_tool_calls.result_id = legacy_messages.#{quoted_primary_key(:<%= message_table_name %>)}
147
- WHERE #{message_value(result_reference)} IS NOT NULL
148
- AND migrated_tool_calls.#{quoted_primary_key(:ruby_llm_tool_calls)} IS NULL
149
- LIMIT 1
150
- SQL
151
- raise "Message #{missing} did not preserve its tool result association" if missing
152
- end
153
-
154
- def verify_usages
155
- joins = usage_identity_joins
156
- conditions = legacy_usage_conditions
157
- return if conditions.empty?
158
-
159
- missing = select_value(<<~SQL)
160
- SELECT #{message_value(connection.primary_key(:<%= message_table_name %>))}
161
- FROM #{quote_table(:<%= message_table_name %>)} legacy_messages
162
- #{joins}
163
- WHERE (#{conditions.join(' OR ')})
164
- AND NOT EXISTS (
165
- SELECT 1
166
- FROM #{quote_table(:ruby_llm_usages)} migrated_usages
167
- WHERE migrated_usages.message_type = #{connection.quote('<%= message_model_name %>')}
168
- AND migrated_usages.message_id = #{message_value(connection.primary_key(:<%= message_table_name %>))}
169
- AND migrated_usages.operation = 'chat'
170
- AND migrated_usages.status = 'succeeded'
171
- )
172
- LIMIT 1
173
- SQL
174
- raise "Message #{missing} did not receive its RubyLLM usage entry" if missing
175
- end
176
-
177
- def usage_identity_joins
178
- joins = []
179
- registry = quote_table(:ruby_llm_models)
180
- registry_key = quoted_primary_key(:ruby_llm_models)
181
- if column_exists?(:<%= message_table_name %>, :<%= v1_model_foreign_key %>) &&
182
- column_definition(:<%= message_table_name %>, :<%= v1_model_foreign_key %>).type != :string
183
- joins << "LEFT JOIN #{registry} legacy_message_models " \
184
- "ON #{message_value(:<%= v1_model_foreign_key %>)} = legacy_message_models.#{registry_key}"
185
- end
186
- if column_exists?(:<%= chat_table_name %>, :ruby_llm_model_id)
187
- joins << "LEFT JOIN #{quote_table(:<%= chat_table_name %>)} legacy_chats " \
188
- "ON #{message_value(:<%= chat_foreign_key %>)} = legacy_chats.#{quoted_primary_key(:<%= chat_table_name %>)}"
189
- joins << "LEFT JOIN #{registry} legacy_chat_models " \
190
- "ON legacy_chats.#{quote_column(:ruby_llm_model_id)} = legacy_chat_models.#{registry_key}"
191
- end
192
- joins.join("\n")
193
- end
194
-
195
- def legacy_usage_conditions
196
- columns = %i[
197
- input_tokens output_tokens cached_tokens cache_creation_tokens cache_read_tokens cache_write_tokens
198
- thinking_tokens total_cost
199
- ]
200
- conditions = columns.filter_map do |column|
201
- "#{message_value(column)} IS NOT NULL" if column_exists?(:<%= message_table_name %>, column)
202
- end
203
- if column_exists?(:<%= message_table_name %>, :cost_details)
204
- costs = %w[input output cache_read cache_write thinking total].map do |key|
205
- "#{legacy_cost_detail(key)} IS NOT NULL"
206
- end
207
- conditions << "(#{costs.join(' OR ')})"
208
- end
209
- conditions << "#{message_value(:role)} = 'assistant'" if column_exists?(:<%= message_table_name %>, :role)
210
- conditions
211
- end
212
-
213
- def legacy_cost_detail(key)
214
- details = message_value(:cost_details)
215
- <% if postgresql? -%>
216
- "NULLIF(#{details}::jsonb ->> '#{key}', '')::numeric"
217
- <% elsif mysql? -%>
218
- "JSON_UNQUOTE(JSON_EXTRACT(#{details}, '$.#{key}'))"
219
- <% else -%>
220
- "json_extract(#{details}, '$.#{key}')"
221
- <% end -%>
222
- end
223
-
224
- <% end -%>
225
- def with_upgrade_safety(&)
226
- return safety_assured(&) if respond_to?(:safety_assured, true)
227
-
228
- yield
229
- end
230
-
231
- def column_definition(table, column) = connection.columns(table).find { |candidate| candidate.name == column.to_s }
232
- <% unless copy_mode? -%>
233
- def message_value(column) = "legacy_messages.#{quote_column(column)}"
234
- def quoted_primary_key(table) = quote_column(connection.primary_key(table))
235
- def quote_table(table) = connection.quote_table_name(table)
236
- <% end -%>
237
- <% if postgresql? || !copy_mode? -%>
238
- def quote_column(column) = connection.quote_column_name(column)
239
- <% end -%>
240
-
241
- def migration_record(table)
242
- Class.new(ActiveRecord::Base) do
243
- self.table_name = table.to_s
244
- self.inheritance_column = :_type_disabled
245
- end
246
- end
247
- end