ruby_llm 2.1.1 → 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 +160 -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 +515 -274
  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 +1593 -331
  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 +50 -24
  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 +127 -106
  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 +15 -15
  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 +1 -7
  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 +5 -14
  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 +213 -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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Protocol
5
+ # Turns the NotImplementedError of a missing seam into an Error naming the
6
+ # provider and the operation. It wraps only Protocol's own methods, so a
7
+ # subclass that replaces an operation answers for its own errors.
8
+ module UnsupportedOperations # :nodoc:
9
+ OPERATIONS = {
10
+ tool_approval_response: 'remote tool approvals',
11
+ render: 'chat',
12
+ count_tokens: 'token counting',
13
+ tokenize: 'text tokenization',
14
+ compact: 'manual compaction',
15
+ list_models: 'model listing',
16
+ embed: 'embeddings',
17
+ moderate: 'moderation',
18
+ paint: 'image generation',
19
+ animate_later: 'video generation',
20
+ find_video_job: 'video generation',
21
+ refresh_video_job: 'video generation',
22
+ speak: 'speech generation',
23
+ transcribe: 'transcription',
24
+ ocr: 'OCR',
25
+ rerank: 'reranking',
26
+ judge: 'judgments',
27
+ cache_content: 'explicit content caching',
28
+ find_cache: 'explicit content caching',
29
+ delete_cache: 'explicit content caching',
30
+ extend_cache: 'explicit content caching'
31
+ }.freeze
32
+
33
+ OPERATIONS.each do |operation, label|
34
+ define_method(operation) do |*args, **options, &block|
35
+ super(*args, **options, &block)
36
+ rescue NotImplementedError
37
+ raise Error, "#{@provider.name} doesn't support #{label}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -38,6 +38,9 @@ module RubyLLM
38
38
  include Streaming
39
39
  include BinaryStreaming
40
40
  include RequestShapes
41
+ include NativeContent
42
+ prepend UnsupportedOperations
43
+ prepend ToolNamespaces
41
44
 
42
45
  # The Provider this protocol talks through.
43
46
  attr_reader :provider
@@ -91,18 +94,37 @@ module RubyLLM
91
94
  @model = model
92
95
  end
93
96
 
94
- def tool_approval_response(tool_call, approved:)
95
- Message.new(role: :tool, content: approved ? 'Approved' : 'Denied', tool_call_id: tool_call.id,
96
- raw_content: render_tool_approval_response(tool_call, approved:))
97
- rescue NotImplementedError
98
- raise Error, "#{@provider.name} doesn't support remote tool approvals"
97
+ # Returns the options of +operation+ (+:upload+, +:paint+, +:transcribe+,
98
+ # or +:embed+) this wire format has no way to carry, as an Array of
99
+ # Symbols. RubyLLM raises ArgumentError when a caller gives one, rather
100
+ # than dropping it. The default is none.
101
+ #
102
+ # def rejected_options(operation)
103
+ # operation == :paint ? %i[count] : super
104
+ # end
105
+ #
106
+ def rejected_options(_operation)
107
+ []
108
+ end
109
+
110
+ def reject_options(operation, **options) # :nodoc:
111
+ rejected = rejected_options(operation) & options.compact.keys
112
+ return if rejected.empty?
113
+
114
+ raise ArgumentError, "#{@provider.name} #{operation} does not accept #{rejected.join(', ')}"
115
+ end
116
+
117
+ def tool_approval_response(tool_call, approved:, note: nil)
118
+ Message.new(role: :tool, content: approved ? 'Approved' : note || 'Denied', tool_call_id: tool_call.id,
119
+ provider_call_id: tool_call.provider_call_id,
120
+ raw_content: render_tool_approval_response(tool_call, approved:, note:))
99
121
  end
100
122
 
101
123
  def complete(messages, tools:, temperature:, provider_options: {}, headers: {}, schema: nil, thinking: nil,
102
124
  max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
103
- usage_recorder: nil, provider_tools: [], compaction: nil, end_user: nil, &)
125
+ usage_recorder: nil, progress_recorder: nil, provider_tools: [], compaction: nil, end_user: nil, &)
104
126
  resolution = resolve_provider_tools_for_request(provider_tools)
105
- headers = resolution.headers.merge(headers) if resolution
127
+ headers = merge_tool_headers(resolution.headers, headers) if resolution
106
128
  headers = apply_compaction_headers(headers, compaction) if compaction
