ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -4,153 +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
- def self.included(base)
34
+ def self.included(base) # :nodoc:
13
35
  super
14
- RubyLLM.config.model_registry_source ||= RubyLLM::ModelRegistry::ActiveRecordSource.new
36
+ RubyLLM.config.model_registry_store ||= RubyLLM::ActiveRecord::Model
37
+ RubyLLM.config.batch_store ||= RubyLLM::ActiveRecord::Batch
15
38
  end
16
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
+
17
84
  class_methods do # rubocop:disable Metrics/BlockLength
18
- def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
19
- model: :model, model_class: nil, model_foreign_key: nil)
85
+ def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil)
20
86
  include RubyLLM::ActiveRecord::ChatMethods
21
87
 
22
- class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
23
-
88
+ class_attribute :messages_association_name, :message_class
24
89
  self.messages_association_name = messages
25
- self.model_association_name = model
26
90
  self.message_class = (message_class || messages.to_s.classify).to_s
27
- self.model_class = (model_class || model.to_s.classify).to_s
28
91
 
29
92
  has_many messages,
30
- -> { order(created_at: :asc) },
93
+ -> { order(:created_at, :id) },
31
94
  class_name: self.message_class,
32
95
  foreign_key: messages_foreign_key,
33
96
  dependent: :destroy
34
97
 
35
- belongs_to model,
36
- class_name: self.model_class,
37
- foreign_key: model_foreign_key,
98
+ belongs_to :model,
99
+ class_name: 'RubyLLM::ActiveRecord::Model',
100
+ foreign_key: :ruby_llm_model_id,
38
101
  optional: true
39
102
 
40
- define_method :messages_association do
41
- send(messages_association_name)
42
- end
43
-
44
- define_method :model_association do
45
- send(model_association_name)
46
- end
47
-
48
- define_method :'model_association=' do |value|
49
- send("#{model_association_name}=", value)
50
- end
51
- end
52
-
53
- def acts_as_model(chats: :chats, chat_class: nil, chats_foreign_key: nil)
54
- include RubyLLM::ActiveRecord::ModelMethods
55
-
56
- class_attribute :chats_association_name, :chat_class
57
-
58
- self.chats_association_name = chats
59
- self.chat_class = (chat_class || chats.to_s.classify).to_s
60
-
61
- validates :model_id, presence: true, uniqueness: { scope: :provider }
62
- validates :provider, presence: true
63
- validates :name, presence: true
64
-
65
- has_many chats, class_name: self.chat_class, foreign_key: chats_foreign_key
66
-
67
- define_method :chats_association do
68
- send(chats_association_name)
69
- end
103
+ has_many :ruby_llm_usages,
104
+ -> { chronological },
105
+ as: :chat,
106
+ class_name: 'RubyLLM::ActiveRecord::Usage',
107
+ dependent: :destroy
70
108
  end
71
109
 
72
- def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
73
- tool_calls: :tool_calls, tool_call_class: nil, tool_calls_foreign_key: nil,
74
- 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)
75
111
  include RubyLLM::ActiveRecord::MessageMethods
76
112
 
77
- class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
78
- :chat_class, :tool_call_class, :model_class
79
-
113
+ class_attribute :chat_association_name, :chat_class
80
114
  self.chat_association_name = chat
81
- self.tool_calls_association_name = tool_calls
82
- self.model_association_name = model
83
115
  self.chat_class = (chat_class || chat.to_s.classify).to_s
84
- self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
85
- self.model_class = (model_class || model.to_s.classify).to_s
86
116
 
87
117
  belongs_to chat,
88
118
  class_name: self.chat_class,
89
119
  foreign_key: chat_foreign_key,
90
120
  touch: touch_chat
91
121
 
92
- has_many tool_calls,
93
- class_name: self.tool_call_class,
94
- foreign_key: tool_calls_foreign_key,
122
+ has_many :ruby_llm_tool_calls,
123
+ as: :message,
124
+ class_name: 'RubyLLM::ActiveRecord::ToolCall',
95
125
  dependent: :destroy
96
126
 
