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
@@ -82,7 +82,7 @@ module RubyLLM
82
82
  # retrying a rate-limited request, or +nil+ when the response carries
83
83
  # no timing information. The retry middleware already honors the
84
84
  # standard <tt>Retry-After</tt> header; override this to read
85
- # provider-specific rate-limit headers.
85
+ # provider-specific rate-limit headers or the error body.
86
86
  #
87
87
  # def retry_delay(response)
88
88
  # response.response_headers['x-acme-ratelimit-reset']&.to_f
@@ -111,6 +111,10 @@ module RubyLLM
111
111
  self.class.configuration_requirements
112
112
  end
113
113
 
114
+ def same_credentials?(other) # :nodoc:
115
+ settings == other.settings
116
+ end
117
+
114
118
  def original_image_detail? # :nodoc:
115
119
  false
116
120
  end
@@ -135,9 +139,10 @@ module RubyLLM
135
139
 
136
140
  def complete(messages, tools:, temperature:, model:, provider_options: {}, headers: {}, schema: nil, # :nodoc:
137
141
  max_output_tokens: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil,
138
- protocol: nil, before_request: [], usage_recorder: nil, provider_tools: [],
139
- compaction: nil, end_user: nil, &)
140
- protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
142
+ protocol: nil, before_request: [], usage_recorder: nil, progress_recorder: nil,
143
+ provider_tools: [], compaction: nil, end_user: nil, &)
144
+ protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:,
145
+ provider_tools:, messages:)
141
146
  protocol_class.new(self, model).complete(
142
147
  messages,
143
148
  tools: tools,
@@ -155,12 +160,21 @@ module RubyLLM
155
160
  end_user: end_user,
156
161
  before_request: before_request,
157
162
  usage_recorder: usage_recorder,
163
+ progress_recorder: progress_recorder,
158
164
  &
159
165
  )
160
166
  end
161
167
 
162
- def tool_approval_response(tool_call, approved:, model:, protocol: nil) # :nodoc:
163
- resolve_protocol(protocol, model).new(self, model).tool_approval_response(tool_call, approved:)
168
+ def tool_search?(model, protocol: nil) # :nodoc:
169
+ preprocessing_protocol(protocol, model).tool_search?
170
+ end
171
+
172
+ def skipped_tool_call_error(tool_call, model:, protocol: nil) # :nodoc:
173
+ resolve_protocol(protocol, model).new(self, model).skipped_tool_call_error(tool_call)
174
+ end
175
+
176
+ def tool_approval_response(tool_call, approved:, model:, protocol: nil, note: nil) # :nodoc:
177
+ resolve_protocol(protocol, model).new(self, model).tool_approval_response(tool_call, approved:, note:)
164
178
  end
165
179
 
166
180
  def compact(messages, model:, protocol: nil, headers: {}, before_request: [], usage_recorder: nil) # :nodoc:
@@ -172,7 +186,8 @@ module RubyLLM
172
186
  def render(messages, tools:, temperature:, model:, provider_options: {}, schema: nil, thinking: nil, # :nodoc:
173
187
  max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, protocol: nil,
174
188
  before_request: [], provider_tools: [], compaction: nil, end_user: nil)
175
- protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
189
+ protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:,
190
+ provider_tools:, messages:)
176
191
  protocol_class.new(self, model).render(
177
192
  messages,
178
193
  tools: tools,
@@ -205,6 +220,14 @@ module RubyLLM
205
220
  )
206
221
  end
207
222
 
223
+ def fallback_caching(caching, model:, protocol: nil) # :nodoc:
224
+ preprocessing_protocol(protocol, model).fallback_caching(caching)
225
+ end
226
+
227
+ def cache_key?(model:, protocol: nil) # :nodoc:
228
+ preprocessing_protocol(protocol, model).cache_key?
229
+ end
230
+
208
231
  def preprocess_messages(messages, model:, protocol: nil, unsupported_attachment: nil) # :nodoc:
209
232
  preprocessor = preprocessing_protocol(protocol, model)
210
233
  messages.map { |message| preprocessor.preprocess_message(message, unsupported_attachment:) }
