ruby_llm 1.15.0 → 2.0.0.rc1

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 (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  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 -6
  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 +25 -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 +100 -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 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  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 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  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 +1208 -150
  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 +274 -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 +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  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 +155 -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 +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -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 +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  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 +540 -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 +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -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/protocols/chat_completions/media.rb +121 -0
  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 +17 -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 +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -4,178 +4,136 @@ require 'active_support/concern'
4
4
  require 'active_support/inflector'
5
5
 
6
6
  module RubyLLM
7
+ # Rails integration brings the RubyLLM API to Active Record models, with
8
+ # Active Storage attachments and a persisted message lifecycle for Hotwire
9
+ # streaming and background jobs. Applications own chats and messages;
10
+ # RubyLLM owns the supporting usage, tool-call, registry, and batch records.
7
11
  module ActiveRecord
8
- # Adds chat and message persistence capabilities to ActiveRecord models.
12
+ # Adds the RubyLLM conversation API to application-owned records.
13
+ #
14
+ # class Chat < ApplicationRecord
15
+ # acts_as_chat
16
+ # end
17
+ #
18
+ # class Message < ApplicationRecord
19
+ # acts_as_message
20
+ # has_many_attached :attachments
21
+ # end
22
+ #
23
+ # chat = Chat.create!(model: "gpt-5.6-luna")
24
+ # response = chat.ask "Summarize this report.", with: report.document
25
+ # response.content
26
+ # chat.cost.total
27
+ #
28
+ # The install generator creates the required schema. The chat_ui
29
+ # generator adds controllers, an Active Job, and Turbo Stream views.
30
+ #
9
31
  module ActsAs
10
32
  extend ActiveSupport::Concern
11
33
 
12
- # When ActsAs is included, ensure models are loaded from database
13
- def self.included(base)
34
+ def self.included(base) # :nodoc:
14
35
  super
15
- # Monkey-patch Models to use database when ActsAs is active
16
- RubyLLM::Models.class_eval do
17
- def self.load_models
18
- database_models = read_from_database
19
- return database_models if database_models.any?
20
-
21
- RubyLLM.logger.debug { 'Model registry is empty in database, falling back to JSON registry' }
22
- read_from_json
23
- rescue StandardError => e
24
- RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
25
- read_from_json
26
- end
27
-
28
- def self.read_from_database
29
- model_class = RubyLLM.config.model_registry_class
30
- model_class = model_class.constantize if model_class.is_a?(String)
31
- return [] unless model_class.table_exists?
32
-
33
- model_class.all.map(&:to_llm)
34
- end
35
-
36
- def load_from_database!
37
- @models = self.class.read_from_database
38
- end
39
- end
36
+ RubyLLM.config.model_registry_store ||= RubyLLM::ActiveRecord::Model
37
+ RubyLLM.config.batch_store ||= RubyLLM::ActiveRecord::Batch
40
38
  end
41
39
 
40
+ ##
41
+ # :singleton-method: acts_as_chat
42
+ # :call-seq:
43
+ # acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil)
44
+ #
45
+ # Turns an application record into a persisted RubyLLM chat.
46
+ #
47
+ # class Chat < ApplicationRecord
48
+ # acts_as_chat
49
+ # end
50
+ #
51
+ # Adds the RubyLLM::Chat API (see ChatMethods), a +has_many+
52
+ # association to the messages, a +belongs_to :model+ association to
53
+ # the registry row, and a +has_many :ruby_llm_usages+ association over
54
+ # the attempts the chat paid for.
55
+ #
56
+ # Options:
57
+ #
58
+ # [+messages:+] Name of the messages association. Defaults to +:messages+.
59
+ # [+message_class:+] Class of the messages. Inferred from the association name.
60
+ # [+messages_foreign_key:+] Foreign key on the messages table. Inferred by Rails.
61
+
62
+ ##
63
+ # :singleton-method: acts_as_message
64
+ # :call-seq:
65
+ # acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false)
66
+ #
67
+ # Turns an application record into a persisted RubyLLM message.
68
+ #
69
+ # class Message < ApplicationRecord
70
+ # acts_as_message
71
+ # end
72
+ #
73
+ # Adds the RubyLLM::Message readers (see MessageMethods), a
74
+ # +belongs_to+ association to the chat, and the tool-call and usage
75
+ # associations behind +tool_calls+, +tokens+, and +cost+.
76
+ #
77
+ # Options:
78
+ #
79
+ # [+chat:+] Name of the chat association. Defaults to +:chat+.
80
+ # [+chat_class:+] Class of the chat. Inferred from the association name.
81
+ # [+chat_foreign_key:+] Foreign key on this table. Inferred by Rails.
82
+ # [+touch_chat:+] Touch the chat when a message is saved. Defaults to +false+.
83
+
42
84
  class_methods do # rubocop:disable Metrics/BlockLength
43
- def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
44
- model: :model, model_class: nil, model_foreign_key: nil)
85
+ def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil)
45
86
  include RubyLLM::ActiveRecord::ChatMethods