107
129
  payload = render(
108
130
  messages, tools:, tool_prefs:, temperature:, max_output_tokens:, provider_options:, schema:, thinking:,
@@ -110,6 +132,10 @@ module RubyLLM
110
132
  stream: block_given?
111
133
  )
112
134
 
135
+ complete_request(payload, headers, usage_recorder:, progress_recorder:, &)
136
+ end
137
+
138
+ def complete_request(payload, headers, usage_recorder: nil, **) # :nodoc:
113
139
  # The caller's block can raise the error of another operation it ran.
114
140
  yielding = false
115
141
  track_usage(:chat, on_finish: usage_recorder) do
@@ -124,11 +150,23 @@ module RubyLLM
124
150
  sync_response payload, headers
125
151
  end
126
152
  end
153
+ rescue Error => e
154
+ raise e if yielding
155
+
156
+ e = unsupported_parameter_error(e)
157
+ claim_error(e, payload)
158
+ raise e
127
159
  rescue NotImplementedError
160
+ raise if yielding
161
+
128
162
  raise Error, "#{@provider.name} doesn't support chat"
129
- rescue Error => e
130
- claim_error(e, payload) unless yielding
131
- raise
163
+ end
164
+
165
+ # Returns the parameter a rejected request named as one the model does
166
+ # not take, as UnsupportedParameterError#parameter describes, or +nil+.
167
+ # Protocols whose errors say so in structured fields override it.
168
+ def parse_unsupported_parameter(_response)
169
+ nil
132
170
  end
133
171
 
134
172
  def render(messages, tools:, temperature:, provider_options: {}, schema: nil, thinking: nil,
@@ -153,8 +191,35 @@ module RubyLLM
153
191
  payload = apply_provider_tools(payload, provider_tools)
154
192
  payload = apply_tool_search(payload)
155
193
  apply_before_request_hooks(payload, before_request)
156
- rescue NotImplementedError
157
- raise Error, "#{@provider.name} doesn't support chat"
194
+ end
195
+
196
+ # Returns whether the API searches deferred tools itself. Chat gives a
197
+ # model on any other API a +tool_search+ tool of its own instead.
198
+ def tool_search?
199
+ false
200
+ end
201
+
202
+ # Returns +caching+ for a fallback attempt, without the options this
203
+ # wire format cannot express, so a hint meant for the model the caller
204
+ # chose does not stop another provider from answering. An explicit
205
+ # cache +id+ names content the prompt relies on and stays.
206
+ def fallback_caching(caching) # :nodoc:
207
+ names = cache_option_names
208
+ return caching unless names
209
+
210
+ dropped = caching.keys - names - [:id]
211
+ return caching if dropped.empty?
212
+
213
+ RubyLLM.logger.debug do
214
+ "RubyLLM left out #{dropped.map { |name| ":#{name}" }.join(', ')} from caching on a fallback to " \
215
+ "#{@provider.name}, which has no use for it"
216
+ end
217
+ caching.except(*dropped)
218
+ end
219
+
220
+ # Returns whether this wire format takes a cache key.
221
+ def cache_key? # :nodoc:
222
+ cache_option_names&.include?(:key) || false
158
223
  end
159
224
 
160
225
  # Writes the chat's safety identifier into the request payload and
@@ -218,8 +283,6 @@ module RubyLLM
218
283
  caching: caching
219
284
  )
220
285
  parse_count_tokens_response post_count_tokens(payload)
221
- rescue NotImplementedError
222
- raise Error, "#{@provider.name} doesn't support token counting"
223
286
  rescue Error => e
224
287
  claim_error(e, payload)
225
288
  raise
@@ -234,8 +297,6 @@ module RubyLLM
234
297
  payload = render_tokenization_payload(text, model:)
235
298
  response = @connection.post tokenization_url, payload
236
299
  parse_tokenization_response(response, model:)
237
- rescue NotImplementedError
238
- raise Error, "#{@provider.name} doesn't support text tokenization"
239
300
  end
240
301
 
241
302
  def compact(messages, headers: {}, before_request: [], usage_recorder: nil)
@@ -246,8 +307,6 @@ module RubyLLM
246
307
  end
247
308
  parse_compaction_response(response)
248
309
  end
249
- rescue NotImplementedError
250
- raise Error, "#{@provider.name} doesn't support manual compaction"
251
310
  rescue Error => e