@@ -251,7 +274,7 @@ module RubyLLM
251
274
 
252
275
  def batch_cost(tokens, model:, category: :text_tokens) # :nodoc:
253
276
  standard = Cost.new(tokens:, model:, category:)
254
- return standard if tokens.reported_cost
277
+ return standard if tokens.reported_cost || model.nil?
255
278
 
256
279
  pricing = model.pricing.public_send(category)
257
280
  batch_tier = pricing.batch unless long_context_pricing?(pricing, tokens)
@@ -320,10 +343,9 @@ module RubyLLM
320
343
  end
321
344
 
322
345
  def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {}) # :nodoc:
323
- protocol = resolve_protocol(nil, model, operation: :embed)
324
- protocol.new(self, model).embed(
325
- text, model: model_id_for(model), dimensions:, task_type:, title:, with:, provider_options:
326
- )
346
+ protocol = resolve_protocol(nil, model, operation: :embed).new(self, model)
347
+ protocol.reject_options(:embed, task_type:, title:)
348
+ protocol.embed(text, model: model_id_for(model), dimensions:, task_type:, title:, with:, provider_options:)
327
349
  end
328
350
 
329
351
  def render_embedding(text, model:, dimensions: nil) # :nodoc:
@@ -347,10 +369,9 @@ module RubyLLM
347
369
  end
348
370
 
349
371
  def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {}) # :nodoc:
350
- protocol = resolve_protocol(nil, model, operation: :paint)
351
- protocol.new(self, model).paint(
352
- prompt, model: model_id_for(model), size:, count:, with:, mask:, provider_options:
353
- )
372
+ protocol = resolve_protocol(nil, model, operation: :paint).new(self, model)
373
+ protocol.reject_options(:paint, count: (count if count.to_i > 1))
374
+ protocol.paint(prompt, model: model_id_for(model), size:, count:, with:, mask:, provider_options:)
354
375
  end
355
376
 
356
377
  def animate_later(prompt, model:, with: nil, extend: nil, provider_options: {}) # :nodoc:
@@ -360,6 +381,11 @@ module RubyLLM
360
381
  )
361
382
  end
362
383
 
384
+ def find_video_job(id, model:) # :nodoc:
385
+ protocol = resolve_protocol(nil, model, operation: :animate)
386
+ protocol.new(self, model).find_video_job(id, model: model_id_for(model))
387
+ end
388
+
363
389
  def speak(input, model:, voice:, format:, provider_options: {}, &) # :nodoc:
364
390
  protocol = resolve_protocol(nil, model, operation: :speak)
