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
@@ -1,213 +1,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'ruby_llm/active_record/attachment_helpers'
4
5
  require 'ruby_llm/active_record/payload_helpers'
5
6
 
6
7
  module RubyLLM
7
8
  module ActiveRecord
8
- # Methods mixed into message models.
9
+ # Maps an application-owned message record onto RubyLLM's public Message
10
+ # API while keeping accounting and tool-call rows private to the gem.
9
11
  module MessageMethods
10
12
  extend ActiveSupport::Concern
11
13
  include PayloadHelpers
14
+ include AttachmentHelpers
12
15
 
13
- class_methods do
14
- attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
16
+ def chat_association # :nodoc:
17
+ send(chat_association_name)
15
18
  end
16
19
 
20
+ # Converts this record to a RubyLLM::Message.
17
21
  def to_llm
22
+ entries = ruby_llm_usage_entries
18
23
  RubyLLM::Message.new(
19
24
  role: role.to_sym,
20
25
  content: extract_content,
26
+ attachments: extract_attachments,
21
27
  thinking: thinking,
22
- tokens: tokens,
23
- tool_calls: extract_tool_calls,
24
- tool_call_id: extract_tool_call_id,
25
- model_id: model_association&.model_id
28
+ citations: citations,
29
+ server_tool_calls: server_tool_calls,
30
+ raw_content: optional_column(:raw_content),
31
+ raw_reasoning: optional_column(:raw_reasoning),
32
+ usage_entries: entries,
33
+ tool_calls: tool_calls,
34
+ tool_call_id: parent_tool_call&.id,
35
+ finish_reason: optional_column(:finish_reason),
36
+ model: entries.reverse.find(&:succeeded?)&.model,
37
+ cache_until_here: cache_until_here?
26
38
  )
27
39
  end
28
40
 
29
- def thinking
30
- RubyLLM::Thinking.build(
31
- text: thinking_text_value,
32
- signature: thinking_signature_value
33
- )
41
+ def ruby_llm_usage_entries # :nodoc:
42
+ ruby_llm_usages.map(&:to_entry)
34
43
  end
35
44
 
36
- def tokens
37
- RubyLLM::Tokens.build(
38
- input: input_tokens,
39
- output: output_tokens,
40
- cached: cached_value,
41
- cache_creation: cache_creation_value,
42
- thinking: thinking_tokens_value
43
- )
45
+ # Returns the model ID from the last successful attempt, or +nil+.
46
+ def model
47
+ to_llm.model
44
48
  end
45
49
 
46
- def cost
47
- RubyLLM::Cost.new(tokens:, model: model_association)
50
+ # Returns the model from the registry, or +nil+ if it is unknown.
51
+ def model_info
52
+ to_llm.model_info
48
53
  end
49
54
 
50
- def cache_read_tokens
51
- cached_value
55
+ # Marks the message as a prompt cache boundary and returns it.
56
+ def cache_until_here
57
+ update!(cache_until_here: true)
58
+ self
52
59
  end
53
60
 
54
- def cache_write_tokens
55
- cache_creation_value
61
+ # Returns +true+ if the message is a prompt cache boundary.
62
+ def cache_until_here?
63
+ optional_column(:cache_until_here) || false
56
64
  end
57
65
 
58
- def to_partial_path
59
- partial_prefix = self.class.name.underscore.pluralize
60
- role_partial = if to_llm.tool_call?
61
- 'tool_calls'
62
- elsif role.to_s == 'tool'
63
- 'tool'
64
- else
65
- role.to_s.presence || 'assistant'
66
- end
67
- "#{partial_prefix}/#{role_partial}"
66
+ # Returns the reasoning the model returned as a RubyLLM::Thinking, or +nil+.
67
+ def thinking
68
+ RubyLLM::Thinking.build(
69
+ text: optional_column(:thinking_text),
70
+ signature: optional_column(:thinking_signature)
71
+ )
68
72
  end
69
73
 
70
- def tool_error_message
71
- payload_error_message(content)
74
+ # Returns the citations as an array of RubyLLM::Citation.
75
+ def citations
76
+ Array(optional_column(:citations)).map { |citation| RubyLLM::Citation.from_h(citation) }
72
77
  end
