ruby_llm 2.1.0 → 2.2.0

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 (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # The Tool an Agent class becomes when it goes wherever tools go, so a
5
+ # model can hand work to it: a subagent. Agent::to_tool returns one, and
6
+ # Chat#with_tools and Agent::tools convert an Agent class themselves.
7
+ #
8
+ # class ResearchAgent < RubyLLM::Agent
9
+ # description "Researches a question across the web and reports back"
10
+ # tools WebSearch, ReadPage
11
+ # end
12
+ #
13
+ # chat.with_tools(ResearchAgent).ask "What changed in Ruby 4.0?"
14
+ #
15
+ # The tool is named after the agent (+research_agent+, or
16
+ # +billing_research_agent+ for <tt>Billing::ResearchAgent</tt>) and takes
17
+ # a +task+, everything the agent needs written to stand alone, plus the
18
+ # agent's ::inputs that ::to_tool was not given. Each call runs a fresh
19
+ # instance of the agent on the task, under the owner of the chat that
20
+ # called it, and returns the content of its last answer.
21
+ #
22
+ # The agent lives only as long as its call, so it has to finish within
23
+ # it. When it stops to wait instead, for an approval, a question, or a
24
+ # task, the call raises ConfigurationError. To let a subagent wait for a
25
+ # person, give it a chat of its own, as the Agents guide shows.
26
+ class AgentTool < Tool
27
+ GUIDE = 'https://rubyllm.com/agents/#subagents-that-wait-for-a-person'
28
+ private_constant :GUIDE
29
+
30
+ class << self
31
+ # The Agent class this tool runs.
32
+ attr_reader :agent_class
33
+
34
+ # The agent's ::inputs that the model supplies, as parameters.
35
+ attr_reader :input_parameters # :nodoc:
36
+
37
+ # Returns a Tool class for +agent_class+, with the inputs in +given+
38
+ # left out of its parameters.
39
+ def for(agent_class, given) # :nodoc:
40
+ raise ArgumentError, 'An agent needs a class name to become a tool' unless agent_class.name
41
+ if agent_class.inputs.include?(:task)
42
+ raise ArgumentError, "#{agent_class.name} has an input named task, which its tool takes for the prompt"
43
+ end
44
+
45
+ missing = agent_class.inputs - given.keys.map(&:to_sym)
46
+ Class.new(self) do
47
+ @agent_class = agent_class
48
+ @input_parameters = missing
49
+ description(agent_class.description || "Hands a task to the #{agent_class.name} agent, which reports back.")
50
+ parameter :task, description: 'Everything the agent needs to do the work, written to stand alone'
51
+ missing.each { |input| parameter input, description: "The agent's #{input}" }
52
+ end
53
+ end
54
+
55
+ # Returns the agent's name in snake case, such as +research_agent+.
56
+ def tool_name
57
+ Support::Utils.underscore(agent_class.name.gsub('::', '_'))
58
+ end
59
+ end
60
+
61
+ def initialize(inputs = {}) # :nodoc:
62
+ super()
63
+ @inputs = inputs
64
+ end
65
+
66
+ # Runs a fresh instance of the agent on +task+, with the inputs given to
67
+ # Agent::to_tool and those the model chose, and returns the content of
68
+ # its last answer. Raises ConfigurationError, naming what the agent
69
+ # waits for, when it stops to wait instead of answering.
70
+ def execute(task:, **inputs)
71
+ agent = self.class.agent_class.new(**@inputs, **inputs.slice(*self.class.input_parameters))
72
+ owner = Accounting::Usage.owner
73
+ agent.with_owner(owner) unless owner.nil?
74
+ message = agent.ask(task)
75
+ raise ConfigurationError, stopped(agent) if agent.waiting?
76
+
77
+ message&.content
78
+ end
79
+
80
+ private
81
+
82
+ def stopped(agent)
83
+ people = agent.pending_questions.map do |question|
84
+ "a person to #{question.approval? ? 'approve' : 'answer'} #{question.tool_call.name}"
85
+ end
86
+ tasks = agent.pending_tasks.map { |task| "the task of #{task.tool_call.name}" }
87
+ "#{self.class.agent_class.name} stopped to wait for #{(people + tasks).join(' and ')}, but an agent used " \
88
+ "as a tool has to finish within its call. To let it wait, give it a chat of its own: #{GUIDE}"
89
+ end
90
+ end
91
+ end
@@ -1,4 +1,7 @@
1
1
  {
2
+ "antigravity-preview": {
3
+ "gemini": "antigravity-preview-latest"
4
+ },
2
5
  "claude-fable-5": {
3
6
  "anthropic": "claude-fable-5",
4
7
  "openrouter": "anthropic/claude-fable-5",
@@ -13,6 +16,13 @@
13
16
  "vertexai": "claude-fable-5-1",
14
17
  "azure": "claude-fable-5-1"
15
18
  },
19
+ "claude-fable-5-2": {
20
+ "anthropic": "claude-fable-5-2",
21
+ "openrouter": "anthropic/claude-fable-5.2",
22
+ "bedrock": "anthropic.claude-fable-5-2",
23
+ "vertexai": "claude-fable-5-2",
24
+ "azure": "claude-fable-5-2"
25
+ },
16
26
  "claude-haiku-4-5": {
17
27
  "anthropic": "claude-haiku-4-5-20251001",
18
28
  "openrouter": "anthropic/claude-haiku-4.5",
@@ -20,6 +30,13 @@
20
30
  "vertexai": "claude-haiku-4-5",
21
31
  "azure": "claude-haiku-4-5-20251001"
22
32
  },
33
+ "claude-haiku-5-5": {
34
+ "anthropic": "claude-haiku-5-5",
35
+ "openrouter": "anthropic/claude-haiku-5.5",
36
+ "bedrock": "anthropic.claude-haiku-5-5",
37
+ "vertexai": "claude-haiku-5-5",
38
+ "azure": "claude-haiku-5-5"
39
+ },
23
40
  "claude-opus-4-1": {
24
41
  "bedrock": "anthropic.claude-opus-4-1-20250805-v1:0",
25
42
  "vertexai": "claude-opus-4-1"
@@ -94,13 +111,6 @@
94
111
  "vertexai": "claude-sonnet-5-5",
95
112
  "azure": "claude-sonnet-5-5"
96
113
  },
97
- "deepseek-ocr": {
98
- "vertexai": "deepseek-ai/deepseek-ocr-maas"
99
- },
100
- "deepseek-r1-0528": {
101
- "vertexai": "deepseek-ai/deepseek-r1-0528-maas",
102
- "openrouter": "deepseek/deepseek-r1-0528"
103
- },
104
114
  "deepseek-v3.1": {
105
115
  "vertexai": "deepseek-ai/deepseek-v3.1-maas"
106
116
  },
@@ -120,6 +130,10 @@
120
130
  "deepseek": "deepseek-v4-pro",
121
131
  "openrouter": "deepseek/deepseek-v4-pro"
122
132
  },
133
+ "deepseek-v4.1-flash": {
134
+ "vertexai": "deepseek-ai/deepseek-v4.1-flash-maas",
135
+ "openrouter": "deepseek/deepseek-v4.1-flash"
136
+ },
123
137
  "enhanced": {
124
138
  "deepgram": "enhanced-general"
125
139
  },
@@ -142,6 +156,9 @@
142
156
  "openrouter": "google/gemini-2.5-flash-lite",
143
157
  "vertexai": "gemini-2.5-flash-lite"
144
158
  },