365
391
  protocol.new(self, model).speak(
@@ -370,6 +396,8 @@ module RubyLLM
370
396
  def transcribe(audio_file, model:, language:, format: nil, timestamps: nil, speaker_names: nil, # :nodoc:
371
397
  speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &)
372
398
  protocol = resolve_protocol(nil, model, operation: :transcribe).new(self, model)
399
+ protocol.reject_options(:transcribe, format:, temperature:, prompt:, speaker_names:, speaker_references:,
400
+ named_speakers: (speaker_names unless Array(speaker_names).empty?))
373
401
  options = protocol.render_transcription_options(timestamps:, format:, streaming: block_given?)
374
402
  provider_options = Support::Utils.deep_merge(options, provider_options)
375
403
  protocol.transcribe(
@@ -406,7 +434,9 @@ module RubyLLM
406
434
  ensure_files_supported!
407
435
  options = { filename:, purpose:, expires_in:, uri:, content_type:, provider_options: }.compact
408
436
 
409
- protocols.fetch(:files).new(self).upload(file, **options)
437
+ protocol = protocols.fetch(:files).new(self)
438
+ protocol.reject_options(:upload, purpose:, expires_in:, uri:)
439
+ protocol.upload(file, **options)
410
440
  end
411
441
 
412
442
  def cache_content(content, model:, ttl: nil, instructions: nil, with: nil) # :nodoc:
@@ -443,6 +473,10 @@ module RubyLLM
443
473
  protocols.fetch(:files).new(self).list_uris(uri)
444
474
  end
445
475
 
476
+ def settings # :nodoc:
477
+ self.class.configuration_options.map { |option| config.public_send(option) }
478
+ end
479
+
446
480
  def configured? # :nodoc:
447
481
  self.class.configured?(@config)
448
482
  end
@@ -470,6 +504,28 @@ module RubyLLM
470
504
  end
471
505
  end
472
506
 
507
+ # Returns the status a response sent as a success really means, for a
508
+ # provider that reports failures in a 2xx body, or +nil+.
509
+ def error_status(_response) # :nodoc:
510
+ nil
511
+ end
512
+
513
+ # Returns the provider's code for an error +response+, such as
514
+ # <tt>"invalid_request_error"</tt>, from the body's +code+, +status+,
515
+ # or +type+ under +error+, or its +code+ or +type+, or +nil+. Override
516
+ # when the provider names its errors elsewhere.
517
+ def parse_error_code(response) # :nodoc:
518
+ body = parse_error_body(response)
519
+ body = body.find { |part| part.is_a?(Hash) } if body.is_a?(Array)
520
+ return unless body.is_a?(Hash)
521
+
522
+ error = body['error']
523
+ candidates = error.is_a?(Hash) ? error.values_at('code', 'status', 'type') : []
524
+ candidates << body['code']
525
+ candidates << body['type'] unless body['type'] == 'error'
526
+ candidates.find { |candidate| candidate.is_a?(String) && candidate.match?(Error::ERROR_CODE) }
527
+ end
528
+
473
529
  class << self
474
530
  attr_reader :default_protocol # :nodoc:
475
531
  attr_writer :slug # :nodoc:
@@ -517,6 +573,18 @@ module RubyLLM
517
573
  id
518
574
  end
519
575
 
576
+ # The models.dev provider ids whose catalogs list this provider's
577
+ # models. Empty for a provider models.dev does not list.
578
+ def models_dev_provider_ids # :nodoc:
579
+ []
580
+ end
581
+
582
+ # The provider's +gen_ai.provider.name+ in the OpenTelemetry GenAI
583
+ # conventions. Providers the conventions name differently override this.
584
+ def open_telemetry_name # :nodoc:
585
+ slug
586
+ end
587
+
520
588
  # Returns the configuration keys that must be set before the
521
589
  # provider is usable. The base implementation returns an empty
522
590
  # array.
@@ -560,7 +628,7 @@ module RubyLLM
560
628
 
561
629
  # Returns whether +operation+ requires an inference model. Override
562
630
  # for endpoints that operate on an explicitly configured resource.
563
- def model_required?(**)
631
+ def model_required?(**) # :nodoc:
564
632
  true
565
633
  end
566
634
 
@@ -584,6 +652,25 @@ module RubyLLM
584
652
  @protocols ||= {}
585
653
  end
586
654
 
655
+ # Returns the part of +message+ that holds its answer, as read by the
656
+ # first of the provider's protocols that marks parts of a turn as
657
+ # intermediate, or +nil+ when none does. Such a protocol defines
658
+ # +parse_answer_text(message)+ as a class method.
659
+ def answer_text(message) # :nodoc:
660
+ protocols.each_value.lazy.filter_map do |protocol|
661
+ protocol.parse_answer_text(message) if protocol.respond_to?(:parse_answer_text)
662
+ end.first
663
+ end
664
+
665
+ # Returns the pages hosted web searches used for +message+, as read by
666
+ # each of the provider's protocols that reports them. Such a protocol
667
+ # defines +parse_web_sources(message)+ as a class method.
668
+ def web_sources(message) # :nodoc:
669
+ protocols.each_value.flat_map do |protocol|
670
+ protocol.respond_to?(:parse_web_sources) ? protocol.parse_web_sources(message) : []
671
+ end
672
+ end
673
+
587
674
  # Registers +provider_class+ under the slug +name+, making it
588
675
  # available to RubyLLM.chat and the other top-level helpers.
589
676
  # Stamps the class's slug, adds it to ::providers, and defines a
@@ -613,14 +700,14 @@ module RubyLLM
613
700
  # Resolves +model_id+ to the id the registry stores it under for this
614
701
  # provider. Defaults to the id unchanged; providers whose catalog ids
615
702
  # differ from their request ids (Bedrock's region prefixes) override it.
616
- def resolve_registry_id(model_id, _models, _config = nil)
703
+ def resolve_registry_id(model_id, _models, _config = nil) # :nodoc:
617
704
  model_id
618
705
  end
619
706
 
620
707
  # Returns the id of the model that the deployment named +model_id+
621
708
  # runs, or +nil+ when +model_id+ is not a declared deployment.
622
709
  # Providers whose requests name deployments override it.
623
- def deployed_model_id(_model_id, _config = nil)
710
+ def deployed_model_id(_model_id, _config = nil) # :nodoc:
624
711
  nil
625
712
  end
626
713
 
@@ -634,14 +721,6 @@ module RubyLLM
634
721
  @model_registry_files ||= {}
635
722
  end
636
723
 
637
- def local_providers # :nodoc:
638
- providers.select { |_slug, provider_class| provider_class.local? }
639
- end
640
-
641
- def remote_providers # :nodoc:
642
- providers.select { |_slug, provider_class| provider_class.remote? }
643
- end
644
-
645
724
  def configured_providers(config) # :nodoc:
646
725
  providers.select do |_slug, provider_class|
647
726
  provider_class.configured?(config)
@@ -711,6 +790,11 @@ module RubyLLM
711
790
  end
712
791
 
713
792
  def fetch_protocol(name)
793
+ if protocols.empty?
794
+ raise Error, "#{self.class.display_name} has no protocols. A provider subclass does not inherit its " \
795
+ 'parent\'s protocols, so declare them with `protocol`.'
796
+ end
797
+
714
798
  protocols.fetch(name.to_sym) do
715
799
  raise Error, "#{name} is not a protocol of #{self.class.display_name}. Available: #{protocols.keys.join(', ')}"
716
800
  end
@@ -720,6 +804,19 @@ module RubyLLM
720
804
  model.respond_to?(:id) ? model.id : model
721
805
  end
722
806
 
807
+ # Returns the base URL +setting+ holds, or +default+, as given. Raises
808
+ # when it has no http(s) scheme or ends in one of the provider's
809
+ # +endpoints+, since RubyLLM adds those paths itself.
810
+ def configured_api_base(setting, default = nil, example: default, endpoints: nil)
811
+ value = @config.public_send(setting) || default
812
+ endpoint = value.to_s.sub(/[?#].*/, '').chomp('/')[endpoints] if endpoints
813
+ return value if value.to_s.match?(%r{\Ahttps?://[^/?#]}i) && !endpoint
814
+
815
+ raise ConfigurationError, "#{setting} should look like #{example}" \
816
+ "#{endpoint ? " (without #{endpoint})" : ', starting with http:// or https://'}, " \
817
+ "got #{value.inspect}"
818
+ end
819
+
723
820
  def account_digest(*parts)
724
821
  Digest::SHA256.hexdigest(parts.join("\0"))
725
822
  end
@@ -745,9 +842,9 @@ module RubyLLM
745
842
  error = part['error']
746
843
  return error if error.is_a?(String)
747
844
 
748
- nested_message = error['message'] if error.is_a?(Hash)
749
- [nested_message, part['message'], part['detail']].find { |message| message.is_a?(String) } ||
750
- validation_detail_message(part['detail'])
845
+ detail = part['detail']
846
+ messages = [error, part, detail].filter_map { |source| source['message'] if source.is_a?(Hash) }
847
+ [*messages, detail].find { |message| message.is_a?(String) } || validation_detail_message(detail)
751
848
  end
752
849
 
753
850
  def validation_detail_message(detail)
@@ -13,7 +13,7 @@ module RubyLLM
13
13
  protocol :files, Protocols::Anthropic::Files
14
14
 
15
15
  def api_base
16
- @config.anthropic_api_base || 'https://api.anthropic.com'
16
+ configured_api_base(:anthropic_api_base, 'https://api.anthropic.com', endpoints: %r{/v1/messages\z})
17
17
  end
18
18
 
19
19
  def headers
@@ -30,6 +30,10 @@ module RubyLLM
30
30
  def batch_cost_multiplier(**) = 0.5
31
31
 
32
32
  class << self
33
+ def models_dev_provider_ids # :nodoc:
34
+ %w[anthropic]
35
+ end
36
+
33
37
  def capabilities
34
38
  Anthropic::Capabilities
35
39
  end
@@ -13,13 +13,27 @@ module RubyLLM
13
13
  Azure::Media.supported_attachment?(attachment)
14
14
  end
15
15
 
16
- def format_content(content, attachments = [])
17
- Media.format_content(content, attachments)
16
+ def render_content(content, attachments = [])
17
+ Media.render_content(content, attachments)
18
18
  end
19
19
 
20
- def format_role(role)
20
+ def render_role(role)
21
21
  role.to_s
22
22
  end
23
+
24
+ private
25
+
26
+ # Grok on Azure reports cached tokens beside prompt_tokens, not inside
27
+ # them, so prompt_tokens is the uncached input.
28
+ def input_tokens(usage)
29
+ grok? ? usage['prompt_tokens'] : super
30
+ end
31
+
32
+ def grok?
33
+ return false unless @model
34
+
35
+ @model.family == 'grok' || @model.id.start_with?('grok')
36
+ end
23
37
  end
24
38
  end
25
39
  end
@@ -13,6 +13,7 @@ module RubyLLM
13
13
  include Azure::Videos
14
14
  include Azure::Images
15
15
  include Azure::Audio
16
+ include Azure::Moderation
16
17
 
17
18
  def azure_endpoint(kind)
18
19
  parts = azure_base_parts
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure < Provider
6
+ # Azure's dialect of OpenAI Decisions, served from the openai/v1 base.
7
+ # The model field carries the deployment name, as on Responses. The v1
8
+ # routes accept only the v1 and preview API versions, so the request
9
+ # names v1 over any dated version the configured base carries.
10
+ class Decisions < Protocols::OpenAI::Decisions
11
+ def judgment_url
12
+ "#{@provider.azure_openai_v1_base.sub(%r{/+\z}, '')}/decisions?api-version=v1"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -11,8 +11,7 @@ module RubyLLM
11
11
  azure_endpoint(:embeddings)
12
12
  end
13
13
 
14
- # rubocop:disable-next Lint/UnusedMethodArgument
15
- def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
14
+ def render_embedding_payload(text, model:, dimensions:, provider_options: {}, **)
16
15
  {
17
16
  model: model,
18
17
  input: [text].flatten,
@@ -11,23 +11,23 @@ module RubyLLM
11
11
  %i[image audio text].include?(attachment.type)
12
12
  end
13
13
 
14
- def format_content(content, attachments = [])
15
- Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
14
+ def render_content(content, attachments = [])
15
+ Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
16
16
  raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
17
17
 
18
18
  case attachment.type
19
19
  when :image
20
- Protocols::ChatCompletions::Media.with_image_detail(format_image(attachment), attachment,
20
+ Protocols::ChatCompletions::Media.with_image_detail(render_image(attachment), attachment,
21
21
  original_detail: true)
22
22
  when :audio
23
- Protocols::ChatCompletions::Media.format_audio(attachment)
23
+ Protocols::ChatCompletions::Media.render_audio(attachment)
24
24
  when :text
25
- Protocols::ChatCompletions::Media.format_text_file(attachment)
25
+ Protocols::ChatCompletions::Media.render_text_file(attachment)
26
26
  end
27
27
  end
28
28
  end
29
29
 
30
- def format_image(image)
30
+ def render_image(image)
31
31
  {
32
32
  type: 'image_url',
33
33
  image_url: {
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure < Provider
6
+ # Azure's Chat Completions plus Mistral's document models on Foundry,
7
+ # which read Mistral's OCR request at a route under the resource root.
8
+ class Mistral < Azure::ChatCompletions
9
+ include Providers::Mistral::OCR
10
+
11
+ def ocr_url
12
+ "#{@provider.azure_base_parts[:root]}/providers/mistral/azure/ocr"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -10,12 +10,26 @@ module RubyLLM
10
10
  end
11
11
 
12
12
  # Azure reports the same id once per region it is offered in, and keeps
13
- # returning models whose inference deprecation date has passed.
13
+ # returning models whose inference deprecation date has passed. The
14
+ # listing leaves out Azure AI Speech, so fast transcription is
15
+ # appended.
14
16
  def parse_list_models_response(response, slug)
15
17
  Array(response.body['data'])
16
18
  .uniq { |model_data| model_data['id'] }
17
19
  .reject { |model_data| azure_inference_retired?(model_data) }
18
- .map { |model_data| azure_model(model_data, slug) }
20
+ .map { |model_data| azure_model(model_data, slug) } + [fast_transcription_model(slug)]
21
+ end
22
+
23
+ def fast_transcription_model(slug)
24
+ Model.new(
25
+ id: Azure::FAST_TRANSCRIPTION_MODEL,
26
+ name: 'Fast Transcription',
27
+ provider: slug,
28
+ family: 'azure-speech',
29
+ modalities: { input: ['audio'], output: ['text'] },
30
+ capabilities: ['transcription'],
31
+ pricing: {}
32
+ )
19
33
  end
20
34
 
21
35
  def azure_inference_retired?(model_data)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Foundry's REST reference lists /openai/v1/moderations, but resource
7
+ # and project endpoints answer it with the 404 of an unknown route and
8
+ # the catalog holds no moderation model.
9
+ module Moderation
10
+ def render_moderation_payload(*, **)
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -11,12 +11,21 @@ module RubyLLM
11
11
  include Azure::Videos
12
12
  include Azure::Images
13
13
  include Azure::Audio
14
+ include Azure::Moderation
14
15
  include Protocols::Responses::Compaction
15
16
 
16
17
  def completion_url
17
18
  "#{@provider.azure_openai_v1_base}/responses"
18
19
  end
19
20
 
21
+ def document_urls?
22
+ true
23
+ end
24
+
25
+ def web_search_sources?
26
+ true
27
+ end
28
+
20
29
  def provider_file_upload_options(_attachment)
21
30
  { purpose: 'assistants' }
22
31
  end
@@ -13,7 +13,7 @@ module RubyLLM
13
13
  azure_video_url('video/generations/jobs')
14
14
  end
15
15
 
16
- def render_video_payload(prompt, model:, with: [], provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
16
+ def render_video_payload(prompt, model:, provider_options: {}, **)
17
17
  { model: model, prompt: prompt }.merge(provider_options)
18
18
  end
19
19
 
@@ -33,7 +33,7 @@ module RubyLLM
33
33
  azure_video_url("video/generations/jobs/#{job.id}")
34
34
  end
35
35
 
36
- def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
36
+ def parse_video_job_status(response, **)
37
37
  body = response.body
38
38
  case body['status']
39
39
  when 'succeeded' then { status: :completed, raw: body }
@@ -7,18 +7,30 @@ module RubyLLM
7
7
  DEFAULT_CHAT_API_VERSION = '2024-05-01-preview'
8
8
  DEFAULT_EMBEDDINGS_API_VERSION = '2024-05-01-preview'
9
9
  DEFAULT_MODELS_API_VERSION = 'preview'
10
+ SPEECH_API_VERSION = '2025-10-15'
11
+
12
+ # Azure AI Speech fast transcription. The endpoint takes no model, so
13
+ # RubyLLM names it after the API.
14
+ FAST_TRANSCRIPTION_MODEL = 'fast-transcription'
10
15
 
11
16
  protocol :chat_completions, Azure::ChatCompletions, batches: Azure::ChatCompletions::Batches
12
17
  protocol :responses, Azure::Responses
13
18
  protocol :files, Protocols::Azure::Files
14
19
  protocol :cohere, Azure::Cohere
20
+ protocol :mistral, Azure::Mistral
21
+ protocol :decisions, Azure::Decisions
22
+ protocol :speech, Protocols::Azure::Speech
15
23
 
16
24
  def original_image_detail? # :nodoc:
17
25
  true
18
26
  end
19
27
 
28
+ # A base ending in /models or /openai/v1/responses is the resource or
29
+ # its openai/v1 base, since RubyLLM adds those paths itself.
20
30
  def api_base
21
- @config.azure_api_base
31
+ configured_api_base(:azure_api_base, example: 'https://RESOURCE.openai.azure.com')
32
+ .sub(%r{\A(https?://[^/?#]+)/models/?(?=\?|\z)}, '\\1')
33
+ .sub(%r{/openai/v1/responses/?(?=\?|\z)}, '/openai/v1')
22
34
  end
23
35
 
24
36
  # Files belong to the Azure resource, whichever key or Entra ID token
@@ -27,12 +39,19 @@ module RubyLLM
27
39
  account_digest(azure_openai_v1_base)
28
40
  end
29
41
 
42
+ def resolve_protocol(name, model, operation: nil, **options)
43
+ name ||= :decisions if operation == :judge
44
+ name ||= :speech if operation == :transcribe && model&.id == FAST_TRANSCRIPTION_MODEL
45
+ super
46
+ end
47
+
30
48
  # Deployments named after gpt-5.4+ and gpt-6+ models need the Responses
31
49
  # API for tool use, so they route there automatically. Deployment names often
32
50
  # differ from model ids, so the routing stays conservative; an explicit
33
51
  # protocol: or the azure_protocol configuration option overrides it.
34
52
  def protocol_for(model, operation: nil, **)
35
53
  return protocols[:cohere] if operation == :rerank
54
+ return protocols[:mistral] if operation == :ocr
36
55
  if operation == :embed &&
37
56
  %w[Cohere-embed-v3-english Cohere-embed-v3-multilingual embed-v-4-0].include?(model.id)
38
57
  return protocols[:cohere]
@@ -53,6 +72,21 @@ module RubyLLM
53
72
 
54
73
  def batch_cost_multiplier(**) = 0.5
55
74
 
75
+ # Azure AI Speech answers on the resource's own host, whichever of its
76
+ # endpoints the API base names.
77
+ def azure_speech_transcription_url # :nodoc:
78
+ uri = URI.parse(api_base)
79
+ uri.path = '/speechtotext/transcriptions:transcribe'
80
+ uri.query = "api-version=#{SPEECH_API_VERSION}"
81
+ uri.to_s
82
+ end
83
+
84
+ # Azure AI Speech takes a resource key in its own header. An Entra ID
85
+ # token is already sent as a bearer token.
86
+ def azure_speech_headers # :nodoc:
87
+ @config.azure_api_key ? { 'Ocp-Apim-Subscription-Key' => @config.azure_api_key } : {}
88
+ end
89
+
56
90
  def azure_cohere_url(operation) # :nodoc:
57
91
  base = azure_base_parts[:path_base]
58
92
  base = if base.include?('/providers/cohere')
@@ -113,6 +147,14 @@ module RubyLLM
113
147
  end
114
148
 
115
149
  class << self
150
+ def open_telemetry_name # :nodoc:
151
+ 'azure.ai.openai'
152
+ end
153
+
154
+ def models_dev_provider_ids # :nodoc:
155
+ %w[azure]
156
+ end
157
+
116
158
  def capabilities
117
159
  Azure::Capabilities
118
160
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Bedrock
6
+ class Anthropic
7
+ # Bedrock Model Invocation Jobs whose records are InvokeModel bodies
8
+ # in the Anthropic Messages format.
9
+ module Batches
10
+ include Protocols::Bedrock::Batches
11
+
12
+ private
13
+
14
+ def bedrock_invocation_type
15
+ 'InvokeModel'
16
+ end
17
+
18
+ def validate_bedrock_batch_requests!(_requests); end
19
+
20
+ def bedrock_model_input(request)
21
+ payload = batch_payload(request)
22
+ render_invoke_body(payload, beta_headers(request[:headers].to_h, payload))
23
+ end
24
+
25
+ def parse_bedrock_model_output(body, model:)
26
+ parse_completion_body(body.merge('model' => model), raw: body)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end