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
@@ -24,10 +24,12 @@ module RubyLLM
24
24
 
25
25
  def finish_reasons = FINISH_REASONS
26
26
 
27
+ def native_tool_namespace?(namespace) = Tools::CLIENT_TOOLSETS.key?(namespace)
28
+
27
29
  def normalize_finish_reason(reason)
28
30
  return nil if reason.nil?
29
31
 
30
- finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
32
+ finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
31
33
  end
32
34
 
33
35
  def completion_url
@@ -64,7 +66,9 @@ module RubyLLM
64
66
  thinking: thinking,
65
67
  citations: citations,
66
68
  caching: caching
67
- ).then { |payload| Tools.apply_tool_search(payload) }.slice(*COUNT_TOKENS_KEYS)
69
+ ).then { |payload| Tools.apply_tool_search(payload) }.slice(*COUNT_TOKENS_KEYS).tap do |payload|
70
+ payload[:thinking]&.delete(:block_binding)
71
+ end
68
72
  end
69
73
 
70
74
  def parse_count_tokens_response(response)
@@ -83,13 +87,11 @@ module RubyLLM
83
87
  end
84
88
 
85
89
  def build_system_content(system_messages, caching: nil)
86
- return [] if system_messages.empty?
87
-
88
90
  # Anthropic's `system` parameter accepts an array of text content blocks
89
91
  # (each optionally with cache_control); each :system message becomes its
90
92
  # own block in the resulting array.
91
93
  system_messages.flat_map do |msg|
92
- blocks = Media.format_content(msg.content, msg.attachments).dup
94
+ blocks = Media.render_content(msg.content, msg.attachments).dup
93
95
  cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) : blocks
94
96
  end
95
97
  end
@@ -98,23 +100,41 @@ module RubyLLM
98
100
  max_output_tokens: nil)
99
101
  payload = {
100
102
  model: model.id,
101
- messages: format_messages(chat_messages, thinking:, citations:, caching:),
103
+ messages: render_messages(chat_messages, thinking:, citations:, caching:),
102
104
  stream: stream,
103
105
  max_tokens: max_output_tokens || model.max_output_tokens || DEFAULT_MAX_OUTPUT_TOKENS
104
106
  }
105
107
 
106
108
  add_thinking_fields(payload, thinking, model)
109
+ bind_thinking(payload, model)
107
110
 
108
111
  payload
109
112
  end
110
113
 
111
- def format_messages(messages, thinking: nil, citations: false, caching: nil)
114
+ # The binding goes on every request, not only once thinking appears
115
+ # in the history: a thinking setting that changes mid-conversation
116
+ # invalidates the prompt cache.
117
+ def bind_thinking(payload, model)
118
+ return unless thinking_block_binding? && BlockBinding.binds?(model.id)
119
+
120
+ bound = BlockBinding.apply(payload[:thinking])
121
+ payload[:thinking] = bound if bound
122
+ end
123
+
124
+ # Hosts that don't take the binding's beta override this.
125
+ def thinking_block_binding?
126
+ true
127
+ end
128
+
129
+ def render_messages(messages, thinking: nil, citations: false, caching: nil)
112
130
  rendered = []
113
131
  tool_result_blocks = []
132
+ calls = {}
114
133
 
115
134
  messages.each do |msg|
135
+ calls.merge!(msg.tool_calls.to_h)
116
136
  if msg.tool_result?
117
- tool_result_blocks << Tools.format_tool_result_block(msg)
137
+ tool_result_blocks << Tools.render_tool_result_block(msg, citations:, tool_call: calls[msg.tool_call_id])
118
138
  inject_cache_control(tool_result_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
119
139
  next
120
140
  end
@@ -124,7 +144,7 @@ module RubyLLM
124
144
  tool_result_blocks = []
125
145
  end
126
146
 
127
- formatted = format_message(msg, thinking:, citations:, caching:)
147
+ formatted = render_message(msg, thinking:, citations:, caching:)
128
148
  rendered << formatted unless formatted[:content].empty?
129
149
  end
130
150
 
@@ -134,8 +154,9 @@ module RubyLLM
134
154
 
135
155
  def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil)
136
156
  if tools.any?
137
- payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
157
+ payload[:tools] = Tools.render_tools(tools)
138
158
  unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
159
+ Protocol::ToolNamespaces.validate_tool_choice(tools, tool_prefs[:choice])
139
160
  payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