73
78
 
74
- private
75
-
76
- def thinking_text_value
77
- has_attribute?(:thinking_text) ? self[:thinking_text] : nil
79
+ # Returns the provider-executed tool steps as an array of RubyLLM::ServerToolCall.
80
+ def server_tool_calls
81
+ Array(optional_column(:server_tool_calls)).map { |call| RubyLLM::ServerToolCall.from_h(call) }
78
82
  end
79
83
 
80
- def thinking_signature_value
81
- has_attribute?(:thinking_signature) ? self[:thinking_signature] : nil
84
+ # Returns the token usage across every attempt that produced the message.
85
+ def tokens
86
+ RubyLLM::Tokens.aggregate(ruby_llm_usages.map(&:tokens))
82
87
  end
83
88
 
84
- def cached_value
85
- has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
89
+ # Returns the cost across every attempt that produced the message, as a RubyLLM::Cost.
90
+ def cost
91
+ records = ruby_llm_usages.to_a
92
+ RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?))
86
93
  end
87
94
 
88
- def cache_creation_value
89
- has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
95
+ # Returns the tool calls as RubyLLM::ToolCall values keyed by call id.
96
+ def tool_calls
97
+ ruby_llm_tool_calls.to_h { |record| [record.tool_call_id, record.to_llm] }
90
98
  end
91
99
 
92
- def thinking_tokens_value
93
- has_attribute?(:thinking_tokens) ? self[:thinking_tokens] : nil
100
+ # Returns the tool call this message answers, or +nil+.
101
+ def parent_tool_call
102
+ ruby_llm_parent_tool_call&.to_llm
94
103
  end
95
104
 
96
- def extract_tool_calls
97
- tool_calls_association.to_h do |tool_call|
98
- [
99
- tool_call.tool_call_id,
100
- RubyLLM::ToolCall.new(
101
- id: tool_call.tool_call_id,
102
- name: tool_call.name,
103
- arguments: tool_call.arguments,
104
- thought_signature: tool_call.try(:thought_signature)
105
- )
106
- ]
107
- end
105
+ # Returns the message records that answer this message's tool calls.
106
+ def tool_results
107
+ ruby_llm_tool_calls.filter_map(&:result)
108
108
  end
109
109
 
110
- def extract_tool_call_id
111
- parent_tool_call&.tool_call_id
110
+ # Returns +true+ if the message carries tool calls.
111
+ def tool_call?
112
+ ruby_llm_tool_calls.any?
112
113
  end
113
114
 
114
- def extract_content
115
- return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
116
-
117
- content_value = content
118
- content_value = content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
119
-
120
- return content_value unless respond_to?(:attachments) && attachments.attached?
121
-
122
- RubyLLM::Content.new(content_value).tap do |content_obj|
123
- @_tempfiles = []
124
-
125
- attachment_sources.each do |attachment, attachable|
126
- add_attachment_to_content(content_obj, attachment, attachable)
127
- end
128
- end
115
+ # Returns +true+ if the message answers a tool call.
116
+ def tool_result?
117
+ ruby_llm_parent_tool_call.present?
129
118
  end
130
119
 
131
- def attachment_sources
132
- change = pending_attachment_change
133
- return attachments.map { |attachment| [attachment, nil] } unless pending_attachment_change?(change)
134
-
135
- change.attachments.zip(change.attachables)
120
+ # Returns the partial path for the message by role, such as +messages/assistant+
121
+ # or +messages/tool_calls+.
122
+ def to_partial_path
123
+ partial_prefix = self.class.name.underscore.pluralize
124
+ role_partial = if tool_call?
125
+ 'tool_calls'
126
+ elsif role.to_s == 'tool'
127
+ 'tool'
128
+ else
129
+ role.to_s.presence || 'assistant'
130
+ end
131
+ "#{partial_prefix}/#{role_partial}"
136
132
  end
137
133
 
138
- def pending_attachment_change
139
- attachment_changes['attachments'] if respond_to?(:attachment_changes)
134
+ # Returns the error text of a failed tool result, or +nil+.
135
+ def tool_error_message
136
+ payload_error_message(extract_content)
140
137
  end
141
138
 