46
87
 
47
- class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
48
-
88
+ class_attribute :messages_association_name, :message_class
49
89
  self.messages_association_name = messages
50
- self.model_association_name = model
51
90
  self.message_class = (message_class || messages.to_s.classify).to_s
52
- self.model_class = (model_class || model.to_s.classify).to_s
53
91
 
54
92
  has_many messages,
55
- -> { order(created_at: :asc) },
93
+ -> { order(:created_at, :id) },
56
94
  class_name: self.message_class,
57
95
  foreign_key: messages_foreign_key,
58
96
  dependent: :destroy
59
97
 
60
- belongs_to model,
61
- class_name: self.model_class,
62
- foreign_key: model_foreign_key,
98
+ belongs_to :model,
99
+ class_name: 'RubyLLM::ActiveRecord::Model',
100
+ foreign_key: :ruby_llm_model_id,
63
101
  optional: true
64
102
 
65
- define_method :messages_association do
66
- send(messages_association_name)
67
- end
68
-
69
- define_method :model_association do
70
- send(model_association_name)
71
- end
72
-
73
- define_method :'model_association=' do |value|
74
- send("#{model_association_name}=", value)
75
- end
76
- end
77
-
78
- def acts_as_model(chats: :chats, chat_class: nil, chats_foreign_key: nil)
79
- include RubyLLM::ActiveRecord::ModelMethods
80
-
81
- class_attribute :chats_association_name, :chat_class
82
-
83
- self.chats_association_name = chats
84
- self.chat_class = (chat_class || chats.to_s.classify).to_s
85
-
86
- validates :model_id, presence: true, uniqueness: { scope: :provider }
87
- validates :provider, presence: true
88
- validates :name, presence: true
89
-
90
- has_many chats, class_name: self.chat_class, foreign_key: chats_foreign_key
91
-
92
- define_method :chats_association do
93
- send(chats_association_name)
94
- end
103
+ has_many :ruby_llm_usages,
104
+ -> { chronological },
105
+ as: :chat,
106
+ class_name: 'RubyLLM::ActiveRecord::Usage',
107
+ dependent: :destroy
95
108
  end
96
109
 
97
- def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
98
- tool_calls: :tool_calls, tool_call_class: nil, tool_calls_foreign_key: nil,
99
- model: :model, model_class: nil, model_foreign_key: nil)
110
+ def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false)
100
111
  include RubyLLM::ActiveRecord::MessageMethods
101
112
 
102
- class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
103
- :chat_class, :tool_call_class, :model_class
104
-
113
+ class_attribute :chat_association_name, :chat_class
105
114
  self.chat_association_name = chat
106
- self.tool_calls_association_name = tool_calls
107
- self.model_association_name = model
108
115
  self.chat_class = (chat_class || chat.to_s.classify).to_s
109
- self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
110
- self.model_class = (model_class || model.to_s.classify).to_s
111
116
 
112
117
  belongs_to chat,
113
118
  class_name: self.chat_class,
114
119
  foreign_key: chat_foreign_key,
115
120
  touch: touch_chat
