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
@@ -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,
@@ -4,30 +4,34 @@ module RubyLLM
4
4
  module Providers
5
5
  class XAI
6
6
  # Transcription dialect for the xAI Voice API. The /v1/stt endpoint is
7
- # model-less multipart with the audio file last in the form; giving
8
- # speaker names turns on diarization, which labels each word with a
9
- # numeric speaker index.
7
+ # multipart with the audio file last in the form; requesting speaker
8
+ # labels turns on diarization, which labels each word with a numeric
9
+ # speaker index.
10
10
  module Transcription
11
+ # grok-stt is the registry's name for the service; the API has its own.
12
+ MODEL_IDS = { 'grok-stt' => 'grok-voice-transcribe-2.0' }.freeze
13
+
11
14
  def render_transcription_options(timestamps:, **)
12
15
  return {} if timestamps.nil? || timestamps == :word
13
16
 
14
17
  raise ArgumentError, 'xAI transcription timestamps must be word'
15
18
  end
16
19
 
20
+ def rejected_options(operation)
21
+ operation == :transcribe ? %i[format temperature prompt speaker_references named_speakers] : super
22
+ end
23
+
17
24
  module_function
18
25
 
19
26
  def transcription_url
20
27
  'stt'
21
28
  end
22
29
 
23
- # rubocop:disable-next Lint/UnusedMethodArgument
24
- def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
25
- speaker_references: nil, provider_options: {}, prompt: nil,
26
- temperature: nil)
27
- payload = {}
30
+ def render_transcription_payload(file_part, model:, language:, speaker_names: nil, provider_options: {}, **)
31
+ payload = { model: MODEL_IDS.fetch(model, model) }
28
32
  payload[:language] = language if language
29
33
  payload[:diarize] = true if speaker_names
30
- payload.merge(provider_options).merge(file: file_part)
34
+ payload.merge(provider_options.transform_keys(&:to_sym)).except(:file).merge(file: file_part)
31
35
  end
32
36
 
33
37
  def parse_transcription_response(response, model:)
@@ -38,6 +42,7 @@ module RubyLLM
38
42
  model: model,
39
43
  language: data['language'],
40
44
  duration: data['duration'],
45
+ audio_seconds: data['duration'],
41
46
  segments: data['channels'],
42
47
  words: data['words']
43
48
  )
@@ -45,7 +45,7 @@ module RubyLLM
45
45
  "videos/#{job.id}"
46
46
  end
47
47
 
48
- def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
48
+ def parse_video_job_status(response, **)
49
49
  body = response.body
50
50
  state = case body['status']
51
51
  when 'done' then { status: :completed }
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  protocol :files, Protocols::XAI::Files
10
10
 
11
11
  def api_base
12
- @config.xai_api_base || 'https://api.x.ai/v1'
12
+ configured_api_base(:xai_api_base, 'https://api.x.ai/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
13
13
  end
14
14
 
15
15
  def headers
@@ -23,6 +23,14 @@ module RubyLLM
23
23
  end
24
24
 
25
25
  class << self
26
+ def open_telemetry_name # :nodoc:
27
+ 'x_ai'
28
+ end
29
+
30
+ def models_dev_provider_ids # :nodoc:
31
+ %w[xai]
32
+ end
33
+
26
34
  def capabilities
27
35
  XAI::Capabilities
28
36
  end