ruby_llm 1.15.0 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -6
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1208 -150
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +274 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +155 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +540 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/protocols/chat_completions/media.rb +121 -0
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +17 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -1,63 +1,127 @@
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
 
41
+ def ruby_llm_usage_entries # :nodoc:
42
+ ruby_llm_usages.map(&:to_entry)
43
+ end
44
+
45
+ # Returns the model ID from the last successful attempt, or +nil+.
46
+ def model
47
+ to_llm.model
48
+ end
49
+
50
+ # Returns the model from the registry, or +nil+ if it is unknown.
51
+ def model_info
52
+ to_llm.model_info
53
+ end
54
+
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
59
+ end
60
+
61
+ # Returns +true+ if the message is a prompt cache boundary.
62
+ def cache_until_here?
63
+ optional_column(:cache_until_here) || false
64
+ end
65
+
66
+ # Returns the reasoning the model returned as a RubyLLM::Thinking, or +nil+.
29
67
  def thinking
30
68
  RubyLLM::Thinking.build(
31
- text: thinking_text_value,
32
- signature: thinking_signature_value
69
+ text: optional_column(:thinking_text),
70
+ signature: optional_column(:thinking_signature)
33
71
  )
34
72
  end
35
73
 
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) }
77
+ end
78
+
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) }
82
+ end
83
+
84
+ # Returns the token usage across every attempt that produced the message.
36
85
  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
- )
86
+ RubyLLM::Tokens.aggregate(ruby_llm_usages.map(&:tokens))
44
87
  end
45
88
 
89
+ # Returns the cost across every attempt that produced the message, as a RubyLLM::Cost.
46
90
  def cost
47
- RubyLLM::Cost.new(tokens:, model: model_association)
91
+ records = ruby_llm_usages.to_a
92
+ RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?))
48
93
  end
49
94
 
50
- def cache_read_tokens
51
- cached_value
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] }
52
98
  end
53
99
 
54
- def cache_write_tokens
55
- cache_creation_value
100
+ # Returns the tool call this message answers, or +nil+.
101
+ def parent_tool_call
102
+ ruby_llm_parent_tool_call&.to_llm
56
103
  end
57
104
 
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
+ end
109
+
110
+ # Returns +true+ if the message carries tool calls.
111
+ def tool_call?
112
+ ruby_llm_tool_calls.any?
113
+ end
114
+
115
+ # Returns +true+ if the message answers a tool call.
116
+ def tool_result?
117
+ ruby_llm_parent_tool_call.present?
118
+ end
119
+
120
+ # Returns the partial path for the message by role, such as +messages/assistant+
121
+ # or +messages/tool_calls+.
58
122
  def to_partial_path
59
123
  partial_prefix = self.class.name.underscore.pluralize
60
- role_partial = if to_llm.tool_call?
124
+ role_partial = if tool_call?
61
125
  'tool_calls'
62
126
  elsif role.to_s == 'tool'
63
127
  'tool'
@@ -67,80 +131,60 @@ module RubyLLM
67
131
  "#{partial_prefix}/#{role_partial}"
68
132
  end
69
133
 
134
+ # Returns the error text of a failed tool result, or +nil+.
70
135
  def tool_error_message
71
- payload_error_message(content)
136
+ payload_error_message(extract_content)
72
137
  end
73
138
 
74
- private
75
-
76
- def thinking_text_value
77
- has_attribute?(:thinking_text) ? self[:thinking_text] : nil
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
78
142
  end
79
143
 
80
- def thinking_signature_value
81
- has_attribute?(:thinking_signature) ? self[:thinking_signature] : nil
144
+ # Returns +true+ if the model finished normally without requesting tools.
145
+ def stopped?
146
+ to_llm.stopped?
82
147
  end
83
148
 
84
- def cached_value
85
- has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
149
+ # Returns +true+ if a token limit stopped the response.
150
+ def max_tokens?
151
+ to_llm.max_tokens?
86
152
  end
87
153
 
88
- def cache_creation_value
89
- has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
154
+ # Returns +true+ if the model stopped to request tool calls.
155
+ def tool_call_stop?
156
+ to_llm.tool_call_stop?
90
157
  end
91
158
 
92
- def thinking_tokens_value
93
- has_attribute?(:thinking_tokens) ? self[:thinking_tokens] : nil
159
+ # Returns +true+ if a provider safety filter stopped the response.
160
+ def content_filtered?
161
+ to_llm.content_filtered?
94
162
  end
95
163
 
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
108
- 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?
109
168
 
110
- def extract_tool_call_id
111
- parent_tool_call&.tool_call_id
169
+ JSON.parse(text)
112
170
  end
113
171
 
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 = []
172
+ private
124
173
 
125
- attachments.each do |attachment|
126
- tempfile = download_attachment(attachment)
127
- content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
128
- end
129
- end
174
+ def optional_column(name)
175
+ self[name] if has_attribute?(name)
130
176
  end
131
177
 
132
- def download_attachment(attachment)
133
- ext = File.extname(attachment.filename.to_s)
134
- basename = File.basename(attachment.filename.to_s, ext)
135
- tempfile = Tempfile.new([basename, ext])
136
- tempfile.binmode
178
+ def extract_content
179
+ plain_text_content(content)
180
+ end
137
181
 
138
- 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)
139
185
 
140
- tempfile.flush
141
- tempfile.rewind
142
- @_tempfiles << tempfile
143
- tempfile
186
+ @_tempfiles = []
187
+ collect_attachments(action_text_attachments)
144
188
  end
145
189
  end
146
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