159
+ "gemini-2.5-flash-native-audio": {
160
+ "gemini": "gemini-2.5-flash-native-audio-latest"
161
+ },
145
162
  "gemini-2.5-pro": {
146
163
  "gemini": "gemini-2.5-pro",
147
164
  "openrouter": "google/gemini-2.5-pro",
@@ -183,8 +200,7 @@
183
200
  },
184
201
  "gemini-3.1-flash-lite-preview": {
185
202
  "gemini": "gemini-3.1-flash-lite-preview",
186
- "openrouter": "google/gemini-3.1-flash-lite-preview",
187
- "vertexai": "gemini-3.1-flash-lite-preview"
203
+ "openrouter": "google/gemini-3.1-flash-lite-preview"
188
204
  },
189
205
  "gemini-3.1-flash-tts-preview": {
190
206
  "gemini": "gemini-3.1-flash-tts-preview",
@@ -217,7 +233,12 @@
217
233
  },
218
234
  "gemini-3.5-transcribe": {
219
235
  "gemini": "gemini-3.5-transcribe",
220
- "openrouter": "google/gemini-3.5-transcribe"
236
+ "openrouter": "google/gemini-3.5-transcribe",
237
+ "vertexai": "gemini-3.5-transcribe"
238
+ },
239
+ "gemini-3.5-transcribe-live": {
240
+ "gemini": "gemini-3.5-transcribe-live",
241
+ "vertexai": "gemini-3.5-transcribe-live"
221
242
  },