140
161
  end
141
162
  end
@@ -165,7 +186,13 @@ module RubyLLM
165
186
  end
166
187
 
167
188
  def apply_compaction_headers(headers, _compaction)
168
- headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], COMPACTION_BETA))
189
+ add_beta(headers, COMPACTION_BETA)
190
+ end
191
+
192
+ def apply_binding_beta(headers, payload)
193
+ return headers unless payload[:thinking]&.key?(:block_binding)
194
+
195
+ add_beta(headers, BlockBinding::BETA)
169
196
  end
170
197
 
171
198
  # The Files API is still a beta, so a request that references an
@@ -173,22 +200,56 @@ module RubyLLM
173
200
  def apply_files_beta(headers, payload)
174
201
  return headers unless provider_file_source?(payload)
175
202
 
176
- headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], Files::BETA_HEADER))
203
+ add_beta(headers, Files::BETA_HEADER)
204
+ end
205
+
206
+ def apply_mcp_beta(headers)
207
+ add_beta(headers, Anthropic::MCP_BETA)
177
208
  end
178
209
 
179
210
  def provider_file_source?(payload)
180
- blocks = Array(payload[:messages]).flat_map { |message| Array(message[:content]) }
211
+ blocks = Array(payload[:messages]).flat_map { |message| Array(block_value(message, :content)) }
181
212
  blocks.concat(Array(payload[:system]))
182
- blocks.any? { |block| block.is_a?(Hash) && block[:source].is_a?(Hash) && block[:source][:type] == 'file' }
213
+ file_source?(blocks)
214
+ end
215
+
216
+ def file_source?(blocks)
217
+ blocks.any? do |block|
218
+ next false unless block.is_a?(Hash)
219
+
220
+ source = block_value(block, :source)
221
+ (source.is_a?(Hash) && block_value(source, :type) == 'file') ||
222
+ file_source?(Array(block_value(block, :content)))
223
+ end
224
+ end
225
+
226
+ # A before_request hook can add blocks parsed from JSON.
227
+ def block_value(block, key)
228
+ block.fetch(key) { block[key.to_s] }
183
229
  end
184
230
 
185
231
  # Anthropic takes several betas as one comma-separated header, so an
186
232
  # added beta joins whatever a server tool or with_headers already set.
187
233
  def join_betas(existing, beta)
188
- (existing.to_s.split(',').map(&:strip).reject(&:empty?) + [beta]).uniq.join(',')
234
+ [existing, beta].flat_map { |betas| betas.to_s.split(',') }.map(&:strip).reject(&:empty?).uniq.join(',')
235
+ end
236
+
237
+ def add_beta(headers, beta)
238
+ headers = canonical_beta_headers(headers)
239
+ headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], beta))
189
240
  end
190
241
 
191
- def supports_provider_file_references?
242
+ # HTTP header names ignore case but Hash keys do not, so every
243
+ # spelling a caller used folds into BETA_HEADER before a beta joins it.
244
+ def canonical_beta_headers(headers)
245
+ names = headers.keys.select { |name| name.to_s.casecmp?(BETA_HEADER) }
246
+ return headers if names.empty?
247
+
248
+ betas = headers.values_at(*names).reduce { |joined, beta| join_betas(joined, beta) }
249
+ headers.except(*names).merge(BETA_HEADER => betas)
250
+ end
251
+
252
+ def provider_file_references?
192
253
  true
193
254
  end
194
255
 
@@ -210,11 +271,11 @@ module RubyLLM
210
271
  def parse_completion_body(data, raw:)
211
272
  content_blocks = data['content'] || []
212
273
 
213
- text_content, citations = extract_text_and_citations(content_blocks)
214
- thinking_content = extract_thinking_content(content_blocks)
215
- thinking_signature = extract_thinking_signature(content_blocks)
274
+ text_content, citations = parse_text_and_citations(content_blocks)
275
+ thinking_content = parse_thinking_content(content_blocks)
276
+ thinking_signature = parse_thinking_signature(content_blocks)
216
277
  tool_use_blocks = Tools.find_tool_uses(content_blocks)
217
- server_tool_calls = extract_server_tool_calls(content_blocks)
278
+ server_tool_calls = parse_server_tool_calls(content_blocks)
218
279
 
