ruby_llm 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +160 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +515 -274
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1593 -331
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +50 -24
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +127 -106
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +15 -15
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +1 -7
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +5 -14
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +213 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RubyLLM
6
+ module Providers
7
+ class Bedrock
8
+ # The Anthropic Messages API on Bedrock's InvokeModel endpoints, which
9
+ # serve every Claude model and cross-region inference profile. Unlike
10
+ # Converse, it carries Anthropic's own features, such as tool search,
11
+ # so a prompt cache survives the tools a model loads on demand.
12
+ class Anthropic < Protocols::Anthropic
13
+ include Protocols::Bedrock::EventStream
14
+
15
+ ANTHROPIC_VERSION = 'bedrock-2023-05-31'
16
+ TOOL_SEARCH_BETA = 'tool-search-tool-2025-10-19'
17
+
18
+ def completion_url
19
+ model_path(@model.id, 'invoke')
20
+ end
21
+
22
+ def stream_url
23
+ model_path(@model.id, 'invoke-with-response-stream')
24
+ end
25
+
26
+ # Bedrock counts tokens for the foundation model, not for the
27
+ # inference profiles that serve it.
28
+ def count_tokens_url
29
+ model_path(Protocols::Converse::Chat.foundation_model_id(@model.id), 'count-tokens')
30
+ end
31
+
32
+ def render_payload(messages, **)
33
+ payload = super.except(:model, :stream)
34
+ payload[:messages] = inline_url_sources(payload[:messages])
35
+ payload[:system] = inline_url_sources(payload[:system]) if payload[:system]
36
+ payload.merge(anthropic_version: ANTHROPIC_VERSION)
37
+ end
38
+
39
+ def parse_count_tokens_response(response)
40
+ response.body['inputTokens']
41
+ end
42
+
43
+ # The body names the foundation model; pricing and the transcript
44
+ # follow the inference profile or ARN that was called.
45
+ def parse_response_model_id(data)
46
+ called_model_id(super)
47
+ end
48
+
49
+ def parse_model_id(data)
50
+ called_model_id(super)
51
+ end
52
+
53
+ def called_model_id(named)
54
+ named && (@model&.id || named)
55
+ end
56
+
57
+ def provider_file_references?
58
+ false
59
+ end
60
+
61
+ private
62
+
63
+ def sync_response(payload, additional_headers = {})
64
+ headers = beta_headers(additional_headers, payload)
65
+ response = @connection.post(completion_url, JSON.generate(render_invoke_body(payload, headers)),
66
+ usage: @usage_tracker) do |request|
67
+ request.headers.merge!(without_beta_header(headers))
68
+ end
69
+ parse_completion_response(response)
70
+ end
71
+
72
+ def stream_events(url, payload, additional_headers = {}, &)
73
+ body = JSON.generate(render_invoke_body(payload, additional_headers))
74
+ super(url, body, without_beta_header(additional_headers), &)
75
+ end
76
+
77
+ def post_count_tokens(payload)
78
+ body = payload.except(:model).merge(anthropic_version: ANTHROPIC_VERSION, max_tokens: 1)
79
+ body = JSON.generate(render_invoke_body(body, beta_headers({}, payload)))
80
+ request = { input: { invokeModel: { body: Base64.strict_encode64(body) } } }
81
+ @connection.post(count_tokens_url, JSON.generate(request))
82
+ end
83
+
84
+ # Bedrock still gates the tool search tool behind its beta.
85
+ def beta_headers(additional_headers, payload)
86
+ headers = super
87
+ tools = Array(payload[:tools] || payload['tools'])
88
+ tools.any? { |tool| search_tool?(tool) } ? add_beta(headers, TOOL_SEARCH_BETA) : headers
89
+ end
90
+
91
+ # InvokeModel takes Anthropic's betas in the body, not as a header.
92
+ def render_invoke_body(payload, headers)
93
+ betas = canonical_beta_headers(headers)[BETA_HEADER].to_s.split(',').map(&:strip).reject(&:empty?)
94
+ return payload if betas.empty?
95
+
96
+ payload.merge(anthropic_beta: (Array(payload[:anthropic_beta]) + betas).uniq)
97
+ end
98
+
99
+ def without_beta_header(headers)
100
+ canonical_beta_headers(headers).except(BETA_HEADER)
101
+ end
102
+
103
+ # Bedrock does not fetch URLs, so remote images and documents travel
104
+ # as their bytes.
105
+ def inline_url_sources(blocks)
106
+ return blocks unless blocks.is_a?(Array)
107
+
108
+ blocks.map do |block|
109
+ next block unless block.is_a?(Hash)
110
+ next inline_url_source(block) if url_source?(block)
111
+
112
+ block[:content].is_a?(Array) ? block.merge(content: inline_url_sources(block[:content])) : block
113
+ end
114
+ end
115
+
116
+ def url_source?(block)
117
+ source = block[:source]
118
+ source.is_a?(Hash) && source[:type] == 'url'
119
+ end
120
+
121
+ def inline_url_source(block)
122
+ attachment = Attachment.new(block.dig(:source, :url))
123
+ block.merge(source: { type: 'base64', media_type: attachment.mime_type, data: attachment.encoded })
124
+ end
125
+
126
+ # Application inference profile ARNs contain '/', which Bedrock
127
+ # expects escaped inside the model segment.
128
+ def model_path(model_id, action)
129
+ "/model/#{model_id.to_s.gsub('/', '%2F')}/#{action}"
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -23,7 +23,7 @@ module RubyLLM
23
23
  payload_hash = Digest::SHA256.hexdigest(body)