222
243
  "gemini-3.6-flash": {
223
244
  "gemini": "gemini-3.6-flash",
@@ -236,16 +257,28 @@
236
257
  },
237
258
  "gemini-3.8-flash-lite-tts": {
238
259
  "gemini": "gemini-3.8-flash-lite-tts",
239
- "openrouter": "google/gemini-3.8-flash-lite-tts"
260
+ "openrouter": "google/gemini-3.8-flash-lite-tts",
261
+ "vertexai": "gemini-3.8-flash-lite-tts"
240
262
  },
241
263
  "gemini-3.8-flash-tts": {
242
264
  "gemini": "gemini-3.8-flash-tts",
243
- "openrouter": "google/gemini-3.8-flash-tts"
265
+ "openrouter": "google/gemini-3.8-flash-tts",
266
+ "vertexai": "gemini-3.8-flash-tts"
244
267
  },
245
268
  "gemini-3.8-live": {
246
269
  "gemini": "gemini-3.8-live",
247
270
  "vertexai": "gemini-3.8-live"
248
271
  },
272
+ "gemini-4-argon": {
273
+ "gemini": "gemini-4-argon",
274
+ "openrouter": "google/gemini-4-argon",
275
+ "vertexai": "gemini-4-argon"
276
+ },
277
+ "gemini-4-carbon": {
278
+ "gemini": "gemini-4-carbon",
279
+ "openrouter": "google/gemini-4-carbon",
280
+ "vertexai": "gemini-4-carbon"
281
+ },
249
282
  "gemini-embedding-001": {
250
283
  "gemini": "gemini-embedding-001",
251
284
  "openrouter": "google/gemini-embedding-001",
@@ -261,22 +294,36 @@
261
294
  "openrouter": "google/gemini-embedding-2-preview"
262
295
  },
263
296
  "gemini-flash": {
264
- "gemini": "gemini-flash-latest",
265
- "vertexai": "gemini-flash-latest"
297
+ "gemini": "gemini-flash-latest"
266
298
  },
267
299
  "gemini-flash-lite": {
268
- "gemini": "gemini-flash-lite-latest",
269
- "vertexai": "gemini-flash-lite-latest"
300
+ "gemini": "gemini-flash-lite-latest"
270
301
  },
271
302
  "gemini-nano-banana-2.1": {
272
303
  "gemini": "gemini-nano-banana-2.1",
273
304
  "openrouter": "google/gemini-nano-banana-2.1",
274
305
  "vertexai": "gemini-nano-banana-2.1"
275
306
  },