142
- def pending_attachment_change?(change)
143
- change.respond_to?(:attachments) && change.respond_to?(:attachables)
139
+ # Returns why the model stopped, as the Symbol RubyLLM::Message#finish_reason reports.
140
+ def finish_reason
141
+ optional_column(:finish_reason)&.to_sym
144
142
  end
145
143
 
146
- def add_attachment_to_content(content_obj, attachment, attachable)
147
- if pending_upload_attachable?(attachable)
148
- add_pending_upload_attachment(content_obj, attachable)
149
- else
150
- tempfile = download_attachment(attachment)
151
- content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
152
- end
144
+ # Returns +true+ if the model finished normally without requesting tools.
145
+ def stopped?
146
+ to_llm.stopped?
153
147
  end
154
148
 
155
- def pending_upload_attachable?(attachable)
156
- return false if attachable.nil? || attachable.is_a?(String)
157
- return false if instance_of_class?(attachable, 'ActiveStorage::Blob')
158
-
159
- uploaded_file?(attachable) || active_storage_upload_hash?(attachable) ||
160
- attachable.is_a?(File) || pathname?(attachable)
149
+ # Returns +true+ if a token limit stopped the response.
150
+ def max_tokens?
151
+ to_llm.max_tokens?
161
152
  end
162
153
 
163
- def uploaded_file?(attachable)
164
- instance_of_class?(attachable, 'ActionDispatch::Http::UploadedFile') ||
165
- instance_of_class?(attachable, 'Rack::Test::UploadedFile')
154
+ # Returns +true+ if the model stopped to request tool calls.
155
+ def tool_call_stop?
156
+ to_llm.tool_call_stop?
166
157
  end
167
158
 
168
- def active_storage_upload_hash?(attachable)
169
- attachable.is_a?(Hash) && attachment_hash_io(attachable).present?
159
+ # Returns +true+ if a provider safety filter stopped the response.
160
+ def content_filtered?
161
+ to_llm.content_filtered?
170
162
  end
171
163
 
172
- def pathname?(attachable)
173
- defined?(Pathname) && attachable.is_a?(Pathname)
174
- end
164
+ # Returns the content parsed as JSON, or +nil+ when the message has no text.
165
+ def parsed
166
+ text = extract_content
167
+ return if text.nil? || text.empty?
175
168
 
176
- def add_pending_upload_attachment(content_obj, attachable)
177
- if attachable.is_a?(Hash)
178
- content_obj.add_attachment(attachment_hash_io(attachable), filename: attachment_hash_filename(attachable))
179
- else
180
- content_obj.add_attachment(attachable)
181
- end
169
+ JSON.parse(text)
182
170
  end
183
171
 
184
- def attachment_hash_io(attachable)
185
- attachable[:io] || attachable['io']
186
- end
172
+ private
187
173
 
188
- def attachment_hash_filename(attachable)
189
- filename = attachable[:filename] || attachable['filename']
190
- filename&.to_s
174
+ def optional_column(name)
175
+ self[name] if has_attribute?(name)
191
176
  end
192
177
 
193
- def instance_of_class?(object, class_name)
194
- Object.const_get(class_name).then { |klass| object.is_a?(klass) }
195
- rescue NameError
196
- false
178
+ def extract_content
179
+ plain_text_content(content)
197
180
  end
198
181
 
199
- def download_attachment(attachment)
200
- ext = File.extname(attachment.filename.to_s)
201
- basename = File.basename(attachment.filename.to_s, ext)
202
- tempfile = Tempfile.new([basename, ext])
203
- tempfile.binmode
204
-
205
- attachment.download { |chunk| tempfile.write(chunk) }
182
+ def extract_attachments
183
+ action_text_attachments = action_text_attachment_sources(content)
184
+ return [] unless content_attachments?(action_text_attachments)
206
185
 
207
- tempfile.flush
208
- tempfile.rewind
209
- @_tempfiles << tempfile
210
- tempfile
186
+ @_tempfiles = []
187
+ collect_attachments(action_text_attachments)
211
188
  end
212
189
  end