24
24
 
25
25
  headers = {
26
- 'host' => URI.parse(base_url).host,
26
+ 'host' => signed_host(base_url),
27
27
  'x-amz-content-sha256' => payload_hash,
28
28
  'x-amz-date' => amz_date
29
29
  }
@@ -85,12 +85,21 @@ module RubyLLM
85
85
  end
86
86
 
87
87
  conn.url_prefix = base_url
88
+ path = url.delete_prefix('/')
89
+ sent = conn.build_url(path).request_uri
88
90
 
89
- conn.public_send(method, url, payload) do |req|
90
- req.headers.merge!(sign_headers(method.to_s.upcase, url, body, base_url:, service:))
91
+ conn.public_send(method, path, payload) do |req|
92
+ req.headers.merge!(sign_headers(method.to_s.upcase, sent, body, base_url:, service:))
91
93
  end
92
94
  end
93
95
 
96
+ # HTTP clients send the port in the Host header unless it is the
97
+ # scheme's default.
98
+ def signed_host(base_url)
99
+ uri = URI.parse(base_url)
100
+ uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
101
+ end
102
+
94
103
  def canonical_query_string(raw_query)
95
104
  return '' if raw_query.nil? || raw_query.empty?
96
105
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Bedrock
6
+ # Keeps Bedrock's API paths under the configured endpoint. Faraday
7
+ # resolves a leading slash against the host, which would drop the path of
8
+ # an endpoint mounted behind a gateway.
9
+ class Connection < Transport::Connection # :nodoc:
10
+ private
11
+
12
+ def prepare(request)
13
+ super
14
+ request.path = request.path.delete_prefix('/')
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -19,7 +19,11 @@ module RubyLLM
19
19
  'anthropic/v1/messages/count_tokens'
20
20
  end
21
21
 
22
- def supports_provider_file_references?
22
+ def thinking_block_binding?
23
+ false
24
+ end
25
+
26
+ def provider_file_references?
23
27
  false
24
28
  end
25
29
 
@@ -10,10 +10,10 @@ module RubyLLM
10
10
  include Mantle
11
11
 
12
12
  def completion_url
13
- 'v1/chat/completions'
13
+ "#{openai_api_path}/chat/completions"
14
14
  end
15
15
 