307
+ "gemini-omni-1.1-flash": {
308
+ "gemini": "gemini-omni-1.1-flash",
309
+ "openrouter": "google/gemini-omni-1.1-flash"
310
+ },
311
+ "gemini-omni-1.2-flash": {
312
+ "gemini": "gemini-omni-1.2-flash",
313
+ "openrouter": "google/gemini-omni-1.2-flash",
314
+ "vertexai": "gemini-omni-1.2-flash"
315
+ },
276
316
  "gemini-omni-flash-preview": {
277
317
  "gemini": "gemini-omni-flash-preview",
278
318
  "vertexai": "gemini-omni-flash-preview"
279
319
  },
320
+ "gemini-pro": {
321
+ "gemini": "gemini-pro-latest"
322
+ },
323
+ "gemini-robotics-er-2-preview": {
324
+ "gemini": "gemini-robotics-er-2-preview",
325
+ "vertexai": "gemini-robotics-er-2-preview"
326
+ },
280
327
  "gemma-4-26b-a4b-it": {
281
328
  "gemini": "gemma-4-26b-a4b-it",
282
329
  "openrouter": "google/gemma-4-26b-a4b-it",
@@ -298,28 +345,10 @@
298
345
  "vertexai": "zai-org/glm-5.2-maas",
299
346
  "openrouter": "z-ai/glm-5.2"
300
347
  },
301
- "gpt-3.5-turbo": {
302
- "openai": "gpt-3.5-turbo",
303
- "openrouter": "openai/gpt-3.5-turbo"
304
- },
305
- "gpt-3.5-turbo-16k": {
306
- "openai": "gpt-3.5-turbo-16k",
307
- "openrouter": "openai/gpt-3.5-turbo-16k"
308
- },
309
- "gpt-3.5-turbo-instruct": {
310
- "openai": "gpt-3.5-turbo-instruct",
311
- "openrouter": "openai/gpt-3.5-turbo-instruct",
312
- "azure": "gpt-3.5-turbo-instruct"
313
- },
314
348
  "gpt-4": {
315
349
  "openai": "gpt-4",
316
350
  "openrouter": "openai/gpt-4"
317
351
  },
318
- "gpt-4-turbo": {
319
- "openai": "gpt-4-turbo",
320
- "openrouter": "openai/gpt-4-turbo",
321
- "azure": "gpt-4-turbo"
322
- },
323
352
  "gpt-4.1": {
324
353
  "openai": "gpt-4.1",
325
354
  "openrouter": "openai/gpt-4.1",
@@ -400,31 +429,11 @@
400
429
  "openrouter": "openai/gpt-5.1",
401
430
  "azure": "gpt-5.1"
402
431
  },
403
- "gpt-5.1-codex": {
404
- "openai": "gpt-5.1-codex",
405
- "openrouter": "openai/gpt-5.1-codex",
406
- "azure": "gpt-5.1-codex"
407
- },
408
- "gpt-5.1-codex-max": {
409
- "openai": "gpt-5.1-codex-max",
410
- "openrouter": "openai/gpt-5.1-codex-max",
411
- "azure": "gpt-5.1-codex-max"
412
- },
413
- "gpt-5.1-codex-mini": {
414
- "openai": "gpt-5.1-codex-mini",
415
- "openrouter": "openai/gpt-5.1-codex-mini",
416
- "azure": "gpt-5.1-codex-mini"
417
- },
418
432
  "gpt-5.2": {
419
433
  "openai": "gpt-5.2",
420
434
  "openrouter": "openai/gpt-5.2",
421
435
  "azure": "gpt-5.2"
422
436
  },
423
- "gpt-5.2-codex": {
424
- "openai": "gpt-5.2-codex",
425
- "openrouter": "openai/gpt-5.2-codex",
426
- "azure": "gpt-5.2-codex"
427
- },
428
437
  "gpt-5.2-pro": {
429
438
  "openai": "gpt-5.2-pro",
430
439
  "openrouter": "openai/gpt-5.2-pro"
@@ -498,6 +507,11 @@
498
507
  "openrouter": "openai/gpt-6.1-sol",
499
508
  "azure": "gpt-6.1-sol"
500
509
  },