213
190
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module/delegation'
4
+
5
+ module RubyLLM
6
+ module ActiveRecord
7
+ # A row of the +ruby_llm_models+ table, one per registry entry. Chats
8
+ # point at it through +belongs_to :model+. The +listed+ scope returns the
9
+ # models the provider still serves; +unlisted+ returns the ones kept only
10
+ # because a chat references them.
11
+ class Model < ::ActiveRecord::Base
12
+ self.table_name = 'ruby_llm_models'
13
+
14
+ validates :model_id, presence: true, uniqueness: { scope: :provider }
15
+ validates :provider, :name, presence: true
16
+
17
+ scope :listed, -> { where(unlisted_at: nil) }
18
+ scope :unlisted, -> { where.not(unlisted_at: nil) }
19
+
20
+ UNLISTED_WARNING_LIMIT = 5 # :nodoc:
21
+
22
+ class << self
23
+ def read # :nodoc:
24
+ return [] unless table_exists?
25
+
26
+ all.map(&:to_llm)
27
+ rescue StandardError => e
28
+ RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
29
+ []
30
+ end
31
+
32
+ def write(registry) # :nodoc:
33
+ save_to_database(registry)
34
+ end
35
+
36
+ def description # :nodoc:
37
+ "database:#{table_name}"
38
+ end
39
+
40
+ def refresh # :nodoc:
41
+ RubyLLM.models.refresh
42
+ end
43
+
44
+ def save_to_database(registry = RubyLLM.models) # :nodoc:
45
+ transaction do
46
+ kept = registry.all.map do |model_info|
47
+ model = find_or_initialize_by(model_id: model_info.id, provider: model_info.provider)
48
+ model.update!(attributes_from_llm(model_info))
49
+ model.id
50
+ end
51
+ unlist(kept)
52
+ end
53
+ end
54
+
55
+ def from_llm(model_info) # :nodoc:
56
+ new(attributes_from_llm(model_info))
57
+ end
58
+
59
+ private
60
+
61
+ # A refresh replaces the registry, so models it no longer carries go
62
+ # away. A row an application record points at cannot: deleting it would
63
+ # dangle the reference. That row is stamped unlisted instead, keeping
64
+ # the chats that use it resolvable while the application migrates them.
65
+ def unlist(kept_ids)
66
+ stayed = []
67
+ where.not(id: kept_ids).find_each do |model|
68
+ transaction(requires_new: true) { model.destroy! }
69
+ rescue ::ActiveRecord::InvalidForeignKey
70
+ model.update!(unlisted_at: Time.current) unless model.unlisted_at
71
+ stayed << "#{model.provider}/#{model.model_id}"
72
+ end
73
+ warn_unlisted(stayed)
74
+ end
75
+
76
+ def warn_unlisted(names)
77
+ return if names.empty?
78
+
79
+ subject = names.size == 1 ? '1 model is' : "#{names.size} models are"
80
+ RubyLLM.logger.warn do
81
+ "#{subject} no longer listed by the provider and may no longer work: #{unlisted_listing(names)}. " \
82
+ 'The rows stay because application records still reference them. The provider may have dropped ' \
83
+ 'them, or your configured region may not offer them.'
84
+ end
85
+ end
86
+
87
+ def unlisted_listing(names)
88
+ extra = names.size - UNLISTED_WARNING_LIMIT
89
+ listing = names.first(UNLISTED_WARNING_LIMIT).join(', ')
90
+ extra.positive? ? "#{listing}, and #{extra} more" : listing
91
+ end
92
+
93
+ def attributes_from_llm(model_info)
94
+ {
95
+ model_id: model_info.id,
96
+ name: model_info.name,
97
+ provider: model_info.provider,
98
+ family: model_info.family,
99
+ model_created_at: model_info.created_at,
100
+ context_window: model_info.context_window,
101
+ max_output_tokens: model_info.max_output_tokens,
102
+ knowledge_cutoff: model_info.knowledge_cutoff,
103
+ modalities: model_info.modalities.to_h,
104
+ capabilities: model_info.capabilities,
105
+ pricing: model_info.pricing.to_h,
106
+ metadata: model_info.metadata,
107
+ unlisted_at: nil
108
+ }
109
+ end
110
+ end
111
+
112
+ # Returns this registry record as a RubyLLM::Model.
113
+ def to_llm
114
+ RubyLLM::Model.new(
115
+ id: model_id,
116
+ name: name,
117
+ provider: provider,
118
+ family: family,
119
+ created_at: model_created_at,
120
+ context_window: context_window,
121
+ max_output_tokens: max_output_tokens,
122
+ knowledge_cutoff: knowledge_cutoff,
123
+ modalities: modalities&.deep_symbolize_keys || {},
124
+ capabilities: capabilities,
125
+ pricing: pricing&.deep_symbolize_keys || {},
126
+ metadata: metadata&.deep_symbolize_keys || {},
127
+ unlisted_at: unlisted_at
128
+ )
129
+ end
130
+
131
+ # Registry readers answered from the RubyLLM::Model value of the row.
132
+ delegate :supports?, :price, :type, :provider_class, :label, :cost_for, :unlisted?, to: :to_llm
133
+ end
134
+ end
135
+ end
@@ -5,8 +5,7 @@ require 'json'
5
5
 