252
311
  claim_error(e, payload)
253
312
  raise
@@ -255,7 +314,7 @@ module RubyLLM
255
314
 
256
315
  def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {})
257
316
  attachments = Attachment.wrap(with, config: @config)
258
- raise UnsupportedAttachmentError, attachments.first.mime_type if attachments.any? && !supports_embedding_media?
317
+ raise UnsupportedAttachmentError, attachments.first.mime_type if attachments.any? && !embedding_media?
259
318
 
260
319
  track_usage(:embedding) do
261
320
  payload = if attachments.any?
@@ -265,10 +324,8 @@ module RubyLLM
265
324
  render_embedding_payload(text, model:, dimensions:, task_type:, title:, provider_options:)
266
325
  end
267
326
  response = @connection.post(embedding_url(model:), payload, usage: @usage_tracker)
268
- parse_embedding_response(response, model:, text:)
327
+ ensure_embedding_per_text(parse_embedding_response(response, model:, text:), text, attachments, response)
269
328
  end
270
- rescue NotImplementedError
271
- raise Error, "#{@provider.name} doesn't support embeddings"
272
329
  end
273
330
 
274
331
  def render_embedding(text, model:, dimensions: nil, task_type: nil, title: nil, provider_options: {}) # :nodoc:
@@ -282,8 +339,6 @@ module RubyLLM
282
339
  response = @connection.post moderation_url, payload, usage: @usage_tracker
283
340
  parse_moderation_response(response, model:)
284
341
  end
285
- rescue NotImplementedError
286
- raise Error, "#{@provider.name} doesn't support moderation"
287
342
  end
288
343
 
289
344
  def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
@@ -295,8 +350,6 @@ module RubyLLM
295
350
  images.each { |image| image.config = @config }
296
351
  images.size <= 1 ? images.first : images
297
352
  end
298
- rescue NotImplementedError
299
- raise Error, "#{@provider.name} doesn't support image generation"
300
353
  end
301
354
 
302
355
  def post_image(payload, with:, mask:)
@@ -329,8 +382,6 @@ module RubyLLM
329
382
  end
330
383
  response = post_video(url, payload)
331
384
  parse_video_job(response, model:, **parse_video_request(payload))
332
- rescue NotImplementedError
333
- raise Error, "#{@provider.name} doesn't support video generation"
334
385
  end
335
386
 
336
387
  # Returns the duration in seconds and the resolution a rendered video
@@ -387,6 +438,10 @@ module RubyLLM
387
438
  parse_video_job_status @connection.get(video_job_url(job)), job: job
388
439
  end
389
440
 
441
+ def find_video_job(id, model:)
442
+ VideoJob.new(id:, protocol: self, model:).refresh
443
+ end
444
+
390
445
  def speak(input, model:, voice:, format:, provider_options: {}, &block)
391
446
  track_usage(:speech) do
392
447
  payload = render_speech_payload(input, model:, voice:, format:, provider_options:)
@@ -395,8 +450,6 @@ module RubyLLM
395
450
  response = @connection.post speech_url(model:), payload, usage: @usage_tracker
396
451
  parse_speech_response(response, model:, voice:, format:)
397
452
  end
398
- rescue NotImplementedError
399
- raise Error, "#{@provider.name} doesn't support speech generation"
400
453
  end
401
454
 
402
455
  def stream_speech(*, **, &)
@@ -431,8 +484,6 @@ module RubyLLM
431
484
  response = @connection.post transcription_url, payload, usage: @usage_tracker
432
485
  parse_transcription_response(response, model:)
433
486
  end
434
- rescue NotImplementedError
435
- raise Error, "#{@provider.name} doesn't support transcription"
436
487
  end
437
488
 
438
489
  # Streams a transcription, yielding TranscriptionChunk objects and
@@ -449,7 +500,7 @@ module RubyLLM
449
500
  # Whether the protocol can embed media attachments alongside text.
450
501
  # Protocols that support multimodal embeddings override this and accept
451
502
  # a +with:+ array of Attachments in render_embedding_payload.
452
- def supports_embedding_media?
503
+ def embedding_media?
453
504
  false
454
505
  end
455
506
 
@@ -459,8 +510,6 @@ module RubyLLM
459
510
  response = @connection.post ocr_url, payload, usage: @usage_tracker
460
511
  parse_ocr_response(response, model:)
461
512
  end