510
+ "gpt-6.1-terra": {
511
+ "openai": "gpt-6.1-terra",
512
+ "openrouter": "openai/gpt-6.1-terra",
513
+ "azure": "gpt-6.1-terra"
514
+ },
501
515
  "gpt-audio": {
502
516
  "openai": "gpt-audio",
503
517
  "openrouter": "openai/gpt-audio",
@@ -508,11 +522,6 @@
508
522
  "openrouter": "openai/gpt-audio-mini",
509
523
  "azure": "gpt-audio-mini"
510
524
  },
511
- "gpt-image-1": {
512
- "openai": "gpt-image-1",
513
- "openrouter": "openai/gpt-image-1",
514
- "azure": "gpt-image-1"
515
- },
516
525
  "gpt-image-1-mini": {
517
526
  "openai": "gpt-image-1-mini",
518
527
  "openrouter": "openai/gpt-image-1-mini",
@@ -533,6 +542,14 @@
533
542
  "openrouter": "openai/gpt-image-2.5-sunburst",
534
543
  "azure": "gpt-image-2.5-sunburst"
535
544
  },
545
+ "gpt-image-2.6-flare": {
546
+ "openai": "gpt-image-2.6-flare",
547
+ "openrouter": "openai/gpt-image-2.6-flare"
548
+ },
549
+ "gpt-image-2.6-sunburst": {
550
+ "openai": "gpt-image-2.6-sunburst",
551
+ "openrouter": "openai/gpt-image-2.6-sunburst"
552
+ },
536
553
  "gpt-oss-120b": {
537
554
  "vertexai": "openai/gpt-oss-120b-maas",
538
555
  "openrouter": "openai/gpt-oss-120b"
@@ -612,6 +629,9 @@
612
629
  "grok-4.6": {
613
630
  "vertexai": "xai/grok-4.6"
614
631
  },
632
+ "grok-4.7": {
633
+ "vertexai": "xai/grok-4.7"
634
+ },
615
635
  "grok-latest": {
616
636
  "xai": "grok-4.3"
617
637
  },
@@ -626,6 +646,9 @@
626
646
  "llama-4-maverick-17b-128e-instruct": {
627
647
  "vertexai": "meta/llama-4-maverick-17b-128e-instruct-maas"
628
648
  },
649
+ "llama-4-scout-17b-16e-instruct": {
650
+ "vertexai": "meta/llama-4-scout-17b-16e-instruct-maas"
651
+ },
629
652
  "lyria-3-clip-preview": {
630
653
  "gemini": "lyria-3-clip-preview",
631
654
  "openrouter": "google/lyria-3-clip-preview"
@@ -648,49 +671,19 @@
648
671
  "nova-3": {
649
672
  "deepgram": "nova-3-general"
650
673
  },
651
- "o1": {
652
- "openai": "o1",
653
- "openrouter": "openai/o1",
654
- "azure": "o1"
655
- },
656
- "o1-pro": {
657
- "openai": "o1-pro",
658
- "openrouter": "openai/o1-pro",
659
- "azure": "o1-pro"
660
- },
661
674
  "o3": {
662
675
  "openai": "o3",
663
676
  "openrouter": "openai/o3",
664
677
  "azure": "o3"
665
678
  },
666
- "o3-mini": {
667
- "openai": "o3-mini",
668
- "openrouter": "openai/o3-mini",
669
- "azure": "o3-mini"
670
- },
671
679
  "o3-pro": {
672
680
  "openai": "o3-pro",
673
- "openrouter": "openai/o3-pro"
674
- },
675
- "o4-mini": {
676
- "openai": "o4-mini",
677
- "openrouter": "openai/o4-mini",
678
- "azure": "o4-mini"
681
+ "openrouter": "openai/o3-pro",
682
+ "azure": "o3-pro"
679
683
  },
680
684
  "qwen3-235b-a22b-instruct-2507": {
681
685
  "vertexai": "qwen/qwen3-235b-a22b-instruct-2507-maas"
682
686
  },
683
- "qwen3-coder-480b-a35b-instruct": {
684
- "vertexai": "qwen/qwen3-coder-480b-a35b-instruct-maas"
685
- },
686
- "qwen3-next-80b-a3b-instruct": {
687
- "vertexai": "qwen/qwen3-next-80b-a3b-instruct-maas",
688
- "openrouter": "qwen/qwen3-next-80b-a3b-instruct"
689
- },
690
- "qwen3-next-80b-a3b-thinking": {
691
- "vertexai": "qwen/qwen3-next-80b-a3b-thinking-maas",
692
- "openrouter": "qwen/qwen3-next-80b-a3b-thinking"
693
- },
694
687
  "text-embedding-3-large": {
695
688
  "openai": "text-embedding-3-large",
696
689
  "openrouter": "openai/text-embedding-3-large",
@@ -37,6 +37,18 @@ module RubyLLM
37
37
  # Media resolutions accepted by +resolution:+.
38
38
  RESOLUTIONS = %i[low medium high ultra_high original].freeze
39
39
 
40
+ # How the model reads a video: +:static+, +:agentic+, or +nil+ for the
41
+ # provider default.
42
+ attr_reader :processing
43
+
44
+ # Video processing modes accepted by +processing:+.
45
+ PROCESSING_MODES = %i[static agentic].freeze
46
+
47
+ ATTRIBUTE_ESCAPES = { # :nodoc:
48
+ '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', "'" => '&#39;', '"' => '&quot;'
49
+ }.freeze
50
+ CLOSING_FILE_TAG = %r{</(file)}i # :nodoc:
51
+
40
52
  # File extensions recognized as document attachments when the MIME type
41
53
  # alone is inconclusive.
42
54
  DOCUMENT_EXTENSIONS = %w[
@@ -62,7 +74,7 @@ module RubyLLM
62
74
 
63
75
  def self.coerce(source, config:) # :nodoc:
64
76
  return if source.nil? || (source.is_a?(String) && source.strip.empty?)
65
- return source if source.is_a?(Attachment)
77
+ return source.with_default_config(config) if source.is_a?(Attachment)
66
78
  return source.to_attachment if source.respond_to?(:to_attachment)
67
79
  return from_h(source, config:) if source.is_a?(Hash)
68
80
 
@@ -72,7 +84,9 @@ module RubyLLM
72
84
 
73
85
  def self.from_h(data, config: nil) # :nodoc:
74
86
  data = data.transform_keys(&:to_sym)
75
- new(data.fetch(:source), filename: data[:filename], resolution: data[:resolution]&.to_sym, config:)
87
+ new(data.fetch(:source), filename: data[:filename], mime_type: data[:mime_type],
88
+ resolution: data[:resolution]&.to_sym,
89
+ processing: data[:processing]&.to_sym, config:)
76
90
  end
77
91
 
78
92
  # Creates an attachment from +source+: a file path, URL, IO-like object,
@@ -89,31 +103,53 @@ module RubyLLM
89
103
  #
90
104
  # RubyLLM::Attachment.new("page-3.png", resolution: :ultra_high)
91
105
  #
92
- # +config:+ is the Configuration a URL source is downloaded with, and
93
- # defaults to the global one.
106
+ # +processing:+ chooses how the model reads a video. +:static+ samples
107
+ # frames across the whole video up front. +:agentic+ lets the model
108
+ # navigate a long video and load only the parts it needs. Providers
109
+ # without the setting ignore it.
110
+ #
111
+ # RubyLLM::Attachment.new("lecture.mp4", processing: :agentic)
112
+ #
113
+ # +mime_type:+ states the file's type, for a source RubyLLM cannot tell
114
+ # it from, such as an IO, instead of detecting it. A <tt>charset</tt>
115
+ # says how to read binary text; #mime_type leaves parameters out. A
116
+ # Rails attachment's type comes from its blob, so set it there for one.
117
+ #
118
+ # RubyLLM::Attachment.new(io, filename: "export.csv", mime_type: "text/csv")
119
+ #
120
+ # +config:+ is the Configuration a URL source is downloaded with. It
121
+ # defaults to that of the chat or operation the attachment is given to,
122
+ # then to the global one.
94
123
  #
95
124
  # Paths and URLs must be trusted and authorized by the application.
96
125
  # They can be read or fetched during construction to detect the MIME type.
97
126
  # Validate upload parameters before passing them here: an unchecked String
98
127
  # can access local files or internal network endpoints.
99
- def initialize(source, filename: nil, resolution: nil, config: nil)
100
- unless resolution.nil? || RESOLUTIONS.include?(resolution)
101
- raise ArgumentError, "resolution must be one of #{RESOLUTIONS.join(', ')}, got #{resolution.inspect}"
102
- end
128
+ def initialize(source, filename: nil, mime_type: nil, resolution: nil, processing: nil, config: nil)
129
+ check_option(:resolution, resolution, RESOLUTIONS)
130
+ check_option(:processing, processing, PROCESSING_MODES)
103
131
 
104
132
  @config = config
105
133
  @resolution = resolution
134
+ @processing = processing
106
135
  @source = source
107
136
  @source = source_type_cast
108
137
  @filename = filename || source_filename
109
138
 
110
- determine_mime_type
139
+ @given_mime_type = mime_type
140
+ @mime_type = media_type(mime_type)
141
+ determine_mime_type unless @mime_type
111
142
  end
112
143
 
113
144
  def config # :nodoc:
114
145
  @config || RubyLLM.config
115
146
  end
116
147
 
148
+ def with_default_config(config) # :nodoc:
149
+ @config ||= config
150
+ self
151
+ end
152
+
117
153
  def url? # :nodoc:
118
154
  @source.is_a?(URI) || (@source.is_a?(String) && @source.match?(%r{\Ahttps?://}i))
119
155
  end
@@ -149,7 +185,7 @@ module RubyLLM
149
185
  end
150
186
 
151
187
  def io_like? # :nodoc:
152
- @source.respond_to?(:read) && !path? && !active_storage? && !provider_file?
188
+ @source.respond_to?(:read) && !path? && !url? && !active_storage? && !provider_file?
153
189
  end
154
190
 
155
191
  def active_storage? # :nodoc:
@@ -178,7 +214,8 @@ module RubyLLM
178
214
  def for_llm # :nodoc:
179
215
  case type
180
216
  when :text
181
- "<file name='#{filename}' mime_type='#{mime_type}'>#{content}</file>"
217
+ "<file name='#{escape_attribute(filename)}' mime_type='#{escape_attribute(mime_type)}'>" \
218
+ "#{content.to_s.gsub(CLOSING_FILE_TAG, '<\/\1')}</file>"
182
219
  else
183
220
  data_uri
184
221
  end
@@ -250,8 +287,23 @@ module RubyLLM
250
287
  RubyLLM::Files::MimeType.text? mime_type
251
288
  end
252
289
 
290
+ # Returns whether the attachment holds only the start of a text, the
291
+ # way a chat with a file store sends a file longer than its inline
292
+ # limit (see Chat#with_file_store). The whole file stays in the chat's
293
+ # files, and the text says where. An attachment you make is never
294
+ # partial.
295
+ def partial?
296
+ @partial == true
297
+ end
298
+
299
+ def mark_partial # :nodoc:
300
+ @partial = true
301
+ self
302
+ end
303
+
253
304
  def to_h # :nodoc:
254
- { type: type, source: @source, filename: filename, resolution: resolution }.compact
305
+ { type: type, source: @source, filename: filename, mime_type: @given_mime_type, resolution: resolution,
306
+ processing: processing }.compact
255
307
  end
256
308
 
257
309
  def byte_size # :nodoc:
@@ -260,6 +312,16 @@ module RubyLLM
260
312
 
261
313
  private
262
314
 
315
+ def escape_attribute(value)
316
+ value.to_s.gsub(/[&<>'"]/, ATTRIBUTE_ESCAPES)
317
+ end
318
+
319
+ def check_option(name, value, allowed)
320
+ return if value.nil? || allowed.include?(value)
321
+
322
+ raise ArgumentError, "#{name} must be one of #{allowed.join(', ')}, got #{value.inspect}"
323
+ end
324
+
263
325
  def data_uri
264
326
  "data:#{mime_type};base64,#{encoded}"
265
327
  end
@@ -309,12 +371,26 @@ module RubyLLM
309
371
  end
310
372
  end
311
373
 
312
- # Text content is loaded as binary; retag it so payloads serialize as UTF-8.
374
+ # Text content loaded as binary is retagged so payloads serialize as
375
+ # UTF-8. An IO can return text already tagged, with bytes its encoding
376
+ # does not allow.
313
377
  def normalize_text_encoding
314
- return unless text? && @content.is_a?(String) && @content.encoding == Encoding::ASCII_8BIT
378
+ return unless text? && @content.is_a?(String)
315
379
 
316
- text = @content.dup.force_encoding(Encoding::UTF_8)
317
- @content = text.valid_encoding? ? text : text.scrub
380
+ @content = decoded_text(@content) if @content.encoding == Encoding::ASCII_8BIT
381
+ @content = @content.scrub unless @content.valid_encoding?
382
+ end
383
+
384
+ def decoded_text(bytes)
385
+ charset = @given_mime_type.to_s[/charset="?([^";\s]+)/i, 1] || Encoding::UTF_8
386
+ bytes.dup.force_encoding(charset).encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
387
+ rescue ArgumentError, EncodingError
388
+ bytes.dup.force_encoding(Encoding::UTF_8)
389
+ end
390
+
391
+ def media_type(mime_type)
392
+ type = mime_type.to_s.split(';').first.to_s.strip.downcase
393
+ type unless type.empty?
318
394
  end
319
395
 
320
396
  def determine_mime_type
@@ -324,13 +400,18 @@ module RubyLLM
324
400
  return @mime_type = content_type unless content_type.to_s.empty?
325
401
 
326
402
  @mime_type = RubyLLM::Files::MimeType.for(url? ? nil : @source, name: @filename)
327
- @mime_type = RubyLLM::Files::MimeType.for(content) if @mime_type == 'application/octet-stream'
403
+ @mime_type = content_mime_type if @mime_type == 'application/octet-stream'
328
404
  @mime_type = 'audio/wav' if @mime_type == 'audio/x-wav'
329
405
  end
330
406
 
407
+ # An IO's bytes were examined already, so it isn't read in full for a
408
+ # second look.
409
+ def content_mime_type
410
+ io_like? ? @mime_type : RubyLLM::Files::MimeType.for(content)
411
+ end
412
+
331
413
  def fetch_content
332
- response = Transport::Connection.basic(config).get @source.to_s
333
- @content = response.body
414
+ @content = Transport::Connection.download(@source, config)
334
415
  end
335
416
 
336
417
  def load_content_from_path
@@ -406,7 +487,7 @@ module RubyLLM
406
487
  end
407
488
 
408
489
  def inspect_attributes # :nodoc:
409
- { filename: filename, mime_type: mime_type, resolution: resolution, source: @source }
490
+ { filename: filename, mime_type: mime_type, resolution: resolution, processing: processing, source: @source }
410
491
  end
411
492
  end
412
493
  end