ruby_llm 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -7,6 +7,10 @@ module RubyLLM
7
7
  module Transcription
8
8
  DEFAULT_LANGUAGE = 'en'
9
9
 
10
+ def rejected_options(operation)
11
+ operation == :transcribe ? %i[format prompt speaker_names speaker_references] : super
12
+ end
13
+
10
14
  module_function
11
15
 
12
16
  def transcription_url
@@ -16,10 +20,7 @@ module RubyLLM
16
20
  # Cohere parses the multipart body as it streams, so every form field
17
21
  # has to be written before the file part or the request is rejected as
18
22
  # missing them. The file goes in last, after provider options.
19
- # rubocop:disable-next Lint/UnusedMethodArgument
20
- def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
21
- speaker_references: nil, provider_options: {}, prompt: nil,
22
- temperature: nil)
23
+ def render_transcription_payload(file_part, model:, language:, provider_options: {}, temperature: nil, **)
23
24
  {
24
25
  model: model,
25
26
  language: language || DEFAULT_LANGUAGE,
@@ -13,12 +13,6 @@ module RubyLLM
13
13
  'Converse'
14
14
  end
15
15
 
16
- def bedrock_batch_jsonl(requests)
17
- requests.map do |request|
18
- JSON.generate(recordId: request[:custom_id], modelInput: batch_payload(request))
19
- end.join("\n")
20
- end
21
-
22
16
  def validate_bedrock_batch_requests!(requests)
23
17
  unsupported = requests.any? do |request|
24
18
  payload = request.fetch(:payload)
@@ -30,24 +24,8 @@ module RubyLLM
30
24
  raise Error, 'bedrock batch requests do not support tools or structured output'
31
25
  end
32
26
 
33
- def parse_bedrock_outputs(outputs, **)
34
- outputs.flat_map { |body| parse_bedrock_output(body) }
35
- end
36
-
37
- def parse_bedrock_output(body)
38
- body.to_s.each_line.filter_map { |line| parse_bedrock_record(JSON.parse(line)) }
39
- end
40
-
41
- def parse_bedrock_record(line)
42
- record_id = line['recordId'] || line['record_id']
43
- index = batch_result_index(record_id)
44
-
45
- if line['modelOutput']
46
- body = line['modelOutput']
47
- [index, parse_completion_body(body, raw: body)]
48
- else
49
- [index, nil, batch_failure(record_id, line.dig('error', 'message') || line['errorMessage'])]
50
- end
27
+ def parse_bedrock_model_output(body, model:)
28
+ parse_completion_body({ 'modelId' => model }.merge(body), raw: body)
51
29
  end
52
30
  end
53
31
  end
@@ -15,6 +15,22 @@ module RubyLLM
15
15
 
16
16
  BEDROCK_INLINE_DOCUMENT_LIMIT = 4_500_000
17
17
  PROMPT_CACHE_OPTIONS = %i[ttl].freeze
18
+ # Bedrock accepts at most four cache points in one request, and
19
+ # Converse takes them only for Claude and Nova models; Nova takes none
20
+ # in toolConfig.tools. See "Supported models, Regions, and explicit
21
+ # caching limits" in
22
+ # https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
23
+ MAX_CACHE_POINTS = 4
24
+ CACHE_POINT_MODELS = %w[anthropic. amazon.nova].freeze
25
+ TOOL_CACHE_POINT_MODELS = %w[anthropic.].freeze
26
+ # Converse refuses searchResult blocks from every model but Claude, so an
27
+ # application inference profile, which doesn't name its model, gets text.
28
+ SEARCH_RESULT_MODELS = %w[anthropic.].freeze
29
+ # Bedrock forwards an S3 source to other models untranslated, and
30
+ # Claude rejects it.
31
+ S3_SOURCE_MODELS = %w[amazon.nova].freeze
32
+ OFFICE_DOCUMENT_FORMATS = %w[doc docx xls xlsx].freeze
33
+ CACHE_LIFETIME_UNITS = { 's' => 1, 'm' => 60, 'h' => 3600 }.freeze
18
34
  COUNT_TOKENS_KEYS = %i[messages system toolConfig additionalModelRequestFields].freeze
19
35
 
20
36
  module_function
@@ -24,7 +40,7 @@ module RubyLLM
24
40
  def normalize_finish_reason(reason)
25
41
  return nil if reason.nil?
26
42
 
27
- finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
43
+ finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
28
44
  end
29
45
 
30
46
  def completion_url
@@ -41,28 +57,31 @@ module RubyLLM
41
57
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
42
58
  schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
43
59
  tool_prefs ||= {}
60
+ max_output_tokens ||= model.max_output_tokens
44
61
  @used_document_names = {}
45
62
  system_messages, chat_messages = messages.partition { |msg| msg.role == :system }
46
63
  prompt_cache_options(caching)
47
- automatic_cache_target = automatic_cache_target(system_messages, chat_messages, caching)
48
64
  payload = {
49
- messages: format_messages(chat_messages, caching:, automatic_cache_target:, citations:)
65
+ messages: render_messages(chat_messages, caching:, citations:, search_results: search_results?(model),
66
+ tool_text: tools.empty?)
50
67
  }
51
68
 
52
- system_blocks = format_system(system_messages, caching:, automatic_cache_target:)
69
+ system_blocks = render_system(system_messages, caching:)
53
70
  payload[:system] = system_blocks unless system_blocks.empty?
54
71
 
55
- payload[:inferenceConfig] = format_inference_config(model, temperature, max_output_tokens)
72
+ payload[:inferenceConfig] = render_inference_config(model, temperature, max_output_tokens)
56
73
 
57
- tool_config = format_tool_config(tools, tool_prefs)
74
+ tool_config = render_tool_config(tools, tool_prefs)
58
75
  payload[:toolConfig] = tool_config if tool_config
59
76
 
60
- additional_fields = format_additional_model_request_fields(thinking, model, max_output_tokens)
77
+ additional_fields = render_additional_model_request_fields(thinking, model, max_output_tokens)
78
+ additional_fields = bind_thinking(additional_fields, model)
61
79
  payload[:additionalModelRequestFields] = additional_fields if additional_fields
62
80
 
63
81
  output_config = build_output_config(schema)
64
82
  payload[:outputConfig] = output_config if output_config
65
83
 
84
+ add_automatic_cache_points(payload, model, caching)
66
85
  payload
67
86
  end
68
87
 
@@ -91,8 +110,8 @@ module RubyLLM
91
110
  response.body['inputTokens']
92
111
  end
93
112
 
94
- def supports_provider_file_references?
95
- true
113
+ def provider_file_references?
114
+ S3_SOURCE_MODELS.any? { |prefix| foundation_model_id(@model&.id).start_with?(prefix) }
96
115
  end
97
116
 
98
117
  def default_large_file_upload_threshold
@@ -108,7 +127,7 @@ module RubyLLM
108
127
  content_blocks = data.dig('output', 'message', 'content') || []
109
128
  usage = data['usage'] || {}
110
129
  thinking_text, thinking_signature = parse_thinking(content_blocks)
111
- text_content, citations = extract_text_and_citations(content_blocks)
130
+ text_content, citations = parse_text_and_citations(content_blocks)
112
131
 
113
132
  Message.new(
114
133
  role: :assistant,
@@ -117,7 +136,7 @@ module RubyLLM
117
136
  thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
118
137
  raw_reasoning: parse_thinking_blocks(content_blocks),
119
138
  tool_calls: parse_tool_calls(content_blocks),
120
- server_tool_calls: extract_server_tool_calls(content_blocks),
139
+ server_tool_calls: parse_server_tool_calls(content_blocks),
121
140
  input_tokens: input_tokens(usage),
122
141
  output_tokens: usage['outputTokens'],
123
142
  cache_read_tokens: usage['cacheReadInputTokens'],
@@ -151,22 +170,31 @@ module RubyLLM
151
170
  end
152
171
  end
153
172
 
154
- def format_messages(messages, caching: nil, automatic_cache_target: nil, citations: false)
173
+ # Converse refuses toolUse and toolResult blocks in a request without
174
+ # a toolConfig, and has no tool choice of none, so a request that
175
+ # declares no tools, such as a summary of a chat that called them,
176
+ # gets its tool rounds as text (+tool_text+).
177
+ def render_messages(messages, caching: nil, citations: false, search_results: false, tool_text: false)
155
178
  rendered = []
156
179
  tool_result_blocks = []
180
+ calls = {}
157
181
 
158
182
  messages.each do |msg|
183
+ calls.merge!(msg.tool_calls.to_h)
159
184
  if msg.tool_result?
160
- tool_result_blocks << format_tool_result_block(msg)
185
+ call = calls[msg.tool_call_id] if tool_text
186
+ tool_result_blocks.concat(render_tool_result_blocks(msg, caching:, citations:, search_results:,
187
+ tool_text:, call:))
161
188
  next
162
189
  end
163
190
 
191
+ message = render_non_tool_message(msg, caching:, citations:, tool_text:)
192
+ next tool_result_blocks.concat(message[:content]) if joins_tool_results?(message, tool_result_blocks)
193
+
164
194
  unless tool_result_blocks.empty?
165
195
  rendered << { role: 'user', content: tool_result_blocks }
166
196
  tool_result_blocks = []
167
197
  end
168
-
169
- message = format_non_tool_message(msg, caching:, automatic_cache_target:, citations:)
170
198
  rendered << message if message
171
199
  end
172
200
 
@@ -174,42 +202,50 @@ module RubyLLM
174
202
  rendered
175
203
  end
176
204
 
177
- def format_non_tool_message(msg, caching: nil, automatic_cache_target: nil, citations: false)
178
- content = format_message_content(msg, caching:, automatic_cache_target:, citations:)
205
+ def joins_tool_results?(message, tool_result_blocks)
206
+ message && message[:role] == 'user' && !tool_result_blocks.empty?
207
+ end
208
+
209
+ def render_non_tool_message(msg, caching: nil, citations: false, tool_text: false)
210
+ content = render_message_content(msg, caching:, citations:, tool_text:)
179
211
  return nil if content.empty?
180
212
 
181
213
  {
182
- role: format_role(msg.role),
214
+ role: render_role(msg.role),
183
215
  content: content
184
216
  }
185
217
  end
186
218
 
187
- def format_message_content(msg, caching: nil, automatic_cache_target: nil, citations: false)
188
- blocks = format_structured_message_content(msg, citations:)
219
+ def render_message_content(msg, caching: nil, citations: false, tool_text: false)
220
+ blocks = render_structured_message_content(msg, citations:)
189
221
 
190
222
  if msg.tool_call?
191
- msg.tool_calls.each_value do |tool_call|
223
+ msg.tool_calls.each do |tool_call|
224
+ next blocks << tool_call_text_block(tool_call) if tool_text
225
+
192
226
  blocks << {
193
227
  toolUse: {
194
- toolUseId: tool_call.id,
228
+ toolUseId: tool_call.provider_call_id,
195
229
  name: tool_call.name,
196
230
  input: tool_call.arguments
197
231
  }
198
232
  }
199
233
  end
200
234
  end
201
- if cache_boundary?(msg, automatic_cache_target, caching:)
202
- blocks << converse_cache_block_for(caching, ttl: msg.cache_ttl)
203
- end
235
+ blocks << converse_cache_block_for(caching, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
204
236
 
205
237
  blocks
206
238
  end
207
239
 
208
- def format_structured_message_content(msg, citations: false)
209
- blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
240
+ def tool_call_text_block(tool_call)
241
+ { text: "Called the #{tool_call.name} tool with #{tool_call.arguments.to_json}" }
242
+ end
243
+
244
+ def render_structured_message_content(msg, citations: false)
245
+ blocks = msg.role == :assistant ? render_thinking_blocks(msg) : []
210
246
 
211
247
  blocks.concat(
212
- Media.format_content(msg.content, msg.attachments, used_document_names: @used_document_names, citations:)
248
+ Media.render_content(msg.content, msg.attachments, used_document_names: @used_document_names, citations:)
213
249
  )
214
250
 
215
251
  blocks
@@ -218,35 +254,55 @@ module RubyLLM
218
254
  # Reasoning rebuilt from thinking text goes back only to the provider
219
255
  # that produced it: Claude refuses another provider's signature, and
220
256
  # a block without one.
221
- def format_thinking_blocks(msg)
257
+ def render_thinking_blocks(msg)
222
258
  blocks = msg.raw_reasoning['converse'] if msg.raw_reasoning.is_a?(Hash)
223
259
  return Support::Utils.deep_dup(blocks) if blocks
224
260
  return [] unless msg.thinking && producer_slug(msg) == @provider.slug
225
261
 
226
- [format_thinking_block(msg.thinking)].compact
262
+ [render_thinking_block(msg.thinking)].compact
263
+ end
264
+
265
+ def render_tool_result_blocks(msg, caching: nil, citations: false, search_results: false, tool_text: false,
266
+ call: nil)
267
+ blocks = if tool_text
268
+ render_tool_result_text(msg, call, citations:)
269
+ else
270
+ [render_tool_result_block(msg, citations:, search_results:)]
271
+ end
272
+ blocks << converse_cache_block_for(caching, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
273
+ blocks
227
274
  end
228
275
 
229
- def format_tool_result_block(msg)
276
+ def render_tool_result_block(msg, citations: false, search_results: false)
230
277
  {
231
278
  toolResult: {
232
- toolUseId: msg.tool_call_id,
233
- content: format_tool_result_content(msg)
279
+ toolUseId: msg.provider_call_id,
280
+ content: render_tool_result_content(msg, citations:, search_results:)
234
281
  }
235
282
  }
236
283
  end
237
284
 
238
- def format_tool_result_content(msg)
239
- search_results = RubyLLM::SearchResults.from_content(msg.content)
240
- return search_results.results.map { |result| search_result_block(result) } if search_results
285
+ def render_tool_result_text(msg, call, citations: false)
286
+ label = call ? "The #{call.name} tool returned:" : 'The tool returned:'
287
+ first, *rest = render_tool_result_content(msg, citations:)
288
+ return [{ text: "#{label}\n#{first[:text]}" }, *rest] if first.key?(:text)
241
289
 
242
- blocks = Media.format_content(msg.content, msg.attachments, used_document_names: @used_document_names)
290
+ [{ text: label }, first, *rest]
291
+ end
292
+
293
+ def render_tool_result_content(msg, citations: false, search_results: false)
294
+ results = RubyLLM::SearchResults.from_content(msg.content) if search_results
295
+ return results.results.map { |result| search_result_block(result) } if results
296
+
297
+ content = msg.content unless msg.content&.strip&.empty?
298
+ blocks = Media.render_content(content, msg.attachments, used_document_names: @used_document_names, citations:)
243
299
  blocks.empty? ? [text_tool_result_block(nil)] : blocks
244
300
  end
245
301
 
246
302
  def search_result_block(result)
247
303
  {
248
304
  searchResult: {
249
- source: result[:url] || result[:title],
305
+ source: result[:source_id] || result[:url] || result[:title],
250
306
  title: result[:title],
251
307
  content: [{ text: result[:text] }],
252
308
  citations: { enabled: true }
@@ -260,43 +316,114 @@ module RubyLLM
260
316
  { text: text }
261
317
  end
262
318
 
263
- def format_role(role)
319
+ def render_role(role)
264
320
  case role
265
321
  when :assistant then 'assistant'
266
322
  else 'user'
267
323
  end
268
324
  end
269
325
 
270
- def format_system(messages, caching: nil, automatic_cache_target: nil)
326
+ def render_system(messages, caching: nil)
271
327
  messages.flat_map do |msg|
272
- blocks = Media.format_content(msg.content, msg.attachments, used_document_names: @used_document_names)
273
- if cache_boundary?(msg, automatic_cache_target, caching:)
274
- blocks + [converse_cache_block_for(caching, ttl: msg.cache_ttl)]
275
- else
276
- blocks
277
- end
328
+ blocks = Media.render_content(msg.content, msg.attachments, used_document_names: @used_document_names)
329
+ cache_boundary?(msg, caching:) ? blocks + [converse_cache_block_for(caching, ttl: msg.cache_ttl)] : blocks
278
330
  end
279
331
  end
280
332
 
281
- def automatic_cache_target(system_messages, chat_messages, caching)
282
- return unless caching
333
+ def cache_boundary?(message, caching: nil)
334
+ caching != false && message.cache_until_here?
335
+ end
336
+
337
+ # Bedrock's own caching of Claude and Nova prompts is best effort, so
338
+ # with caching on RubyLLM places explicit points: at the end of the
339
+ # conversation, so the next tool round or question can read what was
340
+ # sent so far; after the system prompt (or the tools without one),
341
+ # which stays cached when a later message changes; and after the
342
+ # previous assistant message, because Bedrock looks for an earlier
343
+ # cache entry only about 20 content blocks back from each point, and
344
+ # a round of parallel tool calls can be longer than that. Points the
345
+ # messages already mark count toward the limit of four, and an
346
+ # automatic point never puts a shorter lifetime before a longer one,
347
+ # which Bedrock refuses.
348
+ def add_automatic_cache_points(payload, model, caching)
349
+ return unless caching && automatic_cache_points?(model)
350
+
351
+ budget = MAX_CACHE_POINTS - cache_points(payload).size
352
+ automatic_cache_targets(payload, model).each do |blocks|
353
+ index = automatic_cache_point_index(blocks)
354
+ next unless index
355
+ break if budget <= 0
283
356
 
284
- (chat_messages.reverse + system_messages.reverse).find { |msg| cacheable_message?(msg) }
357
+ blocks.insert(index, converse_cache_block_for(caching))
358
+ next blocks.delete_at(index) unless cache_lifetimes_descend?(payload)
359
+
360
+ budget -= 1
361
+ end
285
362
  end
286
363
 
287
- def cacheable_message?(message)
288
- !message.tool_result?
364
+ # Claude refuses a cache point right after an Office document, so the
365
+ # point goes after the last block that takes one.
366
+ def automatic_cache_point_index(blocks)
367
+ last = blocks.rindex { |block| !office_document?(block) }
368
+ last + 1 if last && !cache_point?(blocks[last])
289
369
  end
290
370
 
291
- def cache_boundary?(message, automatic_cache_target, caching: nil)
292
- caching != false && (message.cache_until_here? || message.equal?(automatic_cache_target))
371
+ def office_document?(block)
372
+ block.is_a?(Hash) && OFFICE_DOCUMENT_FORMATS.include?(block.dig(:document, :format))
373
+ end
374
+
375
+ def search_results?(model)
376
+ model_id = foundation_model_id(model&.id)
377
+ SEARCH_RESULT_MODELS.any? { |prefix| model_id.start_with?(prefix) }
378
+ end
379
+
380
+ def automatic_cache_points?(model)
381
+ model_id = foundation_model_id(model&.id)
382
+ # An application inference profile ARN does not name its model.
383
+ CACHE_POINT_MODELS.any? { |prefix| model_id.start_with?(prefix) } || !model_id.include?('.')
384
+ end
385
+
386
+ def automatic_cache_targets(payload, model)
387
+ messages = payload[:messages]
388
+ earlier_answer = messages[0...-1].reverse.find { |message| message[:role] == 'assistant' }
389
+
390
+ [messages.last&.dig(:content), stable_prefix_blocks(payload, model), earlier_answer&.dig(:content)].compact
391
+ end
392
+
393
+ def stable_prefix_blocks(payload, model)
394
+ return payload[:system] if payload[:system]
395
+
396
+ tools = payload.dig(:toolConfig, :tools)
397
+ tools if tools && TOOL_CACHE_POINT_MODELS.any? { |prefix| foundation_model_id(model&.id).start_with?(prefix) }
398
+ end
399
+
400
+ # In the order Bedrock reads them: tools, system, then messages.
401
+ def cache_points(payload)
402
+ blocks = Array(payload.dig(:toolConfig, :tools)) + Array(payload[:system]) +
403
+ payload[:messages].flat_map { |message| message[:content] }
404
+ blocks.select { |block| cache_point?(block) }
405
+ end
406
+
407
+ def cache_lifetimes_descend?(payload)
408
+ lifetimes = cache_points(payload).map { |block| cache_lifetime(block[:cachePoint] || block['cachePoint']) }
409
+ lifetimes.each_cons(2).all? { |earlier, later| earlier >= later }
410
+ end
411
+
412
+ def cache_lifetime(point)
413
+ ttl = point.is_a?(Hash) ? (point[:ttl] || point['ttl']) : nil
414
+ amount, unit = ttl.to_s.match(/\A(\d+)([smh])\z/)&.captures
415
+ amount ? amount.to_i * CACHE_LIFETIME_UNITS.fetch(unit) : 300
416
+ end
417
+
418
+ def cache_point?(block)
419
+ block.is_a?(Hash) && (block.key?(:cachePoint) || block.key?('cachePoint'))
293
420
  end
294
421
 
295
422
  def converse_cache_block_for(caching, ttl: nil)
296
423
  options = prompt_cache_options(caching)
297
424
  ttl ||= options[:ttl]
298
425
  point = { type: 'default' }
299
- point[:ttl] = ttl if ttl
426
+ point[:ttl] = Protocol::CacheLifetime.render(ttl) if ttl
300
427
  { cachePoint: point }
301
428
  end
302
429
 
@@ -311,37 +438,37 @@ module RubyLLM
311
438
  "Bedrock Converse prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
312
439
  end
313
440
 
441
+ def cache_option_names = PROMPT_CACHE_OPTIONS
442
+
314
443
  def format_cache_option_keys(keys)
315
444
  keys.map { |key| ":#{key}" }.join(', ')
316
445
  end
317
446
 
318
- def format_inference_config(_model, temperature, max_output_tokens = nil)
447
+ def render_inference_config(_model, temperature, max_output_tokens = nil)
319
448
  config = {}
320
449
  config[:temperature] = temperature unless temperature.nil?
321
450
  config[:maxTokens] = max_output_tokens unless max_output_tokens.nil?
322
451
  config
323
452
  end
324
453
 
325
- def format_tool_config(tools, tool_prefs)
454
+ def render_tool_config(tools, tool_prefs)
326
455
  return nil if tools.empty?
327
456
 
328
457
  config = {
329
- tools: tools.values.map { |tool| format_tool(tool) }
458
+ tools: tools.values.map { |tool| render_tool(tool) }
330
459
  }
331
460
 
332
461
  return config if tool_prefs.nil? || tool_prefs[:choice].nil?
333
462
 
334
- tool_choice = format_tool_choice(tool_prefs[:choice])
335
- config[:toolChoice] = tool_choice if tool_choice
336
- config
463
+ config.merge(toolChoice: render_tool_choice(tool_prefs[:choice]))
337
464
  end
338
465
 
339
- def format_tool_choice(choice)
466
+ def render_tool_choice(choice)
340
467
  case choice
341
468
  when :auto
342
469
  { auto: {} }
343
470
  when :none
344
- nil
471
+ raise ArgumentError, 'Bedrock Converse has no tool choice of none; remove the tools from this request'
345
472
  when :required
346
473
  { any: {} }
347
474
  else
@@ -349,7 +476,7 @@ module RubyLLM
349
476
  end
350
477
  end
351
478
 
352
- def format_tool(tool)
479
+ def render_tool(tool)
353
480
  input_schema = tool.parameters_schema ||
354
481
  RubyLLM::Tool::SchemaDefinition.from_parameters(tool.declared_parameters)&.json_schema
355
482
 
@@ -368,15 +495,25 @@ module RubyLLM
368
495
  RubyLLM::Support::Utils.deep_merge(tool_spec, tool.provider_options)
369
496
  end
370
497
 
371
- def format_additional_model_request_fields(thinking, model, max_output_tokens = nil)
498
+ def render_additional_model_request_fields(thinking, model, max_output_tokens = nil)
372
499
  fields = {}
373
500
 
374
- reasoning_fields = Reasoning.format_reasoning_fields(thinking, model, max_output_tokens)
501
+ reasoning_fields = Reasoning.render_reasoning_fields(thinking, model, max_output_tokens)
375
502
  fields = RubyLLM::Support::Utils.deep_merge(fields, reasoning_fields) if reasoning_fields
376
503
 
377
504
  fields.empty? ? nil : fields
378
505
  end
379
506
 
507
+ # Bound from the first request, as on the Anthropic API, so the
508
+ # thinking setting never changes mid-conversation.
509
+ def bind_thinking(fields, model)
510
+ return fields unless Anthropic::BlockBinding.binds?(foundation_model_id(model&.id))
511
+ return fields if fields&.key?(:reasoning_config)
512
+
513
+ bound = Anthropic::BlockBinding.apply(fields&.dig(:thinking))
514
+ bound ? { **fields.to_h, thinking: bound, anthropic_beta: [Anthropic::BlockBinding::BETA] } : fields
515
+ end
516
+
380
517
  def build_output_config(schema)
381
518
  return nil unless schema
382
519
 
@@ -403,7 +540,7 @@ module RubyLLM
403
540
  model_id.to_s.rpartition('/').last.sub(/\A(?:#{prefixes})\./, '')
404
541
  end
405
542
 
406
- def format_thinking_block(thinking)
543
+ def render_thinking_block(thinking)
407
544
  return nil unless thinking
408
545
 
409
546
  if thinking.text
@@ -424,7 +561,7 @@ module RubyLLM
424
561
  end
425
562
  end
426
563
 
427
- def extract_text_and_citations(content_blocks)
564
+ def parse_text_and_citations(content_blocks)
428
565
  text = +''
429
566
  citations = []
430
567
 
@@ -468,6 +605,7 @@ module RubyLLM
468
605
  text: text,
469
606
  start_index: start_index,
470
607
  end_index: end_index,
608
+ source_id: data['source'],
471
609
  source_index: citation_source_index(location),
472
610
  start_page: page['start'],
473
611
  end_page: end_page && (end_page - 1)
@@ -526,22 +664,19 @@ module RubyLLM
526
664
  end
527
665
 
528
666
  def parse_tool_calls(content_blocks)
529
- tool_calls = {}
530
-
531
- content_blocks.each do |block|
667
+ calls = content_blocks.filter_map do |block|
532
668
  tool_use = block['toolUse']
533
669
  next unless tool_use
534
670
  next if server_tool_use?(tool_use)
535
671
 
536
- tool_call_id = tool_use['toolUseId']
537
- tool_calls[tool_call_id] = ToolCall.new(
538
- id: tool_call_id,
672
+ ToolCall.new(
673
+ provider_call_id: tool_use['toolUseId'],
539
674
  name: tool_use['name'],
540
675
  arguments: tool_use['input'] || {}
541
676
  )
542
677
  end
543
678
 
544
- tool_calls.empty? ? nil : tool_calls
679
+ ToolCall.index(calls, by: :provider_call_id) unless calls.empty?
545
680
  end
546
681
 
547
682
  # Provider-executed tool steps come back with a distinguishing type,
@@ -557,7 +692,7 @@ module RubyLLM
557
692
  !type.nil? && type != 'tool_result'
558
693
  end
559
694
 
560
- def extract_server_tool_calls(content_blocks)
695
+ def parse_server_tool_calls(content_blocks)
561
696
  content_blocks.filter_map do |block|
562
697
  tool_use = block['toolUse']
563
698
  tool_result = block['toolResult']