219
280
  build_message(data, content: text_content, citations:, thinking: thinking_content,
220
281
  thinking_signature:, tool_use_blocks:, server_tool_calls:,
@@ -230,7 +291,7 @@ module RubyLLM
230
291
  type.end_with?('_tool_result')
231
292
  end
232
293
 
233
- def extract_server_tool_calls(blocks)
294
+ def parse_server_tool_calls(blocks)
234
295
  blocks.select { |block| server_tool_block?(block) }.map do |block|
235
296
  ServerToolCall.new(
236
297
  type: block['type'],
@@ -243,7 +304,15 @@ module RubyLLM
243
304
  end
244
305
  end
245
306
 
246
- def extract_text_and_citations(blocks)
307
+ # A failed search holds one error object instead of a list of results.
308
+ def parse_web_sources(message)
309
+ blocks = message.server_tool_calls.select { |call| call.type == 'web_search_tool_result' }
310
+ blocks.flat_map { |call| call.result.is_a?(Array) ? call.result : [] }.filter_map do |result|
311
+ WebSource.new(url: result['url'], title: result['title']) if result['type'] == 'web_search_result'
312
+ end
313
+ end
314
+
315
+ def parse_text_and_citations(blocks)
247
316
  text = +''
248
317
  citations = []
249
318
 
@@ -272,6 +341,7 @@ module RubyLLM
272
341
  text: text,
273
342
  start_index: start_index,
274
343
  end_index: end_index,
344
+ source_id: data['source'],
275
345
  source_index: data['document_index'] || data['search_result_index'],
276
346
  start_page: data['start_page_number'],
277
347
  end_page: end_page && (end_page - 1)
@@ -286,14 +356,14 @@ module RubyLLM
286
356
 
287
357
  # An empty thinking text is a real block whose display was omitted;
288
358
  # collapsing it to nil would replay the signature as redacted data.
289
- def extract_thinking_content(blocks)
359
+ def parse_thinking_content(blocks)
290
360
  thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
291
361
  return nil if thinking_blocks.empty?
292
362
 
293
363
  thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
294
364
  end
295
365
 
296
- def extract_thinking_signature(blocks)
366
+ def parse_thinking_signature(blocks)
297
367
  thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
298
368
  blocks.find { |c| c['type'] == 'redacted_thinking' }
299
369
  thinking_block&.dig('signature') || thinking_block&.dig('data')
@@ -307,10 +377,6 @@ module RubyLLM
307
377
  def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
308
378
  server_tool_calls: [], raw_content: nil)
309
379
  usage = aggregate_usage(data['usage'])
310
- thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
311
- usage.dig('output_tokens_details', 'reasoning_tokens') ||
312
- usage['thinking_tokens'] ||
313
- usage['reasoning_tokens']
314
380
 
315
381
  Message.new(
316
382
  role: :assistant,
@@ -323,29 +389,33 @@ module RubyLLM
323
389
  raw_content: raw_content,
324
390
  input_tokens: usage['input_tokens'],
325
391
  output_tokens: usage['output_tokens'],
326
- cache_read_tokens: extract_cache_read_tokens(data),
327
- cache_write_tokens: extract_cache_write_tokens(data),
328
- cache_write_tokens_by_ttl: extract_cache_write_by_ttl(data),
329
- thinking_tokens: thinking_tokens,
392
+ cache_read_tokens: parse_cache_read_tokens(data),
393
+ cache_write_tokens: parse_cache_write_tokens(data),
394
+ cache_write_tokens_by_ttl: parse_cache_write_by_ttl(data),
395
+ thinking_tokens: usage_thinking_tokens(usage),
330
396
  server_tool_use: usage['server_tool_use'],
331
397
  finish_reason: normalize_finish_reason(data['stop_reason']),
332
- model: data['model'],
398
+ model: parse_response_model_id(data),
333
399
  raw: raw
334
400
  )
335
401
  end
336
402
 
403
+ def parse_response_model_id(data)
404
+ data['model']
405
+ end
406
+
337
407
  # Stored thinking blocks replay whether or not this request asks for
338
408
  # thinking: Claude emits them on its own and requires them back on
339
409
  # tool-use turns.
340
- def format_message(msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument
410
+ def render_message(msg, citations: false, caching: nil, **)
341
411
  if msg.role == :assistant && msg.raw_content
342
- format_raw_assistant_message(msg, caching:)
412
+ render_raw_assistant_message(msg, caching:)
343
413
  elsif msg.tool_call?
344
- format_tool_call_with_thinking(msg, caching:)
414
+ render_tool_call_with_thinking(msg, caching:)
345
415
  elsif msg.tool_result?
346
- Tools.format_tool_result(msg)
416
+ Tools.render_tool_result(msg, citations:)
347
417
  else
348
- format_basic_message_with_thinking(msg, citations: citations, caching:)
418
+ render_basic_message_with_thinking(msg, citations: citations, caching:)
349
419
  end
350
420
  end
351
421
 
@@ -360,15 +430,15 @@ module RubyLLM
360
430
  # Turns that used server tools replay their provider-shaped blocks
361
431
  # verbatim: the API requires the tool_use/result blocks and their
362
432
  # citations back exactly as returned.
363
- def format_raw_assistant_message(msg, caching: nil)
364
- blocks = msg.raw_content.dup
433
+ def render_raw_assistant_message(msg, caching: nil)
434
+ blocks = Protocol::ReplayedArguments.blank(msg.raw_content.dup, msg, id: 'id', arguments: 'input', empty: {})
365
435
  inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
366
436
 
367
437
  { role: 'assistant', content: blocks }
368
438
  end
369
439
 
370
- def format_basic_message_with_thinking(msg, citations: false, caching: nil)
371
- content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
440
+ def render_basic_message_with_thinking(msg, citations: false, caching: nil)
441
+ content_blocks = msg.role == :assistant ? render_thinking_blocks(msg) : []
372
442
 
373
443
  append_formatted_content(content_blocks, msg, citations: citations)
374
444
  inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
@@ -379,17 +449,18 @@ module RubyLLM
379
449
  }
380
450
  end
381
451
 
382
- def format_tool_call_with_thinking(msg, caching: nil)
452
+ def render_tool_call_with_thinking(msg, caching: nil)
383
453
  content_blocks = prepend_thinking_blocks([], msg)
384
454
  append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
385
455
 
386
- msg.tool_calls.each_value do |tool_call|
456
+ msg.tool_calls.each do |tool_call|
387
457
  content_blocks << {
388
458
  type: 'tool_use',
389
- id: tool_call.id,
459
+ id: tool_call.provider_call_id,
390
460
  name: tool_call.name,
391
- input: tool_call.arguments
392
- }
461
+ input: tool_call.arguments,
462
+ toolset_name: tool_call.namespace
463
+ }.compact
393
464
  end