462
- rescue NotImplementedError
463
- raise Error, "#{@provider.name} doesn't support OCR"
464
513
  end
465
514
 
466
515
  def rerank(query, documents, model:, top_n: nil, provider_options: {})
@@ -469,8 +518,6 @@ module RubyLLM
469
518
  response = @connection.post rerank_url, payload, usage: @usage_tracker
470
519
  parse_rerank_response(response, model:, documents:)
471
520
  end
472
- rescue NotImplementedError
473
- raise Error, "#{@provider.name} doesn't support reranking"
474
521
  end
475
522
 
476
523
  def cache_content(content, model:, ttl: nil, instructions: nil, with: nil)
@@ -478,8 +525,6 @@ module RubyLLM
478
525
  attachments: Attachment.wrap(with, config: @config))
479
526
  response = @connection.post caches_url, payload, idempotent: false
480
527
  parse_cache_response(response.body)
481
- rescue NotImplementedError
482
- raise Error, "#{@provider.name} doesn't support explicit content caching"
483
528
  end
484
529
 
485
530
  def judge(input, questions:, model:, with: [], provider_options: {}) # :nodoc:
@@ -488,35 +533,35 @@ module RubyLLM
488
533
  response = @connection.post judgment_url, payload, usage: @usage_tracker
489
534
  parse_judgment_response(response, questions:)
490
535
  end
491
- rescue NotImplementedError
492
- raise Error, "#{@provider.name} doesn't support judgments"
493
536
  end
494
537
 
495
538
  def find_cache(name)
496
539
  response = @connection.get cache_url(name)
497
540
  parse_cache_response(response.body)
498
- rescue NotImplementedError
499
- raise Error, "#{@provider.name} doesn't support explicit content caching"
500
541
  end
501
542
 
502
543
  def delete_cache(name)
503
544
  @connection.delete cache_url(name)
504
545
  true
505
- rescue NotImplementedError
506
- raise Error, "#{@provider.name} doesn't support explicit content caching"
507
546
  end
508
547
 
509
548
  def extend_cache(name, ttl:)
510
549
  response = @connection.patch cache_url(name), render_cache_update_payload(ttl:)
511
550
  parse_cache_response(response.body)
512
- rescue NotImplementedError
513
- raise Error, "#{@provider.name} doesn't support explicit content caching"
514
551
  end
515
552
 
516
553
  def parse_error(response)
517
554
  @provider.parse_error(response)
518
555
  end
519
556
 
557
+ def slug # :nodoc:
558
+ @provider.slug
559
+ end
560
+
561
+ def parse_error_code(response) # :nodoc:
562
+ @provider.parse_error_code(response)
563
+ end
564
+
520
565
  # Returns a RequestShape describing +payload+, a conversation request
521
566
  # this protocol rendered, as JSON parsed back with String keys.
522
567
  # Error#request_shape asks for it when the request fails. The default
@@ -577,68 +622,29 @@ module RubyLLM
577
622
 
578
623
  private
579
624
 
580
- # An error raised before the request went out answers no request.
581
- def claim_error(error, payload)
582
- return if error.request_protocol || error.response.nil? || payload.nil?
583
-
584
- error.request_protocol = self
585
- error.request_payload = payload
586
- end
587
-
588
- # A thinking signature or a provider-shaped content block belongs to the
589
- # model that produced it, and one provider can serve models that do not
590
- # read each other's, so a message another model produced replays
591
- # without them. A message with no known producer replays as it is.
592
- def foreign_native_content?(message)
593
- return false unless message.role == :assistant && carries_native_content?(message)
594
-
595
- producer = producing_entry(message)
596
- !producer.nil? && (producer.provider != @provider.slug || !current_model?(producer.model))
597
- end
598
-
599
- # Usage records the id a chat resolved its model to, and a message built
600
- # by hand may name the model by an alias of it.
601
- def current_model?(model_id)
602
- return true if @model.nil? || model_id == @model.id
603
-
604
- RubyLLM.models.find(model_id, provider: @provider.slug, config: @config).id == @model.id
605
- rescue ModelNotFoundError
606
- false
607
- end
608
-
609
- # Another model of this provider gets the raw content any of its models
610
- # reads, such as a tool search, and none of the producing model's own.
611
- def without_foreign_native_content(message)
612
- portable = portable_raw_content(message) if producer_slug(message) == @provider.slug
613
- message.without_native_content(raw_content: portable)
614
- end
615
-
616
- def portable_raw_content(_message)
625
+ # The with_caching options this wire format can express, or +nil+ when
626
+ # it ignores the ones it has no use for.
627
+ def cache_option_names
617
628
  nil