16
- def supports_provider_file_references?
16
+ def provider_file_references?
17
17
  false
18
18
  end
19
19
  end
@@ -10,11 +10,21 @@ module RubyLLM
10
10
  class Responses < Protocols::Responses
11
11
  include Mantle
12
12
 
13
+ # AWS Web Search fetches pages as part of the web_search tool, with
14
+ # no separate fetch tool to enable.
15
+ SERVER_TOOL_ALIASES = Protocols::Responses::SERVER_TOOL_ALIASES.merge(
16
+ web_fetch: { tool: { type: 'web_search' } }
17
+ ).freeze
18
+
19
+ def server_tool_aliases
20
+ SERVER_TOOL_ALIASES
21
+ end
22
+
13
23
  def completion_url
14
- 'v1/responses'
24
+ "#{openai_api_path}/responses"
15
25
  end
16
26
 
17
- def supports_provider_file_references?
27
+ def provider_file_references?
18
28
  false
19
29
  end
20
30
  end
@@ -64,7 +64,7 @@ module RubyLLM
64
64
  raise UnsupportedAttachmentError, attachment.mime_type unless attachment.audio?
65
65
 
66
66
  content = [
67
- Protocols::ChatCompletions::Media.format_audio(attachment),
67
+ Protocols::ChatCompletions::Media.render_audio(attachment),
68
68
  { type: 'text', text: transcription_prompt(prompt, language) }
69
69
  ]
70
70
  payload = { model:, messages: [{ role: 'user', content: }], temperature: temperature || 0 }
@@ -30,6 +30,13 @@ module RubyLLM
30
30
 
31
31
  private
32
32
 
33
+ # OpenAI's GPT models answer only under /openai/v1, and every other
34
+ # model, gpt-oss included, only under /v1.
35
+ def openai_api_path
36
+ id = @model&.id.to_s
37
+ id.start_with?('openai.') && !id.start_with?('openai.gpt-oss') ? 'openai/v1' : 'v1'
38
+ end
39
+
33
40
  def sync_response(payload, additional_headers = {})
34
41
  parse_completion_response signed_post(completion_url, payload, additional_headers)
35
42
  end
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  end
26
26
 
27
27
  def models_dev_alias(model_id, models_dev_by_key, _provider_model = nil)
