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
@@ -0,0 +1,432 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A Batch is a provider-side batch of requests: chats awaiting a
5
+ # response (or texts awaiting embeddings) go in together, answers come
6
+ # back at batch prices, typically within hours. Persist the id, pick the
7
+ # batch back up from any process, and collect the results once
8
+ # processing ends.
9
+ #
10
+ # chats = documents.map do |doc|
11
+ # RubyLLM.chat(model: "claude-haiku-4-5").ask_later(doc.text)
12
+ # end
13
+ # batch = RubyLLM.batch(chats)
14
+ # batch.id # => "msgbatch_01EhcDuvb5XfWqcdJArbsfNX"
15
+ # batch.refresh.complete? # => false, check back later
16
+ # batch.messages # the responses, in submission order
17
+ #
18
+ class Batch
19
+ include Support::Inspectable
20
+
21
+ AWAITING_ROLES = %i[user tool].freeze # :nodoc:
22
+
23
+ # The provider's batch id. Persist it to load the batch again later
24
+ # from any process with ::find.
25
+ attr_reader :id
26
+
27
+ # The provider-neutral lifecycle status: +:pending+, +:succeeded+,
28
+ # +:failed+, or +:cancelled+. Refreshed by #refresh.
29
+ attr_reader :status
30
+
31
+ # The provider-reported status string, such as "in_progress".
32
+ # Refreshed by #refresh.
33
+ attr_reader :raw_status
34
+
35
+ # The provider-reported request tallies by state, or +nil+ when the
36
+ # provider does not report them.
37
+ attr_reader :request_counts
38
+
39
+ # The submitted Chat objects in order, or +nil+ when the batch was
40
+ # loaded by id via ::find or holds embedding requests.
41
+ attr_reader :chats
42
+
43
+ # The submitted EmbeddingRequest objects in order, or +nil+ when the
44
+ # batch was loaded by id via ::find or holds chats.
45
+ attr_reader :requests
46
+
47
+ # The normalized outcome of each collected request, in submission order.
48
+ # Values are +:succeeded+, +:failed+, or +:cancelled+.
49
+ attr_reader :statuses
50
+
51
+ class << self
52
+ # Submits chats or embedding requests to their shared provider as a
53
+ # batch and returns a new Batch. Accepts a single Chat or an array.
54
+ # Every chat must be awaiting the model (see Chat#ask_later), and all
55
+ # requests must use the same provider.
56
+ #
57
+ # chats = tickets.map do |ticket|
58
+ # RubyLLM.chat(model: "claude-haiku-4-5").ask_later(ticket.body)
59
+ # end
60
+ # batch = RubyLLM::Batch.submit(chats)
61
+ # batch.status # => :pending
62
+ # batch.raw_status # => "in_progress"
63
+ #
64
+ # Raises ArgumentError if the batch is empty, mixes providers, mixes
65
+ # chats with embedding requests, or includes a chat that is not
66
+ # awaiting the model.
67
+ def submit(chats)
68
+ records = wrap_records(chats)
69
+ return submit_embeddings(records) if records.any?(EmbeddingRequest)
70
+
71
+ submit_chats(records)
72
+ end
73
+
74
+ # Returns a Batch reflecting the provider's current state for +id+.
75
+ # Use it to pick a batch back up from any process.
76
+ #
77
+ # batch = RubyLLM::Batch.find("msgbatch_01EhcDuvb5XfWqcdJArbsfNX",
78
+ # provider: :anthropic)
79
+ # batch.complete? # => true
80
+ #
81
+ # Pass +context:+ to use a Context in place of the global
82
+ # configuration. Raises ArgumentError if +provider+ is not given.
83
+ def find(id, provider: nil, context: nil)
84
+ config = context&.config || RubyLLM.config
85
+ persisted = config.batch_store&.fetch(id, provider:, context:)
86
+ return persisted if persisted
87
+
88
+ unless provider
89
+ raise ArgumentError, 'Provider must be specified to find a batch that is not persisted by RubyLLM'
90
+ end
91
+
92
+ provider = Provider.resolve!(provider).new(config)
93
+ raise Error, "#{provider.slug} doesn't support batch requests" unless provider.batches?
94
+
95
+ new(provider:, store: config.batch_store, **provider.find_batch(id))
96
+ end
97
+
98
+ private
99
+
100
+ def submit_chats(records)
101
+ chats = normalize_chats(records)
102
+
103
+ provider = shared_provider(chats)
104
+ payload = { provider: provider.slug, provider_class: provider.class.display_name, requests: chats.size }
105
+ RubyLLM.instrument('batch.ruby_llm', payload, config: provider.config) do |event|
106
+ requests = chats.each_with_index.map do |chat, index|
107
+ { custom_id: index.to_s, model: chat.model.id, payload: chat.render }
108
+ end
109
+ store = provider.config.batch_store
110
+ batch = new(provider:, chats:, store:, **provider.create_batch(requests))
111
+ store&.persist(batch, records)
112
+ event[:batch_id] = batch.id
113
+ batch
114
+ end
115
+ end
116
+
117
+ def submit_embeddings(requests)
118
+ unless requests.all?(EmbeddingRequest)
119
+ raise ArgumentError, 'A batch takes chats or embedding requests, not both'
120
+ end
121
+
122
+ provider = shared_provider(requests)
123
+ payload = { provider: provider.slug, provider_class: provider.class.display_name, requests: requests.size }
124
+ RubyLLM.instrument('batch.ruby_llm', payload, config: provider.config) do |event|
125
+ lines = requests.each_with_index.map do |request, index|
126
+ { custom_id: index.to_s, model: request.model.id, payload: request.render, text: request.text }
127
+ end
128
+ batch = new(provider:, requests:, store: provider.config.batch_store, **provider.create_batch(lines))
129
+ event[:batch_id] = batch.id
130
+ batch
131
+ end
132
+ end
133
+
134
+ def wrap_records(records)
135
+ return [records] if records.respond_to?(:to_llm)
136
+
137
+ case records
138
+ when Chat, EmbeddingRequest then [records]
139
+ else Array(records)
140
+ end
141
+ end
142
+
143
+ def normalize_chats(records)
144
+ normalized = records.map { |chat| chat.respond_to?(:to_llm) ? chat.to_llm : chat }
145
+ raise ArgumentError, 'Cannot submit an empty batch' if normalized.empty?
146
+
147
+ unless normalized.all? { |chat| awaiting_model?(chat) }
148
+ raise ArgumentError,
149
+ 'Every chat in a batch must be awaiting the model; stage one with ask_later, or run_tools first'
150
+ end
151
+
152
+ normalized
153
+ end
154
+
155
+ def awaiting_model?(chat)
156
+ !chat.complete? && AWAITING_ROLES.include?(chat.messages.last&.role)
157
+ end
158
+
159
+ def shared_provider(chats)
160
+ slugs = chats.map { |chat| chat.provider.slug }.uniq
161
+ raise ArgumentError, "A batch takes one provider per submission, got: #{slugs.join(', ')}" if slugs.size > 1
162
+
163
+ provider = chats.first.provider
164
+ raise Error, "#{provider.slug} doesn't support batch requests" unless provider.batches?
165
+
166
+ provider
167
+ end
168
+ end
169
+
170
+ def initialize(provider:, chats: nil, requests: nil, batch_protocol: nil, store: nil, **attributes) # :nodoc:
171
+ @provider = provider
172
+ @chats = chats
173
+ @requests = requests
174
+ @batch_protocol = protocol_name(batch_protocol)
175
+ @store = store
176
+ @delivered = {}
177
+ @statuses = []
178
+ apply(attributes)
179
+ end
180
+
181
+ # The slug of the provider running the batch, as a String.
182
+ def provider
183
+ @provider.slug
184
+ end
185
+
186
+ attr_reader :batch_protocol, :reported_cost # :nodoc:
187
+
188
+ # Returns whether the batch has finished processing, as of the last
189
+ # state fetched from the provider. Never contacts the provider; poll
190
+ # with #refresh.
191
+ #
192
+ # sleep 60 until batch.refresh.complete?
193
+ #
194
+ def complete?
195
+ @completed
196
+ end
197
+
198
+ # Returns whether the provider completed the batch successfully.
199
+ def succeeded?
200
+ status == :succeeded
201
+ end
202
+
203
+ # Returns whether the provider failed or expired the batch.
204
+ def failed?
205
+ status == :failed
206
+ end
207
+
208
+ # Returns whether the provider cancelled the batch.
209
+ def cancelled?
210
+ status == :cancelled
211
+ end
212
+
213
+ # Re-fetches the batch from the provider, updating #status, #raw_status,
214
+ # #request_counts, and #complete?. Returns +self+.
215
+ def refresh
216
+ apply(@provider.find_batch(id))
217
+ persist_state
218
+ self
219
+ end
220
+
221
+ # Asks the provider to cancel the batch and applies the new state.
222
+ # Requests already processed still return results. Returns +self+.
223
+ def cancel
224
+ apply(@provider.cancel_batch(id))
225
+ persist_state
226
+ self
227
+ end
228
+
229
+ # Returns the answers in submission order, +nil+ where a request
230
+ # failed. In a chat batch the answers are Messages, each also appended
231
+ # to its chat; in an embeddings batch they are Embeddings, each also
232
+ # hydrated into its request's EmbeddingRequest#result. Fetches results
233
+ # from the provider; cached once #complete? is true, so collecting
234
+ # early keeps reading fresh.
235
+ #
236
+ # batch.messages.each do |message|
237
+ # puts message.content
238
+ # end
239
+ #
240
+ def messages
241
+ return @messages if @messages
242
+
243
+ collected = collect_results
244
+ @messages = collected if @completed
245
+ collected
246
+ end
247
+
248
+ alias results messages
249
+
250
+ # Returns token usage aggregated across the batch's collected responses.
251
+ def tokens
252
+ Tokens.aggregate(messages.compact.map(&:tokens))
253
+ end
254
+
255
+ # Returns a Cost for the batch. Uses the provider's reported total when
256
+ # available, otherwise aggregates collected response costs at batch rates.
257
+ # The total is +nil+ until the batch ends or when pricing is unknown.
258
+ def cost
259
+ return Cost.aggregate([reported_cost], complete: complete?) if reported_cost
260
+ return Cost.aggregate([], complete: false) unless complete?
261
+
262
+ Cost.aggregate(messages.compact.map(&:cost))
263
+ end
264
+
265
+ private
266
+
267
+ def persist_state
268
+ @store&.sync(self)
269
+ end
270
+
271
+ def apply(attributes)
272
+ @batch_protocol = protocol_name(attributes[:batch_protocol]) if attributes[:batch_protocol]
273
+ @id = attributes.fetch(:id)
274
+ @raw_status = attributes.fetch(:raw_status)
275
+ @completed = attributes.fetch(:completed)
276
+ @request_counts = attributes[:request_counts]
277
+ @request_count = attributes[:request_count]
278
+ @reported_cost = attributes[:reported_cost] if attributes[:reported_cost]
279
+ @status = @provider.batch_status(@raw_status, completed: @completed, batch_protocol: @batch_protocol)
280
+ end
281
+
282
+ def protocol_name(protocol)
283
+ return if protocol.nil?
284
+
285
+ protocol.is_a?(Module) ? @provider.batch_protocol_name(protocol) : protocol.to_s
286
+ end
287
+
288
+ def collect_results
289
+ results = @provider.batch_results(id, batch_protocol: @batch_protocol)
290
+ slots = Array.new(result_slot_count(results))
291
+
292
+ results.each do |index, result, failure_status|
293
+ slots[index] = result
294
+ deliver(index, result, failure_status)
295
+ end
296
+
297
+ fill_missing_statuses(slots.size) if complete?
298
+
299
+ slots
300
+ end
301
+
302
+ def result_slot_count(results)
303
+ chats&.size || requests&.size || @request_count || ((results.map(&:first).max || -1) + 1)
304
+ end
305
+
306
+ def fill_missing_statuses(size)
307
+ missing_status = cancelled? ? :cancelled : :failed
308
+ size.times { |index| statuses[index] ||= missing_status }
309
+ end
310
+
311
+ # Collecting early keeps reading fresh, so a result already delivered
312
+ # comes back on every later poll: hand each one over once.
313
+ def deliver(index, result, failure_status)
314
+ statuses[index] = result ? :succeeded : failure_status
315
+ return unless result
316
+
317
+ if result.is_a?(Embedding)
318
+ deliver_embedding(index, result)
319
+ else
320
+ deliver_message(index, result)
321
+ end
322
+ end
323
+
324
+ def deliver_embedding(index, embedding)
325
+ request = requests&.[](index)
326
+ delivered = @delivered[index]
327
+ attach_batch_usage(
328
+ embedding,
329
+ operation: :embedding,
330
+ model: request&.model,
331
+ category: :embeddings,
332
+ instrument: !request.nil? && !delivered
333
+ )
334
+ return if delivered
335
+
336
+ @delivered[index] = true
337
+ request&.result = embedding
338
+ end
339
+
340
+ def deliver_message(index, message)
341
+ chat = chats&.[](index)
342
+ delivered = @delivered[index] || (chat && already_in_chat?(chat, message))
343
+ attach_batch_usage(
344
+ message,
345
+ operation: :chat,
346
+ model: chat&.model,
347
+ category: :text_tokens,
348
+ instrument: !chat.nil? && !delivered
349
+ )
350
+ return if delivered
351
+
352
+ @delivered[index] = true
353
+ chat&.add_completion(message, record_usage: true)
354
+ end
355
+
356
+ def attach_batch_usage(result, operation:, model:, category:, instrument:)
357
+ return unless result.ruby_llm_usage_entries.empty?
358
+
359
+ model ||= RubyLLM.models.find(result.model, provider: @provider.slug, config: @provider.config)
360
+ entry = Accounting::Usage::Entry.new(
361
+ operation:,
362
+ provider: @provider.slug,
363
+ model: result.model || model.id,
364
+ status: :succeeded,
365
+ tokens: result.tokens,
366
+ cost: @provider.batch_cost(result.tokens, model:, category:),
367
+ message: result.is_a?(Message) ? result : nil
368
+ )
369
+ result.ruby_llm_usage_entries = [entry]
370
+ Accounting::Usage.instrument(entry, config: @provider.config) if instrument
371
+ end
372
+
373
+ # A plain answer is the chat's last message once it arrives. A tool-call
374
+ # answer is not: running its tools adds messages after it, so we match on its
375
+ # tool-call ids instead.
376
+ def already_in_chat?(chat, message)
377
+ if message.tool_call?
378
+ chat.messages.any? { |m| m.tool_call? && m.tool_calls.keys.intersect?(message.tool_calls.keys) }
379
+ else
380
+ !AWAITING_ROLES.include?(chat.messages.last&.role)
381
+ end
382
+ end
383
+
384
+ module Helpers # :nodoc:
385
+ private
386
+
387
+ def batch_result_index(id)
388
+ Integer(id)
389
+ end
390
+
391
+ def batch_failure(custom_id, detail, status: 'failed')
392
+ RubyLLM.logger.warn ["Batch request #{custom_id} #{status}", detail].compact.join(': ')
393
+ status.to_s.match?(/cancel/i) ? :cancelled : :failed
394
+ end
395
+
396
+ def batch_error_message(line)
397
+ response = line['response']
398
+ body = response['body'] if response.is_a?(Hash)
399
+ body_error = body['error'] if body.is_a?(Hash)
400
+ response_error = response['error'] if response.is_a?(Hash)
401
+
402
+ batch_error_value(line['error']) ||
403
+ line['error_message'] ||
404
+ batch_error_value(body_error) ||
405
+ batch_error_value(response_error)
406
+ end
407
+
408
+ def batch_error_value(error)
409
+ case error
410
+ when Hash then error['message']
411
+ when String then error
412
+ end
413
+ end
414
+
415
+ def single_batch_model!(requests, provider_name)
416
+ models = requests.map { |request| request.fetch(:model) }.uniq
417
+ return models.first if models.one?
418
+
419
+ raise Error, "#{provider_name} batch requests must use one model per submission"
420
+ end
421
+
422
+ def batch_payload(request, except: [])
423
+ excluded = (Array(except) + [:stream]).map(&:to_s)
424
+ request.fetch(:payload).reject { |key, _| excluded.include?(key.to_s) }
425
+ end
426
+ end
427
+
428
+ def inspect_attributes # :nodoc:
429
+ { id: id, status: status, raw_status: raw_status, chats: chats&.count, requests: requests&.count }
430
+ end
431
+ end
432
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A CachedContent is a provider-side prompt cache resource. Create one
5
+ # with ::create from a long, stable prompt prefix, then attach it to a
6
+ # chat so later requests read the cached tokens instead of resending
7
+ # them. This lifecycle requires a provider with managed cache resources;
8
+ # automatic prompt caching is configured with Chat#with_caching.
9
+ #
10
+ # cache = RubyLLM.cache(big_document, model: 'gemini-3.7-flash', ttl: 3600)
11
+ # chat = RubyLLM.chat(model: 'gemini-3.7-flash').with_caching(id: cache)
12
+ # chat.ask "What does the document conclude?"
13
+ # cache.delete
14
+ #
15
+ # Cache names are provider-owned. Persist #provider alongside #name and
16
+ # pass it back when finding the cache later.
17
+ class CachedContent
18
+ include Support::Inspectable
19
+
20
+ def inspect_attributes # :nodoc:
21
+ { name: name, model: model, provider: provider, expires_at: expires_at }
22
+ end
23
+
24
+ # The provider-assigned resource name, such as
25
+ # <tt>"cachedContents/abc123"</tt>.
26
+ attr_reader :name
27
+
28
+ # The model the cache was created for.
29
+ attr_reader :model
30
+
31
+ # The slug of the provider that stores the cache.
32
+ attr_reader :provider
33
+
34
+ # The Time the provider stored the cache.
35
+ attr_reader :created_at
36
+
37
+ # The Time the provider will delete the cache.
38
+ attr_reader :expires_at
39
+
40
+ # The number of tokens stored in the cache.
41
+ attr_reader :tokens
42
+
43
+ # The raw provider response data for the cache, as a Hash.
44
+ attr_reader :metadata
45
+
46
+ def initialize(name:, **attributes) # :nodoc:
47
+ @name = name
48
+ @model = attributes[:model]
49
+ @provider_instance = attributes[:provider_instance]
50
+ @provider = attributes[:provider] || @provider_instance&.slug
51
+ @created_at = attributes[:created_at]
52
+ @expires_at = attributes[:expires_at]
53
+ @tokens = attributes[:tokens]
54
+ @metadata = attributes[:metadata] || {}
55
+ end
56
+
57
+ # Deletes the cache resource from the provider. Returns +self+.
58
+ def delete
59
+ @provider_instance.delete_cache(name)
60
+ self
61
+ end
62
+
63
+ # Extends the cache's lifetime to +ttl:+ seconds from now, given as an
64
+ # Integer or a provider duration string such as <tt>"600s"</tt>.
65
+ # Updates #expires_at and returns +self+.
66
+ #
67
+ # cache.renew(ttl: 3600)
68
+ #
69
+ def renew(ttl:)
70
+ refreshed = @provider_instance.extend_cache(name, ttl: ttl)
71
+ @expires_at = refreshed.expires_at
72
+ @metadata = refreshed.metadata
73
+ self
74
+ end
75
+
76
+ # Creates a provider-side prompt cache from +content+ and returns a
77
+ # CachedContent. Also available as RubyLLM.cache.
78
+ #
79
+ # RubyLLM::CachedContent.create(big_document, model: 'gemini-3.7-flash')
80
+ #
81
+ # +content+ is the text to cache; pass file attachments with +with:+
82
+ # the way Chat#ask accepts them. +instructions:+ caches a system
83
+ # prompt alongside the content, and +ttl:+ sets the cache lifetime in
84
+ # seconds (Integer) or as a provider duration string such as
85
+ # <tt>"300s"</tt>. When +provider:+ is omitted, the model's default
86
+ # provider is used. The content must exceed the model's minimum
87
+ # cacheable token count.
88
+ def self.create(content, model:, ttl: nil, instructions: nil, provider: nil, context: nil, with: nil)
89
+ config = context&.config || RubyLLM.config
90
+ model_instance, provider_instance = Models.resolve(model, provider: provider, config: config)
91
+
92
+ provider_instance.cache_content(content, model: model_instance, ttl:, instructions:, with:)
93
+ end
94
+
95
+ # Fetches an existing cache resource by +name+ and returns a
96
+ # CachedContent. When +provider:+ is omitted, the provider of the
97
+ # configured default model is used.
98
+ #
99
+ # cache = RubyLLM::CachedContent.find("cachedContents/abc123", provider: :gemini)
100
+ #
101
+ def self.find(name, provider: nil, context: nil)
102
+ config = context&.config || RubyLLM.config
103
+
104
+ provider_instance = if provider
105
+ Provider.resolve!(provider).new(config)
106
+ else
107
+ Models.resolve(config.default_model, config:).last
108
+ end
109
+ provider_instance.find_cache(name)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Chat
5
+ module ToolConcurrency # :nodoc: all
6
+ MODES = %i[threads fibers].freeze
7
+ Result = Struct.new(:index, :tool_call, :value, :error, keyword_init: true)
8
+
9
+ module_function
10
+
11
+ def run(mode, tool_calls, on_result: nil, &)
12
+ case mode
13
+ when :threads
14
+ run_with_threads(tool_calls, on_result:, &)
15
+ when :fibers
16
+ run_with_fibers(tool_calls, on_result:, &)
17
+ end
18
+ end
19
+
20
+ def run_with_threads(tool_calls, on_result:, &execute)
21
+ executor = rails_executor
22
+ workflow_context = Support::Instrumentation.current_workflow
23
+ queue = Queue.new
24
+ threads = tool_calls.each_value.with_index.map do |tool_call, index|
25
+ thread = Thread.new do
26
+ Support::Instrumentation.with_workflow(workflow_context) do
27
+ queue << capture_result(index, tool_call, executor, execute)
28
+ end
29
+ end
30
+ thread.report_on_exception = false
31
+ thread
32
+ end
33
+
34
+ collect_results(queue, threads.size, on_result:)
35
+ ensure
36
+ threads&.each(&:join)
37
+ end
38
+
39
+ def run_with_fibers(tool_calls, on_result:, &execute)
40
+ begin
41
+ require 'async'
42
+ require 'async/queue'
43
+ rescue LoadError
44
+ raise LoadError, "The 'async' gem is required for concurrent tool execution with fibers. " \
45
+ "Add `gem 'async', '>= 2.0'` to your Gemfile or use `concurrency: :threads`."
46
+ end
47
+ if Gem.loaded_specs.fetch('async').version < Gem::Version.new('2.0')
48
+ raise LoadError, "The 'async' gem version 2.0 or newer is required for concurrent tool execution with fibers."
49
+ end
50
+
51
+ executor = rails_executor
52
+ workflow_context = Support::Instrumentation.current_workflow
53
+ Async do |task|
54
+ queue = Async::Queue.new
55
+ tasks = tool_calls.each_value.with_index.map do |tool_call, index|
56
+ task.async do
57
+ Support::Instrumentation.with_workflow(workflow_context) do
58
+ queue << capture_result(index, tool_call, executor, execute)
59
+ end
60
+ end
61
+ end
62
+
63
+ collect_results(queue, tasks.size, on_result:)
64
+ ensure
65
+ tasks&.each(&:wait)
66
+ end.wait
67
+ end
68
+
69
+ def collect_results(queue, count, on_result:)
70
+ results = Array.new(count)
71
+ errors = []
72
+
73
+ count.times do
74
+ result = queue.pop
75
+ if result.error
76
+ errors << result.error
77
+ else
78
+ results[result.index] = [result.tool_call, result.value]
79
+ on_result&.call(result.tool_call, result.value)
80
+ end
81
+ end
82
+
83
+ raise errors.first if errors.any?
84
+
85
+ results
86
+ end
87
+
88
+ def capture_result(index, tool_call, rails_executor, execute)
89
+ tool_call, value = run_tool_call(tool_call, rails_executor, execute)
90
+ Result.new(index:, tool_call:, value:)
91
+ rescue Exception => e # rubocop:disable Lint/RescueException
92
+ Result.new(index:, tool_call:, error: e)
93
+ end
94
+
95
+ def run_tool_call(tool_call, rails_executor, execute)
96
+ if rails_executor
97
+ rails_executor.wrap { [tool_call, execute.call(tool_call)] }
98
+ else
99
+ [tool_call, execute.call(tool_call)]
100
+ end
101
+ end
102
+
103
+ def rails_executor
104
+ defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.executor
105
+ end
106
+
107
+ private_class_method :run_with_threads, :run_with_fibers, :collect_results, :capture_result, :run_tool_call,
108
+ :rails_executor
109
+ end
110
+ end
111
+ end