618
629
  end
619
630
 
620
- def carries_native_content?(message)
621
- return true if message.thinking || message.raw_reasoning || message.raw_content
631
+ def unsupported_parameter_error(error)
632
+ return error unless error.instance_of?(BadRequestError) && error.response
622
633
 
623
- message.tool_call? && message.tool_calls.each_value.any?(&:thought_signature)
624
- end
634
+ parameter = parse_unsupported_parameter(error.response)
635
+ return error unless parameter
625
636
 
626
- # Only a usage entry names the producer: a model id alone can belong
627
- # to several providers.
628
- def producer_slug(message)
629
- producing_entry(message)&.provider
637
+ UnsupportedParameterError.new(nil, parameter:, response: error.response, provider: error.provider,
638
+ provider_message: error.provider_message, error_code: error.error_code,
639
+ redact: @provider.config&.redact_error_messages)
630
640
  end
631
641
 
632
- def producing_entry(message)
633
- message.ruby_llm_usage_entries.reverse.find(&:succeeded?)
634
- end
642
+ # An error raised before the request went out answers no request.
643
+ def claim_error(error, payload)
644
+ return if error.request_protocol || error.response.nil? || payload.nil?
635
645
 
636
- # A thinking signature can be another provider's opaque blob, and a
637
- # message restored without its usage could come from anyone, so a
638
- # protocol that verifies signatures gets back only its own.
639
- def own_signature(message)
640
- signature = message.thinking&.signature
641
- signature if signature && producer_slug(message) == @provider.slug
646
+ error.request_protocol = self
647
+ error.request_payload = payload
642
648
  end
643
649
 
644
650
  def resolve_provider_tools_for_request(entries)
@@ -654,6 +660,10 @@ module RubyLLM
654
660
  RubyLLM::Tools::ProviderTools.resolve(entries, aliases: aliases, owner: @provider.name)
655
661
  end
656
662
 
663
+ def merge_tool_headers(tool_headers, headers)
664
+ tool_headers.merge(headers)
665
+ end
666
+
657
667
  def apply_provider_tools(payload, entries)
658
668
  resolution = resolve_provider_tools_for_request(entries)
659
669
  return payload unless resolution
@@ -673,6 +683,13 @@ module RubyLLM
673
683
  payload
674
684
  end
675
685
 
686
+ def ensure_embedding_per_text(embedding, text, attachments, response)
687
+ count = Array(embedding.vectors).size
688
+ return embedding if attachments.any? || !text.is_a?(Array) || count == text.size
689
+
690
+ raise Error.new("Expected #{text.size} embeddings, got #{count}", response:)
691
+ end
692
+
676
693
  def track_usage(operation, on_finish: nil, model_id: nil)