97
- belongs_to :parent_tool_call,
98
- class_name: self.tool_call_class,
99
- foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
100
- optional: true
101
-
102
- has_many :tool_results,
103
- through: tool_calls,
104
- source: :result,
105
- class_name: name
106
-
107
- belongs_to model,
108
- class_name: self.model_class,
109
- foreign_key: model_foreign_key,
110
- optional: true
111
-
112
- delegate :tool_call?, :tool_result?, to: :to_llm
113
-
114
- define_method :chat_association do
115
- send(chat_association_name)
116
- end
117
-
118
- define_method :tool_calls_association do
119
- send(tool_calls_association_name)
120
- end
121
-
122
- define_method :model_association do
123
- send(model_association_name)
124
- end
125
- end
126
-
127
- def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
128
- result: :result, result_class: nil, result_foreign_key: nil)
129
- include RubyLLM::ActiveRecord::ToolCallMethods
130
-
131
- class_attribute :message_association_name, :result_association_name, :message_class, :result_class
132
-
133
- self.message_association_name = message
134
- self.result_association_name = result
135
- self.message_class = (message_class || message.to_s.classify).to_s
136
- self.result_class = (result_class || self.message_class).to_s
137
-
138
- belongs_to message,
139
- class_name: self.message_class,
140
- foreign_key: message_foreign_key
141
-
142
- has_one result,
143
- class_name: self.result_class,
144
- foreign_key: result_foreign_key,
127
+ has_one :ruby_llm_parent_tool_call,
128
+ as: :result,
129
+ class_name: 'RubyLLM::ActiveRecord::ToolCall',
145
130
  dependent: :nullify
146
131
 
147
- define_method :message_association do
148
- send(message_association_name)
149
- end
150
-
151
- define_method :result_association do
152
- send(result_association_name)
153
- end
132
+ has_many :ruby_llm_usages,
133
+ -> { chronological },
134
+ as: :message,
135
+ class_name: 'RubyLLM::ActiveRecord::Usage',
136
+ dependent: :nullify
154
137
  end
155
138
  end
156
139
  end
@@ -0,0 +1,186 @@
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_with_blobs.map { |attachment| [attachment, nil] } unless pending_attachment_change?(change)
100
+
101
+ change.attachments.zip(change.attachables)
102
+ end
103
+
104
+ def attachments_with_blobs
105
+ records = attachments.to_a
106
+ ::ActiveRecord::Associations::Preloader.new(records: records, associations: :blob).call
107
+ records
108
+ end
109
+
110
+ def pending_attachment_change
111
+ attachment_changes['attachments'] if respond_to?(:attachment_changes)
112
+ end
113
+
114
+ def pending_attachment_change?(change)
115
+ change.respond_to?(:attachments) && change.respond_to?(:attachables)
116
+ end
117
+
118
+ def stored_attachment(attachment, attachable)
119
+ if pending_upload_attachable?(attachable)
120
+ pending_upload_attachment(attachable)
121
+ else
122
+ tempfile = download_attachment(attachment)
123
+ RubyLLM::Attachment.new(tempfile, filename: attachment.filename.to_s)
124
+ end
125
+ end
126
+
127
+ def pending_upload_attachable?(attachable)
128
+ return false if attachable.nil? || attachable.is_a?(String)
129
+ return false if instance_of_class?(attachable, 'ActiveStorage::Blob')
130
+
131
+ uploaded_file?(attachable) || active_storage_upload_hash?(attachable) ||
132
+ attachable.is_a?(File) || pathname?(attachable)
133
+ end
134
+
135
+ def uploaded_file?(attachable)
136
+ instance_of_class?(attachable, 'ActionDispatch::Http::UploadedFile') ||
137
+ instance_of_class?(attachable, 'Rack::Test::UploadedFile')
138
+ end
139
+
140
+ def active_storage_upload_hash?(attachable)
141
+ attachable.is_a?(Hash) && attachment_hash_io(attachable).present?
142
+ end
143
+
144
+ def pathname?(attachable)
145
+ defined?(Pathname) && attachable.is_a?(Pathname)
146
+ end
147
+
148
+ def pending_upload_attachment(attachable)
149
+ if attachable.is_a?(Hash)
150
+ RubyLLM::Attachment.new(attachment_hash_io(attachable), filename: attachment_hash_filename(attachable))
151
+ else
152
+ RubyLLM::Attachment.new(attachable)
153
+ end
154
+ end
155
+
156
+ def attachment_hash_io(attachable)
157
+ attachable[:io] || attachable['io']
158
+ end
159
+
160
+ def attachment_hash_filename(attachable)
161
+ filename = attachable[:filename] || attachable['filename']
162
+ filename&.to_s
163
+ end
164
+
165
+ def instance_of_class?(object, class_name)
166
+ Object.const_get(class_name).then { |klass| object.is_a?(klass) }
167
+ rescue NameError
168
+ false
169
+ end
170
+
171
+ def download_attachment(attachment)
172
+ ext = File.extname(attachment.filename.to_s)
173
+ basename = File.basename(attachment.filename.to_s, ext)
174
+ tempfile = Tempfile.new([basename, ext])
175
+ tempfile.binmode
176
+
177
+ attachment.download { |chunk| tempfile.write(chunk) }
178
+
179
+ tempfile.flush
180
+ tempfile.rewind
181
+ @_tempfiles << tempfile
182
+ tempfile
183
+ end
184
+ end
185
+ end
186
+ 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