ruby_llm 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -4,6 +4,8 @@ module RubyLLM
4
4
  module Providers
5
5
  # OpenRouter API integration.
6
6
  class OpenRouter < Provider
7
+ BAD_GATEWAY = 502
8
+
7
9
  # OpenRouter's dialect of the Chat Completions API.
8
10
  class ChatCompletions < Protocols::ChatCompletions
9
11
  include OpenRouter::Chat
@@ -30,22 +32,27 @@ module RubyLLM
30
32
  end
31
33
  end
32
34
 
35
+ # OpenRouter's dialect of the System One API for decision models such as Jev and Clef.
36
+ class SystemOne < Protocols::SystemOne
37
+ include OpenRouter::Judgments
38
+ end
39
+
33
40
  protocol :chat_completions, ChatCompletions, batches: Protocols::OpenRouter::Batches
34
41
  protocol :responses, Protocols::OpenRouter::Responses
35
42
  protocol :files, Protocols::OpenRouter::Files
43
+ protocol :system_one, SystemOne
36
44
 
37
- def resolve_protocol(name, model, **request)
38
- return fetch_protocol(:chat_completions) if !name && request[:operation]
39
-
45
+ def resolve_protocol(name, model, operation: nil, **options)
46
+ name ||= (operation == :judge ? :system_one : :chat_completions) if operation
40
47
  super
41
48
  end
42
49
 
43
50
  def api_base