116
121
 
117
- has_many tool_calls,
118
- class_name: self.tool_call_class,
119
- foreign_key: tool_calls_foreign_key,
122
+ has_many :ruby_llm_tool_calls,
123
+ as: :message,
124
+ class_name: 'RubyLLM::ActiveRecord::ToolCall',
120
125
  dependent: :destroy
121
126
 
122
- belongs_to :parent_tool_call,
123
- class_name: self.tool_call_class,
124
- foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
125
- optional: true
126
-
127
- has_many :tool_results,
128
- through: tool_calls,
129
- source: :result,
130
- class_name: name
131
-
132
- belongs_to model,
133
- class_name: self.model_class,
134
- foreign_key: model_foreign_key,
135
- optional: true
136
-
137
- delegate :tool_call?, :tool_result?, to: :to_llm
138
-
139
- define_method :chat_association do
140
- send(chat_association_name)
141
- end
142
-
143
- define_method :tool_calls_association do
144
- send(tool_calls_association_name)
145
- end
146
-
147
- define_method :model_association do
148
- send(model_association_name)
149
- end
150
- end
151
-
152
- def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
153
- result: :result, result_class: nil, result_foreign_key: nil)
154
- include RubyLLM::ActiveRecord::ToolCallMethods
155
-
156
- class_attribute :message_association_name, :result_association_name, :message_class, :result_class
157
-
158
- self.message_association_name = message
159
- self.result_association_name = result
160
- self.message_class = (message_class || message.to_s.classify).to_s
161
- self.result_class = (result_class || self.message_class).to_s
162
-
163
- belongs_to message,
164
- class_name: self.message_class,
165
- foreign_key: message_foreign_key
166
-
167
- has_one result,
168
- class_name: self.result_class,
169
- foreign_key: result_foreign_key,
127
+ has_one :ruby_llm_parent_tool_call,
128
+ as: :result,
129
+ class_name: 'RubyLLM::ActiveRecord::ToolCall',
170
130
  dependent: :nullify
171
131
 
172
- define_method :message_association do
173
- send(message_association_name)
174
- end
175
-
176
- define_method :result_association do
177
- send(result_association_name)
178
- end
132
+ has_many :ruby_llm_usages,
133
+ -> { chronological },
134
+ as: :message,
135
+ class_name: 'RubyLLM::ActiveRecord::Usage',
136
+ dependent: :nullify
179
137
  end
180
138
  end
181
139
  end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'tempfile'