677
694
  @usage_tracker = Accounting::Usage::Tracker.new(
678
695
  operation:,
@@ -689,19 +706,22 @@ module RubyLLM
689
706
  @usage_tracker.fail_pending(e)
690
707
  raise
691
708
  ensure
709
+ @usage_tracker.cancel_pending
692
710
  @usage_tracker = nil
693
711
  end
694
712
 
695
713
  def apply_before_request_hooks(payload, hooks)
696
- Array(hooks).each { |hook| hook.call(payload) }
697
- payload
714
+ hooks = Array(hooks)
715
+ return payload if hooks.empty?
716
+
717
+ Support::Utils.deep_dup(payload).tap { |copy| hooks.each { |hook| hook.call(copy) } }
698
718
  end
699
719
 
700
720
  def auto_upload_large_files?
701
- @config.auto_upload_large_files && @provider.files? && supports_provider_file_references?
721
+ @config.auto_upload_large_files && @provider.files? && provider_file_references?
702
722
  end
703
723
 
704
- def supports_provider_file_references?
724
+ def provider_file_references?
705
725
  false
706
726
  end
707
727
 
@@ -709,7 +729,8 @@ module RubyLLM
709
729
  return attachment if attachment.provider_file?
710
730
  return attachment unless upload_large_attachment?(attachment)
711
731
 
712
- Attachment.new(provider_upload(attachment), resolution: attachment.resolution, config: @config)
732
+ Attachment.new(provider_upload(attachment), resolution: attachment.resolution,
733
+ processing: attachment.processing, config: @config)
713
734
  end
714
735
 
715
736
  def supported_message_attachment?(_message, attachment)
@@ -9,8 +9,11 @@ module RubyLLM
9
9
  include RubyLLM::Batch::Helpers
10
10
 
11
11
  def create_batch(requests)
12
+ headers = batch_betas(requests)
12
13
  requests = requests.map { |request| { custom_id: request[:custom_id], params: request[:payload] } }
13
- response = @connection.post batches_url, { requests: requests }, idempotent: false
14
+ response = @connection.post(batches_url, { requests: requests }, idempotent: false) do |request|
15
+ request.headers.merge!(headers)
16
+ end
14
17
  parse_batch_response response.body
15
18
  end
16
19
 
@@ -31,6 +34,23 @@ module RubyLLM
31
34
 
32
35
  private
33
36
 
37
+ # One request carries the whole batch, so it takes every beta any
38
+ # of its entries needs.
39
+ def batch_betas(requests)
40
+ requests.reduce({}) do |headers, request|
41
+ payload = request[:payload]
42
+ headers = beta_headers(with_request_betas(headers, request), payload)
43
+ headers = apply_mcp_beta(headers) if block_value(payload, :mcp_servers)
44
+ compaction = block_value(payload, :context_management)
45
+ compaction ? apply_compaction_headers(headers, compaction) : headers
46
+ end
47
+ end
48
+
49
+ def with_request_betas(headers, request)
50
+ betas = canonical_beta_headers(request[:headers].to_h)[Chat::BETA_HEADER]
51
+ betas ? add_beta(headers, betas) : headers
52
+ end
53
+
34
54
  def batches_url
35
55
  'v1/messages/batches'
36
56
  end
@@ -41,18 +61,35 @@ module RubyLLM
41
61
 
42
62
  def parse_batch_response(data)
43
63
  request_counts = data['request_counts']
64
+ completed = data['processing_status'] == 'ended'
44
65
 
45
66
  {
46
67
  id: data['id'],
47
- raw_status: data['processing_status'],
48
- completed: data['processing_status'] == 'ended',
68
+ raw_status: completed ? ended_batch_status(request_counts) : data['processing_status'],
69
+ completed:,
49
70
  request_counts:,
50
71
  request_count: request_counts&.values&.sum
51
72
  }
52
73
  end
53
74
 
75
+ # Anthropic ends every batch as "ended"; only the request counts say
76
+ # whether it was cancelled or ran out of time. A batch with any
77
+ # success ended, and its cancelled or expired slots fail on their own.
78
+ def ended_batch_status(request_counts)
79
+ return 'ended' if request_counts&.fetch('succeeded', 0).to_i.positive?
80
+
81
+ %w[canceled expired].find { |outcome| request_counts&.fetch(outcome, 0).to_i.positive? } || 'ended'
82
+ end
83
+
54
84
  def parse_batch_status(raw_status, completed:)
55
- completed && raw_status == 'ended' ? :succeeded : :pending
85
+ return :pending unless completed
86
+
87
+ case raw_status
88
+ when 'ended' then :succeeded
89
+ when 'canceled' then :cancelled
90
+ when 'expired' then :failed
91
+ else :pending
92
+ end
56
93
  end
57
94
 
58
95
  def parse_batch_result(line)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # These Claude models bind a signed thinking block to the instructions,
7
+ # tools, and messages it was written under, and refuse it once they
8
+ # change. Dropping the block lets the model think that turn again.
9
+ module BlockBinding # :nodoc:
10
+ BETA = 'thinking-binding-controls-2026-08-01'
11
+ MODELS = /claude-(?:(?:opus|sonnet|haiku)-5-5|fable-5-1)/
12
+ DROP_STALE_BLOCKS = { prefix_mismatch_behavior: 'drop_block' }.freeze
13
+
14
+ module_function
15
+
16
+ def binds?(model_id)
17
+ MODELS.match?(model_id.to_s)
18
+ end
19
+
20
+ # Returns +thinking+ with the binding added, or +nil+ when thinking
21
+ # is set to anything but adaptive, which the binding would replace.
22
+ def apply(thinking)
23
+ return unless thinking.nil? || thinking[:type] == 'adaptive'
24
+
25
+ { type: 'adaptive', **thinking.to_h, block_binding: DROP_STALE_BLOCKS }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end