44
- @config.openrouter_api_base || 'https://openrouter.ai/api/v1'
51
+ configured_api_base(:openrouter_api_base, 'https://openrouter.ai/api/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
45
52
  end
46
53
 
47
54
  def batch_api_base
48
- "#{api_base.sub(%r{/v1/?\z}, '')}/beta/batches"
55
+ "#{api_base}/batches"
49
56
  end
50
57
 
51
58
  def headers
@@ -77,7 +84,27 @@ module RubyLLM
77
84
  end
78
85
  end
79
86
 
87
+ # OpenRouter answers 200 when an upstream provider fails after routing,
88
+ # with an error object or an empty body. A stream has no body here.
89
+ def error_status(response)
90
+ return if response.request.stream_response?
91
+
92
+ body = response.body.to_s
93
+ return BAD_GATEWAY if body.strip.empty?
94
+ return unless body.include?('"error"')
95
+
96
+ error = parse_error_body(response).then { |parsed| parsed['error'] if parsed.is_a?(Hash) }
97
+ return unless error.is_a?(Hash)
98
+
99
+ code = error['code']
100
+ code.is_a?(Integer) && code >= 400 ? code : BAD_GATEWAY
101
+ end
102
+
80
103
  class << self
104
+ def models_dev_provider_ids # :nodoc:
105
+ %w[openrouter]
106
+ end
107
+
81
108
  def configuration_options
82
109
  %i[openrouter_api_key openrouter_api_base openrouter_app_url openrouter_app_name]
83
110
  end
@@ -7,7 +7,7 @@ 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
 
@@ -15,8 +15,8 @@ module RubyLLM
15
15
  Perplexity::Media.supported_attachment?(attachment)
16
16
  end
17
17
 
18
- def format_content(content, attachments = [])
19
- Perplexity::Media.format_content(content, attachments)
18
+ def render_content(content, attachments = [])
19
+ Perplexity::Media.render_content(content, attachments)
20
20
  end
21
21
  end
22
22
  end
@@ -15,7 +15,7 @@ module RubyLLM
15
15
  def parse_embedding_response(response, model:, text:)
16
16
  data = response.body
17
17
  input_tokens = data.dig('usage', 'prompt_tokens')
18
- vectors = Array(data['data']).map { |vector| decode_embedding(vector['embedding']) }
18
+ vectors = order_embedding_rows(Array(data['data'])).map { |vector| decode_embedding(vector['embedding']) }
19
19
  vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
20
20
 
21
21
  Embedding.new(vectors:, model:, input_tokens:)
@@ -11,22 +11,22 @@ module RubyLLM
11
11
  %i[image pdf document 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.format_image(attachment)
20
+ Protocols::ChatCompletions::Media.render_image(attachment)
21
21
  when :pdf, :document
22
- format_document(attachment)
22
+ render_document(attachment)
23
23
  when :text
24
- Protocols::ChatCompletions::Media.format_text_file(attachment)
24
+ Protocols::ChatCompletions::Media.render_text_file(attachment)
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
- def format_document(attachment)
29
+ def render_document(attachment)
30
30
  {
31
31
  type: 'file_url',
32
32
  file_url: {
@@ -14,10 +14,11 @@ module RubyLLM
14
14
  protocol :chat_completions, ChatCompletions
15
15
  protocol :router_chat_completions, Protocols::Perplexity::Router
16
16
  protocol :files, Protocols::Perplexity::Files
17
- protocol :agent_responses, Protocols::Perplexity::Agent
17
+ protocol :agent_responses, Protocols::Perplexity::Agent, batches: Protocols::Perplexity::Batches
18
18
 
19
19
  def api_base
20
- @config.perplexity_api_base || 'https://api.perplexity.ai'
20
+ configured_api_base(:perplexity_api_base, 'https://api.perplexity.ai',
21
+ endpoints: %r{/(?:chat/completions|v1/agent|v1/embeddings)\z})
21
22
  end
22
23
 
23
24
  # Chat runs on the Agent API. Embeddings and the model listing stay on
@@ -58,6 +59,10 @@ module RubyLLM
58
59
  end
59
60
 
60
61
  class << self
62
+ def models_dev_provider_ids # :nodoc:
63
+ %w[perplexity perplexity-agent]
64
+ end
65
+
61
66
  def configuration_options
62
67
  %i[perplexity_api_key perplexity_api_base]
63
68
  end
@@ -66,6 +71,12 @@ module RubyLLM
66
71
  %i[perplexity_api_key]
67
72
  end
68
73
  end
74
+
75
+ private
76
+
77
+ def batch_protocol
78
+ batch_protocol_for_name(:agent_responses)
79
+ end
69
80
  end
70
81
  end
71
82
  end
@@ -7,7 +7,7 @@ module RubyLLM
7
7
  protocol :system_one, Protocols::SystemOne
8
8
 
9
9
  def api_base
10
- @config.typesafe_api_base || 'https://api.typesafe.ai'
10
+ configured_api_base(:typesafe_api_base, 'https://api.typesafe.ai', endpoints: %r{/v1/systemone\z})
11
11
  end
12
12
 
13
13
  def headers
@@ -25,7 +25,11 @@ module RubyLLM
25
25
  raise Error, "#{@provider.name} doesn't support token counting for Claude models"
26
26
  end
27
27
 
28
- def supports_provider_file_references?
28
+ def thinking_block_binding?
29
+ false
30
+ end
31
+
32
+ def provider_file_references?
29
33
  false
30
34
  end
31
35
  end
@@ -9,7 +9,13 @@ module RubyLLM
9
9
  # Google credentials shared by every Vertex AI provider in the process.
10
10
  # googleauth keeps the access token on the credentials object and
11
11
  # refreshes it a minute before it expires, so sharing the object shares
12
- # the token.
12
+ # the token. With a token timeout, token requests go through the
13
+ # middleware and adapter of Faraday's default connection, as googleauth's
14
+ # own would, with that timeout and its proxy. Faraday 1 drops a
15
+ # builder's adapter when it duplicates a builder without middleware, so
16
+ # the builder is shared, not duplicated. The connection is built for
17
+ # each token fetch, so it takes the proxy the default connection would
18
+ # use at that moment, from its settings or the environment.
13
19
  class Credentials # :nodoc:
14
20
  CACHE = Support::ProcessCache.new
15
21
 
@@ -17,7 +23,8 @@ module RubyLLM
17
23
 
18
24
  def self.for(config)
19
25
  key = config.vertexai_service_account_key
20
- CACHE.fetch([source(key), SCOPES]) { new(key) }
26
+ timeout = config.vertexai_token_timeout
27
+ CACHE.fetch([source(key), SCOPES, timeout]) { new(key, token_timeout: timeout) }
21
28
  end
22
29
 
23
30
  def self.cache
@@ -31,7 +38,8 @@ module RubyLLM
31
38
  end
32
39
  private_class_method :source
33
40
 
34
- def initialize(service_account_key)
41
+ def initialize(service_account_key, token_timeout: nil)
42
+ @token_timeout = token_timeout
35
43
  @authorizer = build(service_account_key)
36
44
  @lock = Mutex.new
37
45
  end
@@ -44,6 +52,22 @@ module RubyLLM
44
52
  private
45
53
 
46
54
  def build(service_account_key)
55
+ authorizer = google_credentials(service_account_key)
56
+ return authorizer unless @token_timeout && authorizer.respond_to?(:configure_connection)
57
+
58
+ authorizer.configure_connection(connection_builder: method(:token_connection))
59
+ authorizer
60
+ end
61
+
62
+ def token_connection
63
+ default = Faraday.default_connection
64
+ connection = Faraday.new(builder: default.builder, headers: default.headers, params: default.params,
65
+ ssl: default.ssl, request: default.options.merge(timeout: @token_timeout))
66
+ connection.proxy = default.proxy_for_request(@authorizer.token_credential_uri.to_s)
67
+ connection
68
+ end
69
+
70
+ def google_credentials(service_account_key)
47
71
  require 'googleauth'
48
72
  if service_account_key
49
73
  ::Google::Auth::ServiceAccountCredentials.make_creds(
@@ -23,7 +23,7 @@ module RubyLLM
23
23
  raise ArgumentError, "#{model} takes task instructions and titles in the text" if task_type || title
24
24
 
25
25
  payload = {
26
- content: { parts: Protocols::Gemini::Media.format_content(text.is_a?(Array) ? text.first : text, with) },
26
+ content: { parts: Protocols::Gemini::Media.render_content(text.is_a?(Array) ? text.first : text, with) },
27
27
  outputDimensionality: dimensions
28
28
  }.compact
29
29
  Support::Utils.deep_merge(payload, provider_options)
@@ -11,12 +11,11 @@ module RubyLLM
11
11
  "#{@provider.model_path(model)}:predict"
12
12
  end
13
13
 
14
- def supports_embedding_media?
14
+ def embedding_media?
15
15
  false
16
16
  end
17
17
 
18
- # rubocop:disable-next Lint/UnusedMethodArgument
19
- def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
18
+ def render_embedding_payload(text, dimensions:, task_type: nil, title: nil, provider_options: {}, **)
20
19
  instances = [text].flatten.map do |t|
21
20
  { content: t.to_s, task_type: task_type, title: title }.compact
22
21
  end
@@ -48,7 +48,7 @@ module RubyLLM
48
48
  @provider.model_path(model_id)
49
49
  end
50
50
 
51
- def images_url(with: nil, mask: nil) # rubocop:disable Lint/UnusedMethodArgument
51
+ def images_url(**)
52
52
  id = model_id(@model)
53
53
 
54
54
  "#{@provider.model_path(id)}:#{image_endpoint_action(id)}"
@@ -10,6 +10,7 @@ module RubyLLM
10
10
  include Providers::Mistral::Embeddings
11
11
  include Providers::Mistral::Media
12
12
  include Providers::Mistral::Models
13
+ include Providers::Mistral::OCR
13
14
 
14
15
  # The Mistral model families Vertex AI serves directly. Shared by the
15
16
  # registry (which models to list) and protocol_for (where to route them).
@@ -22,6 +23,10 @@ module RubyLLM
22
23
  def stream_url
23
24
  "#{@provider.model_path(@model.id, publisher: 'mistralai')}:streamRawPredict"
24
25
  end
26
+
27
+ def ocr_url
28
+ completion_url
29
+ end
25
30
  end
26
31
  end
27
32
  end
@@ -29,6 +29,10 @@ module RubyLLM
29
29
  text-multilingual-embedding-002
30
30
  ].freeze
31
31
 
32
+ # Cloud Text-to-Speech voice models, which no publisher catalog lists.
33
+ # Each id is the model part of its voice names: en-US-Chirp3-HD-Charon.
34
+ TEXT_TO_SPEECH_MODELS = { 'Chirp3-HD' => 'Chirp 3: HD' }.freeze
35
+
32
36
  # Every publisher with models Vertex AI serves as a service. The rest
33
37
  # of the Model Garden is deploy-it-yourself and not callable directly.
34
38
  PUBLISHERS = %w[google anthropic mistralai meta deepseek-ai qwen openai moonshotai zai-org].freeze
@@ -57,7 +61,7 @@ module RubyLLM
57
61
  models = fetched.uniq(&:id)
58
62
  log_catalog(counts, models)
59
63
 
60
- models + build_known_models(models.map(&:id))
64
+ models + build_known_models(models.map(&:id)) + text_to_speech_models
61
65
  end
62
66
 
63
67
  private
@@ -77,7 +81,7 @@ module RubyLLM
77
81
  def connection_for(location)
78
82
  return @connection if location == configured_location
79
83
 
80
- Transport::Connection.new(@provider, @config, api_base: @provider.api_base_for(location))
84
+ @provider.signed_connection(@provider.api_base_for(location))
81
85
  end
82
86
 
83
87
  def log_catalog(counts, models)
@@ -178,6 +182,24 @@ module RubyLLM
178
182
  end
179
183
  end
180
184
 
185
+ def text_to_speech_models
186
+ TEXT_TO_SPEECH_MODELS.map do |model_id, name|
187
+ Model.new(
188
+ id: model_id,
189
+ name:,
190
+ provider: @provider.slug,
191
+ family: 'chirp',
192
+ created_at: nil,
193
+ context_window: nil,
194
+ max_output_tokens: nil,
195
+ modalities: { input: %w[text], output: %w[audio] },
196
+ capabilities: %w[speech_generation],
197
+ pricing: nil,
198
+ metadata: { source: 'cloud_text_to_speech' }
199
+ )
200
+ end
201
+ end
202
+
181
203
  def build_model_from_api_data(model_data, model_id)
182
204
  Model.new(
183
205
  id: model_id,
@@ -23,7 +23,7 @@ module RubyLLM
23
23
  def render_transcription_payload(attachment, language:, speaker_names:, provider_options:, prompt:, **)
24
24
  config = { languageCodes: language && Array(language), customVocabulary: prompt && Array(prompt),
25
25
  diarization: speaker_names && true }.compact
26
- payload = { contents: [{ role: 'user', parts: [format_audio_part(attachment)] }],
26
+ payload = { contents: [{ role: 'user', parts: [render_audio_part(attachment)] }],
27
27
  generationConfig: { audioTranscriptionConfig: config } }
28
28
  Support::Utils.deep_merge(payload, provider_options)
29
29
  end
@@ -38,7 +38,7 @@ module RubyLLM
38
38
  raise_if_transcription_blocked(data, text, response)
39
39
  words = segments.flat_map { |segment| segment['words'] }
40
40
  RubyLLM::Transcription.new(text:, model:, segments: segments.empty? ? nil : segments,
41
- words: words.empty? ? nil : words, **extract_usage(data))
41
+ words: words.empty? ? nil : words, **parse_usage(data))
42
42
  end
43
43
 
44
44
  def parse_transcription_segment(part)
@@ -10,7 +10,7 @@ module RubyLLM
10
10
  end
11
11
 
12
12
  def video_job_url(job)
13
- "#{job.id.split('/operations/').first}:fetchPredictOperation"
13
+ "#{video_operation_name(job).split('/operations/').first}:fetchPredictOperation"
14
14
  end
15
15
 
16
16
  def refresh_video_job(job)
@@ -4,6 +4,8 @@ module RubyLLM
4
4
  module Providers
5
5
  # Google Vertex AI implementation
6
6
  class VertexAI < Provider
7
+ include Protocols::Gemini::RetryInfo
8
+
7
9
  protocol :gemini, VertexAI::Gemini, batches: VertexAI::Gemini::Batches
8
10
  protocol :anthropic, VertexAI::Anthropic, batches: VertexAI::Anthropic::Batches
9
11
  protocol :mistral, VertexAI::Mistral
@@ -13,6 +15,7 @@ module RubyLLM
13
15
  protocol :transcription, VertexAI::Transcription
14
16
  protocol :live_transcription, VertexAI::LiveTranscription
15
17
  protocol :ranking, Protocols::VertexAI::Ranking
18
+ protocol :text_to_speech, Protocols::VertexAI::TextToSpeech
16
19
  protocol :research, Protocols::VertexAI::Research
17
20
  protocol :files, Protocols::VertexAI::Files
18
21
 
@@ -47,6 +50,19 @@ module RubyLLM
47
50
  def models_dev_model_id(id)
48
51
  id&.split('@')&.first
49
52
  end
53
+
54
+ def open_telemetry_name # :nodoc:
55
+ 'gcp.vertex_ai'
56
+ end
57
+
58
+ def models_dev_provider_ids # :nodoc:
59
+ %w[google-vertex]
60
+ end
61
+ end
62
+
63
+ def initialize(config) # :nodoc:
64
+ super
65
+ @connection = signed_connection(api_base)
50
66
  end
51
67
 
52
68
  # Vertex AI hosts models from several publishers, each speaking its
@@ -55,8 +71,8 @@ module RubyLLM
55
71
  def protocol_for(model, operation: nil, **)
56
72
  return protocols[:ranking] if operation == :rerank
57
73
 
58
- transcription = transcription_protocol_for(model.id) if operation == :transcribe
59
- return transcription if transcription
74
+ audio = audio_protocol_for(model.id, operation)
75
+ return audio if audio
60
76
 
61
77
  if operation == :embed && %w[gemini-embedding-2 gemini-embedding-2-preview].include?(model.id)
62
78
  return protocols[:embed_content]
@@ -124,12 +140,15 @@ module RubyLLM
124
140
  end
125
141
 
126
142
  def api_base_for(location)
127
- return @config.vertexai_api_base if @config.vertexai_api_base
143
+ if @config.vertexai_api_base
144
+ return configured_api_base(:vertexai_api_base, example: 'https://LOCATION-aiplatform.googleapis.com/v1beta1',
145
+ endpoints: %r{/projects/.*\z})
146
+ end
128
147
 
129
- if location.to_s == 'global'
130
- 'https://aiplatform.googleapis.com/v1beta1'
131
- else
132
- "https://#{location}-aiplatform.googleapis.com/v1beta1"
148
+ case location.to_s
149
+ when 'global' then 'https://aiplatform.googleapis.com/v1beta1'
150
+ when 'eu', 'us' then "https://aiplatform.#{location}.rep.googleapis.com/v1beta1"
151
+ else "https://#{location}-aiplatform.googleapis.com/v1beta1"
133
152
  end
134
153
  end
135
154
 
@@ -140,11 +159,25 @@ module RubyLLM
140
159
 
141
160
  def ranking_connection # :nodoc:
142
161
  base = @config.vertexai_ranking_api_base || 'https://discoveryengine.googleapis.com/v1'
143
- @ranking_connection ||= Transport::Connection.new(
144
- self, @config, api_base: base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
162
+ @ranking_connection ||= signed_connection(
163
+ base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
164
+ )
165
+ end
166
+
167
+ # Cloud Text-to-Speech answers at the multi-region or region named by
168
+ # vertexai_location, never at the global endpoint unless asked for it.
169
+ def text_to_speech_connection # :nodoc:
170
+ @text_to_speech_connection ||= signed_connection(
171
+ text_to_speech_api_base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
145
172
  )
146
173
  end
147
174
 
175
+ # Signs each attempt as it is sent, so a retry never replays an access
176
+ # token that expired while the previous attempt waited.
177
+ def signed_connection(base_url, headers: {}) # :nodoc:
178
+ Transport::Connection.new(self, @config, api_base: base_url, headers:, signer: ->(_env) { self.headers })
179
+ end
180
+
148
181
  # The rescue can't name Google::Auth::AuthorizationError directly:
149
182
  # when googleauth is missing, evaluating the constant would replace
150
183
  # the helpful install error with a NameError.
@@ -170,6 +203,7 @@ module RubyLLM
170
203
  vertexai_batch_gcs_uri
171
204
  vertexai_ranking_api_base
172
205
  vertexai_ranking_config
206
+ vertexai_token_timeout
173
207
  ]
174
208
  end
175
209
 
@@ -180,6 +214,19 @@ module RubyLLM
180
214
 
181
215
  private
182
216
 
217
+ def text_to_speech_api_base
218
+ location = @config.vertexai_location.to_s
219
+ host = location == 'global' ? 'texttospeech.googleapis.com' : "#{location}-texttospeech.googleapis.com"
220
+ "https://#{host}/v1"
221
+ end
222
+
223
+ def audio_protocol_for(id, operation)
224
+ case operation
225
+ when :transcribe then transcription_protocol_for(id)
226
+ when :speak then protocols[:text_to_speech] if Protocols::VertexAI::TextToSpeech::MODELS.include?(id)
227
+ end
228
+ end
229
+
183
230
  def transcription_protocol_for(id)
184
231
  case id
185
232
  when 'gemini-3.5-transcribe-preview' then protocols[:transcription]
@@ -6,7 +6,7 @@ module RubyLLM
6
6
  # Chat implementation for xAI
7
7
  # https://docs.x.ai/docs/api-reference#chat-completions
8
8
  module Chat
9
- def format_role(role)
9
+ def render_role(role)
10
10
  role.to_s
11
11
  end
12
12
 
@@ -16,8 +16,8 @@ module RubyLLM
16
16
  )
17
17
  end
18
18
 
19
- def format_content(content, attachments = [])
20
- Protocols::ChatCompletions::Media.format_content(
19
+ def render_content(content, attachments = [])
20
+ Protocols::ChatCompletions::Media.render_content(
21
21
  content,
22
22
  attachments,
23
23
  document_attachments: :none,
@@ -76,13 +76,18 @@ module RubyLLM
76
76
  def parse_batch_status(raw_status, completed:)
77
77
  return :pending unless completed
78
78
 
79
- raw_status == 'failed' ? :failed : :succeeded
79
+ case raw_status
80
+ when 'failed' then :failed
81
+ when 'cancelled' then :cancelled
82
+ else :succeeded
83
+ end
80
84
  end
81
85
 
82
86
  def xai_batch_status(state, completed:)
83
87
  return 'failed' unless state['error'].to_s.empty?
88
+ return 'processing' unless completed
84
89
 
85
- completed ? 'completed' : 'processing'
90
+ state['num_cancelled'].to_i.positive? ? 'cancelled' : 'completed'
86
91
  end
87
92
 
88
93
  def completed_batch_state?(state)
@@ -96,7 +101,7 @@ module RubyLLM
96
101
  result.dig('response', 'chat_get_completion')
97
102
 
98
103
  if body
99
- [index, parse_completion_body(body, raw: body)]
104
+ parsed_batch_result(index, request_id, body) { parse_completion_body(body, raw: body) }
100
105
  else
101
106
  [index, nil, batch_failure(request_id, batch_error_message(result))]
102
107
  end
@@ -35,7 +35,7 @@ module RubyLLM
35
35
  end
36
36
 
37
37
  def image_references(sources)
38
- Array(sources).filter_map do |source|
38
+ Support::Utils.to_safe_array(sources).filter_map do |source|
39
39
  next if blank_attachment?(source)
40
40
 
41
41
  { type: 'image_url', url: image_reference_url(source) }
@@ -43,7 +43,7 @@ module RubyLLM
43
43
  end
44
44
 
45
45
  def image_reference_url(source)
46
- attachment = Attachment.new(source, config: @config)
46
+ attachment = source.is_a?(Attachment) ? source : Attachment.new(source, config: @config)
47
47
  return attachment.provider_file_id if attachment.provider_file?
48
48
  return attachment.source.to_s if attachment.url?
49
49
 
@@ -68,12 +68,17 @@ module RubyLLM
68
68
  data: image_data['b64_json'],
69
69
  mime_type: image_data['mime_type'] || 'image/png',
70
70
  model: model,
71
- usage: index.zero? ? (data['usage'] || {}) : {}
71
+ usage: index.zero? ? image_usage(data) : {}
72
72
  )
73
73
  end
74
74
  end
75
75
 
76
- def validate_paint_inputs!(with:, mask:) # rubocop:disable Lint/UnusedMethodArgument
76
+ def image_usage(data)
77
+ usage = data['usage'] || {}
78
+ usage.merge('cost' => reported_cost(usage)).compact
79
+ end
80
+
81
+ def validate_paint_inputs!(mask:, **)
77
82
  raise Error, 'xAI image editing does not support a mask parameter' if mask
78
83
  end
79
84
 
@@ -4,8 +4,8 @@ module RubyLLM
4
4
  module Providers
5
5
  class XAI
6
6
  # Models metadata for xAI list models. The list endpoint omits the
7
- # model-less TTS and STT services, so those are appended under the
8
- # grok-tts and grok-stt names from xAI's model catalog.
7
+ # TTS and STT services, so those are appended under the grok-tts and
8
+ # grok-stt names from xAI's model catalog.
9
9
  module Models
10
10
  module_function
11
11
 
@@ -33,6 +33,16 @@ module RubyLLM
33
33
  SERVER_TOOL_ALIASES
34
34
  end
35
35
 
36
+ # xAI answers tool_search and defer_loading with 403 for every
37
+ # account outside its alpha, so Chat brings its own search.
38
+ def tool_search?
39
+ false
40
+ end
41
+
42
+ def document_urls?
43
+ true
44
+ end
45
+
36
46
  def parse_usage(usage)
37
47
  super.merge(reported_cost: reported_cost(usage))
38
48
  end
@@ -9,11 +9,11 @@ module RubyLLM
9
9
  module Speech
10
10
  module_function
11
11
 
12
- def speech_url(model:) # rubocop:disable Lint/UnusedMethodArgument
12
+ def speech_url(**)
13
13
  'tts'
14
14
  end
15
15
 
16
- def render_speech_payload(input, model:, voice:, format:, provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
16
+ def render_speech_payload(input, voice:, format:, provider_options: {}, **)
17
17
  payload = {
18
18
  text: input,
19
19
  voice_id: voice,