394
465
  inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
395
466
 
@@ -400,12 +471,12 @@ module RubyLLM
400
471
  end
401
472
 
402
473
  def prepend_thinking_blocks(content_blocks, msg)
403
- content_blocks.unshift(*format_thinking_blocks(msg))
474
+ content_blocks.unshift(*render_thinking_blocks(msg))
404
475
 
405
476
  content_blocks
406
477
  end
407
478
 
408
- def format_thinking_blocks(msg)
479
+ def render_thinking_blocks(msg)
409
480
  blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
410
481
  return Support::Utils.deep_dup(blocks) if blocks
411
482
 
@@ -422,7 +493,7 @@ module RubyLLM
422
493
  end
423
494
 
424
495
  def append_formatted_content(content_blocks, msg, citations: false)
425
- content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
496
+ content_blocks.concat(Media.render_content(msg.content, msg.attachments, citations: citations))
426
497
  end
427
498
 
428
499
  def cache_boundary?(message, caching: nil)
@@ -430,11 +501,9 @@ module RubyLLM
430
501
  end
431
502
 
432
503
  def inject_cache_control(blocks, caching: nil, ttl: nil)
433
- return blocks if blocks.empty?
434
-
435
504
  last = blocks.last
436
- return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
437
505
  return blocks unless last.is_a?(Hash)
506
+ return blocks if last[:cache_control] || last['cache_control']
438
507
 
439
508
  blocks[-1] = last.merge(cache_control: prompt_cache_control(caching, ttl:))
440
509
  blocks
@@ -445,13 +514,11 @@ module RubyLLM
445
514
  ttl ||= options[:ttl]
446
515
 
447
516
  { type: CACHE_CONTROL_TYPE }.tap do |control|
448
- control[:ttl] = ttl if ttl
517
+ control[:ttl] = Protocol::CacheLifetime.render(ttl) if ttl
449
518
  end
450
519
  end
451
520
 
452
521
  def prompt_cache_options(caching)