5
+
6
+ module RubyLLM
7
+ module ActiveRecord
8
+ module AttachmentHelpers # :nodoc:
9
+ private
10
+
11
+ def persist_content(message_record, attachments)
12
+ return unless message_record.respond_to?(:attachments)
13
+
14
+ attachables = prepare_for_active_storage(attachments)
15
+ message_record.attachments.attach(attachables) if attachables.any?
16
+ end
17
+
18
+ def prepare_for_active_storage(attachments)
19
+ Support::Utils.to_safe_array(attachments).filter_map do |attachment|
20
+ case attachment
21
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
22
+ attachment
23
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
24
+ active_storage_blobs(attachment)
25
+ when Hash
26
+ attachment.values.map { |v| prepare_for_active_storage(v) }
27
+ else
28
+ convert_to_active_storage_format(attachment)
29
+ end
30
+ end.flatten.compact
31
+ end
32
+
33
+ def convert_to_active_storage_format(source)
34
+ return if source.blank?
35
+
36
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
37
+
38
+ if attachment.active_storage?
39
+ active_storage_blobs(attachment.source)
40
+ else
41
+ {
42
+ io: StringIO.new(attachment.content),
43
+ filename: attachment.filename,
44
+ content_type: attachment.mime_type
45
+ }
46
+ end
47
+ rescue StandardError => e
48
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
49
+ nil
50
+ end
51
+
52
+ def active_storage_blobs(attachment)
53
+ case attachment
54
+ when ActiveStorage::Blob then attachment
55
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One then attachment.blob
56
+ when ActiveStorage::Attached::Many then attachment.blobs
57
+ end
58
+ end
59
+
60
+ def plain_text_content(content_value)
61
+ return content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
62
+
63
+ content_value
64
+ end
65
+
66
+ def action_text_attachment_sources(content_value)
67
+ return [] unless content_value.respond_to?(:body)
68
+
69
+ body = content_value.body
70
+ return [] unless body.respond_to?(:attachables)
71
+
72
+ body.attachables.flat_map { |attachable| action_text_attachable_sources(attachable) }.compact
73
+ end
74
+
75
+ def action_text_attachable_sources(attachable)
76
+ source = active_storage_blobs(attachable)
77
+ source ||= attachable.blob if attachable.respond_to?(:blob)
78
+
79
+ Support::Utils.to_safe_array(source)
80
+ end
81
+
82
+ def content_attachments?(action_text_attachments)
83
+ action_text_attachments.any? || active_storage_attachments?
84
+ end
85
+
86
+ def active_storage_attachments?
87
+ respond_to?(:attachments) && attachments.attached?
88
+ end
89
+
90
+ def collect_attachments(action_text_attachments)
91
+ list = action_text_attachments.map { |source| RubyLLM::Attachment.new(source) }
92
+ return list unless active_storage_attachments?
93
+
94
+ list + attachment_sources.map { |attachment, attachable| stored_attachment(attachment, attachable) }
95
+ end
96
+
97
+ def attachment_sources
98
+ change = pending_attachment_change
99
+ return attachments.map { |attachment| [attachment, nil] } unless pending_attachment_change?(change)
100
+
101
+ change.attachments.zip(change.attachables)
102
+ end
103
+
104
+ def pending_attachment_change
105
+ attachment_changes['attachments'] if respond_to?(:attachment_changes)
106
+ end
107
+
108
+ def pending_attachment_change?(change)
109
+ change.respond_to?(:attachments) && change.respond_to?(:attachables)
110
+ end
111
+
112
+ def stored_attachment(attachment, attachable)
113
+ if pending_upload_attachable?(attachable)
114
+ pending_upload_attachment(attachable)
115
+ else
116
+ tempfile = download_attachment(attachment)
117
+ RubyLLM::Attachment.new(tempfile, filename: attachment.filename.to_s)
118
+ end
119
+ end
120
+
121
+ def pending_upload_attachable?(attachable)
122
+ return false if attachable.nil? || attachable.is_a?(String)
123
+ return false if instance_of_class?(attachable, 'ActiveStorage::Blob')
124
+
125
+ uploaded_file?(attachable) || active_storage_upload_hash?(attachable) ||
126
+ attachable.is_a?(File) || pathname?(attachable)
127
+ end
128
+
129
+ def uploaded_file?(attachable)
130
+ instance_of_class?(attachable, 'ActionDispatch::Http::UploadedFile') ||
131
+ instance_of_class?(attachable, 'Rack::Test::UploadedFile')
132
+ end
133
+
134
+ def active_storage_upload_hash?(attachable)
135
+ attachable.is_a?(Hash) && attachment_hash_io(attachable).present?
136
+ end
137
+
138
+ def pathname?(attachable)
139
+ defined?(Pathname) && attachable.is_a?(Pathname)
140
+ end
141
+
142
+ def pending_upload_attachment(attachable)
143
+ if attachable.is_a?(Hash)
144
+ RubyLLM::Attachment.new(attachment_hash_io(attachable), filename: attachment_hash_filename(attachable))
145
+ else
146
+ RubyLLM::Attachment.new(attachable)
147
+ end
148
+ end
149
+
150
+ def attachment_hash_io(attachable)
151
+ attachable[:io] || attachable['io']
152
+ end
153
+
154
+ def attachment_hash_filename(attachable)
155
+ filename = attachable[:filename] || attachable['filename']
156
+ filename&.to_s
157
+ end
158
+
159
+ def instance_of_class?(object, class_name)
160
+ Object.const_get(class_name).then { |klass| object.is_a?(klass) }
161
+ rescue NameError
162
+ false
163
+ end
164
+
165
+ def download_attachment(attachment)
166
+ ext = File.extname(attachment.filename.to_s)
167
+ basename = File.basename(attachment.filename.to_s, ext)
168
+ tempfile = Tempfile.new([basename, ext])
169
+ tempfile.binmode
170
+
171
+ attachment.download { |chunk| tempfile.write(chunk) }
172
+
173
+ tempfile.flush
174
+ tempfile.rewind
175
+ @_tempfiles << tempfile
176
+ tempfile
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # RubyLLM's private persistence for provider-side batches.
6
+ class Batch < ::ActiveRecord::Base # :nodoc:
7
+ self.table_name = 'ruby_llm_batches'
8
+
9
+ STATUSES = %w[pending succeeded failed cancelled].freeze
10
+
11
+ validates :provider_batch_id, :provider, :status, presence: true
12
+ validates :status, inclusion: { in: STATUSES }
13
+
14
+ class << self
15
+ def persist(batch, chats)
16
+ records = Array(chats)
17
+ return unless table_exists?
18
+ return unless persistable_chats?(records)
19
+
20
+ create!(
21
+ provider_batch_id: batch.id,
22
+ provider: batch.provider,
23
+ status: batch.status,
24
+ raw_status: batch.raw_status,
25
+ completed: batch.complete?,
26
+ request_counts: batch.request_counts,
27
+ reported_cost: batch.reported_cost&.to_h,
28
+ batch_protocol: batch.batch_protocol,
29
+ chat_type: records.first.class.polymorphic_name,
30
+ chat_ids: records.map(&:id)
31
+ )
32
+ end
33
+
34
+ def fetch(id, provider: nil, context: nil)
35
+ find_record(id, provider:)&.to_llm(context:, store: self)
36
+ end
37
+
38
+ def sync(batch)
39
+ find_record(batch.id, provider: batch.provider)&.sync_from(batch)
40
+ end
41
+
42
+ private
43
+
44
+ def find_record(id, provider: nil)
45
+ return unless table_exists?
46
+
47
+ scope = where(provider_batch_id: id)
48
+ scope = scope.where(provider: provider.to_s) if provider
49
+ scope.order(id: :desc).first
50
+ end
51
+
52
+ def persistable_chats?(records)
53
+ records.all? { |chat| persisted_chat?(chat) } && records.map(&:class).uniq.one?
54
+ end
55
+
56
+ def persisted_chat?(chat)
57
+ chat.respond_to?(:to_llm) && chat.respond_to?(:id) && chat.persisted?
58
+ end
59
+ end
60
+
61
+ def chats
62
+ klass = chat_type.constantize
63
+ by_id = klass.where(klass.primary_key => Array(chat_ids))
64
+ .index_by { |chat| chat.public_send(klass.primary_key) }
65
+ Array(chat_ids).map { |id| by_id[id] }
66
+ end
67
+
68
+ def to_llm(context: nil, store: nil)
69
+ config = context&.config || RubyLLM.config
70
+ provider_instance = RubyLLM::Provider.resolve!(provider).new(config)
71
+ RubyLLM::Batch.new(
72
+ provider: provider_instance,
73
+ chats: chats.map { |chat| chat&.to_llm },
74
+ id: provider_batch_id,
75
+ raw_status: raw_status,
76
+ completed: completed,
77
+ request_counts: request_counts,
78
+ reported_cost: reported_cost && RubyLLM::Cost.from_h(reported_cost),
79
+ batch_protocol: batch_protocol,
80
+ store: store
81
+ )
82
+ end
83
+
84
+ def sync_from(batch)
85
+ attributes = {
86
+ status: batch.status,
87
+ raw_status: batch.raw_status,
88
+ completed: batch.complete?,
89
+ request_counts: batch.request_counts,
90
+ batch_protocol: batch.batch_protocol
91
+ }
92
+ attributes[:reported_cost] = batch.reported_cost.to_h if batch.reported_cost
93
+ update!(attributes)
94
+ end
95
+ end
96
+ end
97
+ end