6
6
  module RubyLLM
7
7
  module ActiveRecord
8
- # Shared helpers for parsing serialized payloads on ActiveRecord-backed models.
9
- module PayloadHelpers
8
+ module PayloadHelpers # :nodoc:
10
9
  private
11
10
 
12
11
  def payload_error_message(value)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm/active_record/payload_helpers'
4
+
5
+ module RubyLLM
6
+ module ActiveRecord
7
+ # RubyLLM's private persistence for provider tool calls.
8
+ class ToolCall < ::ActiveRecord::Base # :nodoc:
9
+ self.table_name = 'ruby_llm_tool_calls'
10
+
11
+ include PayloadHelpers
12
+
13
+ belongs_to :message, polymorphic: true
14
+ belongs_to :result, polymorphic: true, optional: true
15
+
16
+ validates :tool_call_id, :name, presence: true
17
+
18
+ def to_llm
19
+ RubyLLM::ToolCall.new(
20
+ id: tool_call_id,
21
+ name: name,
22
+ arguments: arguments || {},
23
+ thought_signature: thought_signature,
24
+ remote: self[:remote]
25
+ )
26
+ end
27
+
28
+ def tool_error_message
29
+ payload_error_message(arguments)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # RubyLLM's private normalized persistence for one provider attempt.
6
+ class Usage < ::ActiveRecord::Base # :nodoc:
7
+ self.table_name = 'ruby_llm_usages'
8
+
9
+ belongs_to :chat, polymorphic: true
10
+ belongs_to :message, polymorphic: true, optional: true
11
+
12
+ validates :operation, inclusion: { in: ::RubyLLM::Accounting::Usage::Entry::OPERATIONS.map(&:to_s) }
13
+ validates :status, inclusion: { in: ::RubyLLM::Accounting::Usage::Entry::STATUSES.map(&:to_s) }
14
+ validates :provider, :model, presence: true
15
+
16
+ scope :chronological, -> { order(created_at: :asc, id: :asc) }
17
+
18
+ def tokens
19
+ RubyLLM::Tokens.new(
20
+ input: input_tokens,
21
+ output: output_tokens,
22
+ cache_read: cache_read_tokens,
23
+ cache_write: cache_write_tokens,
24
+ thinking: thinking_tokens
25
+ )
26
+ end
27
+
28
+ def cost
29
+ recorded = {
30
+ input: input_cost,
31
+ output: output_cost,
32
+ cache_read: cache_read_cost,
33
+ cache_write: cache_write_cost,
34
+ thinking: thinking_cost,
35
+ total: total_cost
36
+ }.compact
37
+ RubyLLM::Cost.from_h(recorded, tokens: tokens)
38
+ end
39
+
40
+ def usage_available?
41
+ tokens.to_h.any?
42
+ end
43
+
44
+ def cost_available?
45
+ !total_cost.nil?
46
+ end
47
+
48
+ def to_entry
49
+ ::RubyLLM::Accounting::Usage::Entry.new(
50
+ operation: operation,
51
+ provider: provider,
52
+ model: model,
53
+ status: status,
54
+ tokens: tokens,
55
+ cost: cost,
56
+ message: message
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end