453
- return {} unless caching
454
-
455
522
  options = caching.to_h.transform_keys(&:to_sym)
456
523
  unsupported = options.keys - PROMPT_CACHE_OPTIONS
457
524
  return options if unsupported.empty?
@@ -460,6 +527,8 @@ module RubyLLM
460
527
  "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
461
528
  end
462
529
 
530
+ def cache_option_names = PROMPT_CACHE_OPTIONS
531
+
463
532
  def format_cache_option_keys(keys)
464
533
  keys.map { |key| ":#{key}" }.join(', ')
465
534
  end
@@ -499,9 +568,10 @@ module RubyLLM
499
568
  # Effort alone never turns thinking on; Claude needs a thinking block.
500
569
  # Generations that take a budget get one sized from the effort with
501
570
  # the levels Bedrock publishes. Generations without a budget option
502
- # think adaptively.
571
+ # think adaptively, and so does a bare switch, except on generations
572
+ # that only take a budget, which get the smallest one.
503
573
  def thinking_mode(thinking, model, effort, max_tokens)
504
- return { type: 'adaptive' } if thinking.enabled == true
574
+ return toggle_mode(model) if thinking.enabled == true
505
575
 
506
576
  budget = thinking.budget || effort_budget(effort, model, max_tokens)
507
577
  mode = if budget
@@ -515,6 +585,13 @@ module RubyLLM
515
585
 
516
586
  EFFORT_BUDGETS = { 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze
517
587
 
588
+ def toggle_mode(model)
589
+ budget = model.reasoning_option(:budget_tokens)
590
+ return { type: 'adaptive' } unless budget && !model.reasoning_option(:effort)
591
+
592
+ { type: 'enabled', budget_tokens: [budget[:min].to_i, EFFORT_BUDGETS['low']].max }
593
+ end
594
+
518
595
  def adaptive_thinking?(thinking, effort, model)
519
596
  return true if thinking.display
520
597
  return false unless effort
@@ -527,8 +604,6 @@ module RubyLLM
527
604
 
528
605
  budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
529
606
  minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
530
- return [budget, minimum].max unless max_tokens
531
-
532
607
  budget.clamp(minimum, [max_tokens - 1, minimum].max)
533
608
  end
534
609
 
@@ -7,12 +7,20 @@ module RubyLLM
7
7
  class Files < Protocols::Files
8
8
  BETA_HEADER = 'files-api-2025-04-14'
9
9
 
10
+ def rejected_options(operation)
11
+ operation == :upload ? %i[purpose uri] : super
12
+ end
13
+
10
14
  private
11
15
 
12
16
  def files_url
13
17
  'v1/files'
14
18
  end
15
19
 
20
+ def render_upload_fields(_attachment, expires_in: nil, **)
21
+ { expires_in_seconds: expires_in }
22
+ end
23
+
16
24
  def upload_headers(request)
17
25
  file_headers(request)
18
26
  end
@@ -28,6 +36,7 @@ module RubyLLM
28
36
  filename: data['filename'],
29
37
  byte_size: data['size_bytes'],
30
38
  created_at: timestamp(data['created_at']),
39
+ expires_at: timestamp(data['expires_at']),
31
40
  mime_type: data['mime_type'],
32
41
  downloadable: data['downloadable']
33
42
  )
@@ -11,39 +11,39 @@ module RubyLLM
11
11
  attachment.provider_file? || %i[image pdf text].include?(attachment.type)
12
12
  end
13
13
 
14
- def format_content(content, attachments = [], citations: false)
14
+ def render_content(content, attachments = [], citations: false)
15
15
  parts = []
16
- parts << format_text(content) unless content.nil? || content.empty?
16
+ parts << render_text(content) unless content.nil? || content.empty?
17
17
 
18
18
  attachments.each do |attachment|
19
19
  raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
20
20
 
21
21
  if attachment.provider_file?
22
- parts << format_provider_file(attachment, citations: citations)
22
+ parts << render_provider_file(attachment, citations: citations)
23
23
  next
24
24
  end
25
25
 
26
26
  case attachment.type
27
27
  when :image
28
- parts << format_image(attachment)
28
+ parts << render_image(attachment)
29
29
  when :pdf
30
- parts << format_pdf(attachment, citations: citations)
30
+ parts << render_pdf(attachment, citations: citations)
31
31
  when :text