28
- normalized_id = model_id.sub(/^[a-z]{2}\./, '')
28
+ normalized_id = model_id.sub(/\A(?:#{Protocols::Converse::REGION_PREFIXES.join('|')})\./, '')
29
29
  context_override = nil
30
30
  normalized_id = normalized_id.gsub(/:(\d+)k\b/) do
31
31
  context_override = Regexp.last_match(1).to_i * 1000
@@ -209,12 +209,6 @@ module RubyLLM
209
209
  inference_types.include?('INFERENCE_PROFILE') && !inference_types.include?('ON_DEMAND')
210
210
  end
211
211
 
212
- def normalize_inference_profile_id(model_id, inference_types, region)
213
- return model_id unless inference_profile_only?(inference_types)
214
-
215
- with_region_prefix(model_id, region)
216
- end
217
-
218
212
  def with_region_prefix(model_id, region)
219
213
  prefixed_with(model_id, region_prefix(region))
220
214
  end
@@ -8,6 +8,7 @@ module RubyLLM
8
8
  include Bedrock::Models
9
9
 
10
10
  protocol :converse, Protocols::Converse, batches: Protocols::Converse::Batches
11
+ protocol :anthropic, Bedrock::Anthropic, batches: Bedrock::Anthropic::Batches
11
12
  protocol :mantle_anthropic, Bedrock::Mantle::Anthropic
12
13
  protocol :mantle_responses, Bedrock::Mantle::Responses
13
14
  protocol :mantle_chat_completions, Bedrock::Mantle::ChatCompletions
@@ -50,6 +51,14 @@ module RubyLLM
50
51
  Models.models_dev_alias(...)
51
52
  end
52
53
 
54
+ def self.open_telemetry_name # :nodoc:
55
+ 'aws.bedrock'
56
+ end
57
+
58
+ def self.models_dev_provider_ids # :nodoc:
59
+ %w[amazon-bedrock]
60
+ end
61
+
53
62
  def initialize(config) # :nodoc:
54
63
  super
55
64
  @connection = signed_connection(api_base)
@@ -64,9 +73,14 @@ module RubyLLM
64
73
  return protocols[:rerank] if operation == :rerank
65
74
  return video_protocol_for(model_id) if operation == :animate
66
75
  return protocols[:voxtral_transcription] if voxtral_transcription?(operation, model_id)
67
- return mantle_protocol_for(model_id) if mantle_model?(model_id)
68
76
 
69
- super
77
+ chat_protocol_for(model_id) || super
78
+ end
79
+
80
+ # Claude speaks the Anthropic Messages API through InvokeModel, which
81
+ # keeps features Converse drops, such as tool search.
82
+ def claude_model?(model_id) # :nodoc:
83
+ Protocols::Converse::Chat.foundation_model_id(model_id).start_with?('anthropic.claude')
70
84
  end
71
85
 
72
86
  # Un-versioned ids such as anthropic.claude-sonnet-5 and
@@ -77,7 +91,8 @@ module RubyLLM
77
91
  end
78
92
 
79
93
  def mantle_api_base
80
- @config.bedrock_mantle_api_base || "https://bedrock-mantle.#{bedrock_region}.api.aws"
94
+ configured_api_base(:bedrock_mantle_api_base, "https://bedrock-mantle.#{bedrock_region}.api.aws",
95
+ endpoints: %r{/(?:chat/completions|responses|messages)\z})
81
96
  end
82
97
 
83
98
  def mantle_connection
@@ -85,7 +100,8 @@ module RubyLLM
85
100
  end
86
101
 
87
102
  def api_base
88
- @config.bedrock_api_base || "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
103
+ configured_api_base(:bedrock_api_base, "https://bedrock-runtime.#{bedrock_region}.amazonaws.com",
104
+ endpoints: %r{/model/.*\z})
89
105
  end
90
106
 
91
107
  # Uploads are objects in the configured bucket, readable with any
@@ -96,11 +112,13 @@ module RubyLLM
96
112
  end
97
113
 
98
114
  def control_api_base
99
- @config.bedrock_api_base || "https://bedrock.#{bedrock_region}.amazonaws.com"
115
+ configured_api_base(:bedrock_api_base, "https://bedrock.#{bedrock_region}.amazonaws.com",
116
+ endpoints: %r{/model/.*\z})
100
117
  end
101
118
 
102
119
  def agent_api_base # :nodoc:
103
- @config.bedrock_api_base || "https://bedrock-agent-runtime.#{bedrock_region}.amazonaws.com"
120
+ configured_api_base(:bedrock_api_base, "https://bedrock-agent-runtime.#{bedrock_region}.amazonaws.com",
121
+ endpoints: %r{/model/.*\z})
104
122
  end
105
123
 
106
124
  def agent_connection # :nodoc:
@@ -131,6 +149,10 @@ module RubyLLM
131
149
 
132
150
  def batch_cost_multiplier(**) = 0.5
133
151
 
152
+ def invoke_model_batch_protocol(model_id) # :nodoc:
153
+ claude_model?(model_id) ? protocols[:anthropic] : embedding_batch_protocol(model_id)
154
+ end
155
+
134
156
  def embedding_batch_protocol(model_id) # :nodoc:
135
157
  case model_id
136
158
  when 'amazon.titan-embed-text-v2:0' then protocols[:titan_text_embeddings]
@@ -146,6 +168,13 @@ module RubyLLM
146
168
  body['message'] || body['Message'] || nested_error_message(body) || body['__type'] || super
147
169
  end
148
170
 
171
+ def parse_error_code(response)
172
+ type = response.headers['x-amzn-errortype'] if response.respond_to?(:headers) && response.headers
173
+ type ||= parse_error_body(response).then { |body| body['__type'] if body.is_a?(Hash) }
174
+ code = type.to_s.split(':').first.to_s.split('#').last
175
+ code&.match?(Error::ERROR_CODE) ? code : super
176
+ end
177
+
149
178
  # bedrock-mantle nests code, message, and type under "error" rather
150
179
  # than answering with a bare string.
151
180
  def nested_error_message(body)
@@ -219,7 +248,7 @@ module RubyLLM
219
248
  signed = sign_headers(env.method.to_s.upcase, env.url.request_uri, env.body.to_s, base_url:, service:)
220
249
  { 'X-Amz-Security-Token' => nil }.merge(signed)
221
250
  end
222
- Transport::Connection.new(self, @config, api_base: base_url, signer:)
251
+ Connection.new(self, @config, api_base: base_url, signer:)
223
252
  end
224
253
 
225
254
  def voxtral_transcription?(operation, model_id)
@@ -227,8 +256,12 @@ module RubyLLM
227
256
  end
228
257
 
229
258
  def batch_protocol_for(requests)
230
- return super unless requests.any? { |request| request.key?(:text) }
259
+ return embedding_batch_protocol_for(requests) if requests.any? { |request| request.key?(:text) }
231
260
 
261
+ requests.all? { |request| anthropic_payload?(request[:payload]) } ? protocols[:anthropic] : super
262
+ end
263
+
264
+ def embedding_batch_protocol_for(requests)
232
265
  kinds = requests.map { |request| embedding_batch_protocol(request.fetch(:model)) }.uniq
233
266
  unless requests.all? { |request| request.key?(:text) } && kinds.one? && kinds.first
234
267
  raise Error, 'Bedrock embedding batches require one supported Titan embedding model'
@@ -237,6 +270,10 @@ module RubyLLM
237
270
  kinds.first
238
271
  end
239
272
 
273
+ def anthropic_payload?(payload)
274
+ payload.is_a?(Hash) && (payload.key?(:anthropic_version) || payload.key?('anthropic_version'))
275
+ end
276
+
240
277
  def bedrock_region
241
278
  @config.bedrock_region
242
279
  end
@@ -258,6 +295,12 @@ module RubyLLM
258
295
  parse_mantle_models_response(response, slug)
259
296
  end
260
297
 
298
+ def chat_protocol_for(model_id)
299
+ return mantle_protocol_for(model_id) if mantle_model?(model_id)
300
+
301
+ protocols[:anthropic] if claude_model?(model_id)
302
+ end
303
+
261
304
  def mantle_protocol_for(model_id)
262
305
  return protocols[:mantle_anthropic] if model_id.start_with?('anthropic.')
263
306
  return protocols[:mantle_responses] if Bedrock::Mantle::RESPONSES_MODELS.include?(model_id)
@@ -8,7 +8,7 @@ module RubyLLM
8
8
  protocol :files, Protocols::Cohere::Datasets
9
9
 
10
10
  def api_base
11
- @config.cohere_api_base || 'https://api.cohere.com'
11
+ configured_api_base(:cohere_api_base, 'https://api.cohere.com', endpoints: %r{/v2/(?:chat|embed|rerank)\z})
12
12
  end
13
13
 
14
14
  def headers
@@ -18,6 +18,10 @@ module RubyLLM
18
18
  end
19
19
 
20
20
  class << self
21
+ def models_dev_provider_ids # :nodoc:
22
+ %w[cohere]
23
+ end
24
+
21
25
  def configuration_options
22
26
  %i[cohere_api_key cohere_api_base]
23
27
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Deepgram
6
+ # Deepgram's dialect of Chat Completions on POST /v1/think. It takes
7
+ # the full history on each request and rejects any field it does not
8
+ # list, including the prompt cache fields.
9
+ module Chat
10
+ def completion_url
11
+ @provider.think_url
12
+ end
13
+
14
+ # Think-Fast rejects the developer role.
15
+ def render_role(role)
16
+ role.to_s
17
+ end
18
+
19
+ def openai_prompt_caching?
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,12 +2,31 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- # Integrates Deepgram transcription and speech synthesis.
5
+ # Integrates Deepgram transcription, speech synthesis, and chat.
6
6
  class Deepgram < Provider
7
+ # Deepgram's dialect of the Chat Completions API.
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include Deepgram::Chat
10
+ end
11
+
12
+ THINK_BASE = 'https://agent.deepgram.com'
13
+ private_constant :THINK_BASE
14
+
7
15
  protocol :deepgram, Protocols::Deepgram
16
+ protocol :chat_completions, ChatCompletions
8
17
 
9
18
  def api_base
10
- @config.deepgram_api_base || 'https://api.deepgram.com'
19
+ configured_api_base(:deepgram_api_base, 'https://api.deepgram.com', endpoints: %r{/v1/(?:listen|speak|think)\z})
20
+ end
21
+
22
+ # Chat runs on its own host. Speech, transcription, and the model
23
+ # listing stay on the Deepgram protocol.
24
+ def protocol_for(model, operation: nil, **)
25
+ operation ? super : protocols[:chat_completions]
26
+ end
27
+
28
+ def think_url # :nodoc:
29
+ "#{(@config.deepgram_api_base || THINK_BASE).chomp('/')}/v1/think"
11
30
  end
12
31
 
13
32
  def headers
@@ -7,15 +7,16 @@ module RubyLLM
7
7
  module Chat
8
8
  module_function
9
9
 
10
- def format_role(role)
10
+ def render_role(role)
11
11
  role.to_s
12
12
  end
13
13
 
14
14
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
15
15
  schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
16
+ raise ArgumentError, 'DeepSeek Chat Completions cannot enforce a schema. Use protocol: :responses.' if schema
17
+
16
18
  payload = super
17
19
  configure_thinking_payload(payload, thinking)
18
- degrade_schema_payload(payload) if schema
19
20
  payload
20
21
  end
21
22
 
@@ -26,9 +27,7 @@ module RubyLLM
26
27
  def configure_thinking_payload(payload, thinking)
27
28
  return unless thinking&.enabled?
28
29
 
29
- if thinking.budget
30
- RubyLLM.logger.debug { "DeepSeek has no thinking budgets; ignoring budget #{thinking.budget}" }
31
- end
30
+ raise ArgumentError, 'DeepSeek has no thinking budget; use effort:' if thinking.budget
32
31
 
33
32
  if thinking.disabled?
34
33
  payload.delete(:reasoning_effort)
@@ -38,15 +37,7 @@ module RubyLLM
38
37
  end
39
38
  end
40
39
 
41
- def degrade_schema_payload(payload)
42
- RubyLLM.logger.warn(
43
- 'DeepSeek Chat Completions does not support json_schema response formats. ' \
44
- 'Use protocol: :responses to enforce the schema. Falling back to json_object mode.'
45
- )
46
- payload[:response_format] = { type: 'json_object' }
47
- end
48
-
49
- def format_thinking(msg)
40
+ def render_thinking(msg)
50
41
  return {} unless msg.role == :assistant
51
42
 
52
43
  thinking = msg.thinking
@@ -63,8 +54,8 @@ module RubyLLM
63
54
  )
64
55
  end
65
56
 
66
- def format_content(content, attachments = [])
67
- Protocols::ChatCompletions::Media.format_content(
57
+ def render_content(content, attachments = [])
58
+ Protocols::ChatCompletions::Media.render_content(
68
59
  content,
69
60
  attachments,
70
61
  document_attachments: :none,
@@ -8,6 +8,7 @@ module RubyLLM
8
8
  # instead of summaries, both in responses and in the stream.
9
9
  class Responses < Protocols::Responses
10
10
  SERVER_TOOL_ALIASES = {
11
+ web_search: { tool: { type: 'web_search' } },
11
12
  apply_patch: { tool: { type: 'custom', name: 'apply_patch' } }
12
13
  }.freeze
13
14
 
@@ -15,6 +16,22 @@ module RubyLLM
15
16
  SERVER_TOOL_ALIASES
16
17
  end
17
18
 
19
+ def render_payload(messages, thinking: nil, **)
20
+ raise ArgumentError, 'DeepSeek has no thinking budget; use effort:' if thinking&.budget
21
+
22
+ super
23
+ end
24
+
25
+ SERVER_TOOL_USAGE_NAMES = { 'tool_search' => 'web_search', 'tool_fetch' => 'web_fetch' }.freeze
26
+
27
+ # DeepSeek counts searches and page opens in usage, unlike tool_usage
28
+ # on the other Responses APIs.
29
+ def parse_server_tool_use(response)
30
+ response.dig('usage', 'server_tool_use').to_h.transform_keys do |tool|
31
+ "#{SERVER_TOOL_USAGE_NAMES.fetch(tool, tool)}_requests"
32
+ end
33
+ end
34
+
18
35
  def apply_end_user(payload, identifier)
19
36
  payload.merge(user_id: identifier)
20
37
  end
@@ -33,32 +50,32 @@ module RubyLLM
33
50
  texts.empty? ? super : texts.join("\n")
34
51
  end
35
52
 
36
- def format_assistant_items(message)
53
+ def render_assistant_items(message)
37
54
  items = super
38
55
  return items if message.raw_content || message.thinking&.text.to_s.empty? || message.thinking.signature
39
56
 
40
- items.unshift(format_reasoning_item(message.thinking))
57
+ items.unshift(render_reasoning_item(message.thinking))
41
58
  end
42
59
 
43
- def format_reasoning_item(thinking)
60
+ def render_reasoning_item(thinking)
44
61
  { type: 'reasoning', content: [{ type: 'reasoning_text', text: thinking.text }] }
45
62
  end
46
63
 
47
- def format_tool_items(message)
64
+ def render_tool_items(message)
48
65
  [{
49
66
  type: 'function_call_output',
50
- call_id: message.tool_call_id,
51
- output: format_content(message.content, message.attachments)
67
+ call_id: message.provider_call_id,
68
+ output: render_content(message.content, message.attachments)
52
69
  }]
53
70
  end
54
71
 
55
- def format_provider_file(file)
72
+ def render_provider_file(file)
56
73
  raise UnsupportedAttachmentError, file.mime_type unless file.image?
57
74
 
58
75
  { type: 'input_image', file_id: file.provider_file_id }
59
76
  end
60
77
 
61
- def format_document(document)
78
+ def render_document(document)
62
79
  raise UnsupportedAttachmentError, document.mime_type
63
80
  end
64
81
 
@@ -14,7 +14,7 @@ module RubyLLM
14
14
  protocol :files, Protocols::DeepSeek::Files
15
15
 
16
16
  def api_base
17
- @config.deepseek_api_base || 'https://api.deepseek.com'
17
+ configured_api_base(:deepseek_api_base, 'https://api.deepseek.com', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
18
18
  end
19
19
 
20
20
  def headers
@@ -28,6 +28,10 @@ module RubyLLM
28
28
  end
29
29
 
30
30
  class << self
31
+ def models_dev_provider_ids # :nodoc:
32
+ %w[deepseek]
33
+ end
34
+
31
35
  def capabilities
32
36
  DeepSeek::Capabilities
33
37
  end
@@ -14,7 +14,8 @@ module RubyLLM
14
14
  end
15
15
 
16
16
  def api_base
17
- @config.elevenlabs_api_base || 'https://api.elevenlabs.io'
17
+ configured_api_base(:elevenlabs_api_base, 'https://api.elevenlabs.io',
18
+ endpoints: %r{/v1/(?:speech-to-text\z|text-to-speech/.*\z)})
18
19
  end
19
20
 
20
21
  def headers