32
- parts << (citations ? format_text_document(attachment) : format_text_file(attachment))
32
+ parts << (citations ? render_text_document(attachment) : render_text_file(attachment))
33
33
  end
34
34
  end
35
35
 
36
36
  parts
37
37
  end
38
38
 
39
- def format_text(text)
39
+ def render_text(text)
40
40
  {
41
41
  type: 'text',
42
42
  text: text
43
43
  }
44
44
  end
45
45
 
46
- def format_image(image)
46
+ def render_image(image)
47
47
  if image.url?
48
48
  {
49
49
  type: 'image',
@@ -64,8 +64,8 @@ module RubyLLM
64
64
  end
65
65
  end
66
66
 
67
- def format_provider_file(file, citations: false)
68
- return format_provider_image(file) if file.image?
67
+ def render_provider_file(file, citations: false)
68
+ return render_provider_image(file) if file.image?
69
69
 
70
70
  document = {
71
71
  type: 'document',
@@ -78,7 +78,7 @@ module RubyLLM
78
78
  document
79
79
  end
80
80
 
81
- def format_provider_image(image)
81
+ def render_provider_image(image)
82
82
  {
83
83
  type: 'image',
84
84
  source: {
@@ -88,7 +88,7 @@ module RubyLLM
88
88
  }
89
89
  end
90
90
 
91
- def format_pdf(pdf, citations: false)
91
+ def render_pdf(pdf, citations: false)
92
92
  document = {
93
93
  type: 'document',
94
94
  source: pdf_source(pdf)
@@ -113,7 +113,7 @@ module RubyLLM
113
113
  end
114
114
  end
115
115
 
116
- def format_text_file(text_file)
116
+ def render_text_file(text_file)
117
117
  {
118
118
  type: 'text',
119
119
  text: text_file.for_llm
@@ -121,7 +121,7 @@ module RubyLLM
121
121
  end
122
122
 
123
123
  # Text attachments become citable documents when citations are enabled.
124
- def format_text_document(text_file)
124
+ def render_text_document(text_file)
125
125
  document = {
126
126
  type: 'document',
127
127
  source: {
@@ -70,28 +70,34 @@ module RubyLLM
70
70
  end
71
71
  end
72
72
 
73
- def extract_model_id(data)
73
+ def parse_model_id(data)
74
74
  data.dig('message', 'model')
75
75
  end
76
76
 
77
- def extract_input_tokens(data)
78
- message_usage(data)['input_tokens']
77
+ def parse_input_tokens(data)
78
+ message_usage(data)['input_tokens'] || delta_usage(data)['input_tokens']
79
79
  end
80
80
 
81
- def extract_output_tokens(data)
81
+ def parse_output_tokens(data)
82
82
  message_usage(data)['output_tokens'] || delta_usage(data)['output_tokens']
83
83
  end
84
84
 
85
- def extract_thinking_tokens(data)
86
- message_usage(data).dig('output_tokens_details', 'thinking_tokens') ||
87
- delta_usage(data).dig('output_tokens_details', 'thinking_tokens')
85
+ def parse_thinking_tokens(data)
86
+ usage_thinking_tokens(message_usage(data)) || usage_thinking_tokens(delta_usage(data))
88
87
  end
89
88
 
90
- def extract_cache_read_tokens(data)
89
+ def usage_thinking_tokens(usage)
90
+ usage.dig('output_tokens_details', 'thinking_tokens') ||
91
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
92
+ usage['thinking_tokens'] ||
93
+ usage['reasoning_tokens']
94
+ end
95
+
96
+ def parse_cache_read_tokens(data)
91
97
  message_usage(data)['cache_read_input_tokens'] || delta_usage(data)['cache_read_input_tokens']
92
98
  end
93
99
 
94
- def extract_cache_write_tokens(data)
100
+ def parse_cache_write_tokens(data)
95
101
  direct = message_usage(data)['cache_creation_input_tokens'] ||
96
102
  delta_usage(data)['cache_creation_input_tokens']
97
103
  return direct if direct
@@ -102,7 +108,7 @@ module RubyLLM
102
108
  breakdown.values.compact.sum
103
109
  end
104
110
 
105
- def extract_cache_write_by_ttl(data)
111
+ def parse_cache_write_by_ttl(data)
106
112
  breakdown = message_usage(data)['cache_creation'] || delta_usage(data)['cache_creation']
107
113
  return unless breakdown.is_a?(Hash)
108
114