ruby_llm 1.15.0 → 2.0.0.rc1

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 (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -6
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1208 -150
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +274 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +155 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +540 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/protocols/chat_completions/media.rb +121 -0
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +17 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -3,12 +3,57 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class VertexAI
6
- # Vertex AI specific helpers for audio transcription
7
- module Transcription
6
+ class Transcription < VertexAI::Gemini # :nodoc: all
7
+ def render_transcription_options(timestamps:, **)
8
+ return {} if timestamps.nil?
9
+ raise ArgumentError, 'Vertex AI transcription timestamps must be word' unless timestamps == :word
10
+
11
+ { generationConfig: { audioTranscriptionConfig: { wordTimestamp: true } } }
12
+ end
13
+
14
+ include Protocols::Gemini::FileTranscription
15
+
16
+ def validate_transcription_request(...)
17
+ super
18
+ return if @config.vertexai_location == 'global'
19
+
20
+ raise ArgumentError, 'Vertex AI dedicated transcription requires vertexai_location = "global"'
21
+ end
22
+
23
+ def render_transcription_payload(attachment, language:, speaker_names:, provider_options:, prompt:, **)
24
+ config = { languageCodes: language && Array(language), customVocabulary: prompt && Array(prompt),
25
+ diarization: speaker_names && true }.compact
26
+ payload = { contents: [{ role: 'user', parts: [format_audio_part(attachment)] }],
27
+ generationConfig: { audioTranscriptionConfig: config } }
28
+ Support::Utils.deep_merge(payload, provider_options)
29
+ end
30
+
8
31
  private
9
32
 
10
- def transcription_url(model)
11
- "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{model}:generateContent" # rubocop:disable Layout/LineLength
33
+ def parse_transcription_response(response, model:)
34
+ data = response.body
35
+ parts = data.dig('candidates', 0, 'content', 'parts') || []
36
+ segments = parts.filter_map { |part| parse_transcription_segment(part) }
37
+ text = parts.filter_map { |part| part['text'] || part.dig('audioTranscription', 'text') }.join
38
+ words = segments.flat_map { |segment| segment['words'] }
39
+ RubyLLM::Transcription.new(text:, model:, segments: segments.empty? ? nil : segments,
40
+ words: words.empty? ? nil : words, **extract_usage(data))
41
+ end
42
+
43
+ def parse_transcription_segment(part)
44
+ transcription = part['audioTranscription']
45
+ return unless transcription
46
+
47
+ { 'text' => part['text'] || transcription['text'], 'speaker' => transcription['speakerLabel'],
48
+ 'words' => Array(transcription['words']).map do |word|
49
+ parse_transcription_word(word, transcription)
50
+ end }.compact
51
+ end
52
+
53
+ def parse_transcription_word(word, transcription)
54
+ { 'word' => word['word'], 'speaker' => transcription['speakerLabel'],
55
+ 'start' => word['startOffset'] && Float(word['startOffset'].delete_suffix('s')),
56
+ 'end' => word['endOffset'] && Float(word['endOffset'].delete_suffix('s')) }.compact
12
57
  end
13
58
  end
14
59
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI
6
+ # Veo prediction jobs with inline or Cloud Storage video output.
7
+ module Videos
8
+ def video_url
9
+ "#{@provider.model_path(model_id(@model))}:predictLongRunning"
10
+ end
11
+
12
+ def video_job_url(job)
13
+ "#{job.id.split('/operations/').first}:fetchPredictOperation"
14
+ end
15
+
16
+ def refresh_video_job(job)
17
+ response = @connection.post video_job_url(job), { operationName: job.id }
18
+ parse_video_job_status(response, job:)
19
+ end
20
+
21
+ def download_video(job)
22
+ video = generated_video(job.raw)
23
+ raise Error, 'Vertex AI returned no video' unless video
24
+
25
+ data = if video['bytesBase64Encoded']
26
+ Base64.decode64(video['bytesBase64Encoded'])
27
+ else
28
+ @provider.download_file(video.fetch('gcsUri'))
29
+ end
30
+
31
+ Video.new(data:, mime_type: video['mimeType'] || 'video/mp4', model: job.model, raw: job.raw)
32
+ end
33
+
34
+ private
35
+
36
+ def render_video_extension(source)
37
+ uri = source.respond_to?(:uri) ? source.uri : source
38
+ return { gcsUri: uri, mimeType: 'video/mp4' } if uri.is_a?(String) && uri.start_with?('gs://')
39
+
40
+ video = video_extension_attachment(source)
41
+ { bytesBase64Encoded: video.encoded, mimeType: video.mime_type }
42
+ end
43
+
44
+ def render_video_image(image)
45
+ { bytesBase64Encoded: image.encoded, mimeType: image.mime_type }
46
+ end
47
+
48
+ def generated_video(body)
49
+ video = body.dig('response', 'videos', 0)
50
+ video if video && %w[bytesBase64Encoded gcsUri].any? { |key| !video[key].to_s.empty? }
51
+ end
52
+
53
+ def filtered_video_failure(body)
54
+ error = Array(body.dig('response', 'raiMediaFilteredReasons')).join(' ')
55
+ error = 'Vertex AI returned no video' if error.empty?
56
+ { status: :failed, raw: body, error: }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,47 +1,161 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stringio'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  # Google Vertex AI implementation
6
- class VertexAI < Gemini
7
- include VertexAI::Chat
8
- include VertexAI::Streaming
9
- include VertexAI::Embeddings
10
- include VertexAI::Models
11
- include VertexAI::Transcription
8
+ class VertexAI < Provider
9
+ protocol :gemini, VertexAI::Gemini, batches: VertexAI::Gemini::Batches
10
+ protocol :anthropic, VertexAI::Anthropic, batches: VertexAI::Anthropic::Batches
11
+ protocol :mistral, VertexAI::Mistral
12
+ protocol :chat_completions, VertexAI::ChatCompletions, batches: VertexAI::ChatCompletions::Batches
13
+ protocol :embed_content, VertexAI::EmbedContent
14
+ protocol :embedding_prediction, Protocols::VertexAI::EmbeddingPrediction
15
+ protocol :transcription, VertexAI::Transcription
16
+ protocol :live_transcription, VertexAI::LiveTranscription
17
+ protocol :ranking, Protocols::VertexAI::Ranking
18
+ protocol :research, Protocols::VertexAI::Research
19
+ protocol :files, Protocols::VertexAI::Files
12
20
 
13
21
  SCOPES = [
14
22
  'https://www.googleapis.com/auth/cloud-platform',
15
23
  'https://www.googleapis.com/auth/generative-language.retriever'
16
24
  ].freeze
17
25
 
26
+ class << self
27
+ def capabilities
28
+ VertexAI::Capabilities
29
+ end
30
+
31
+ def models_dev_alias(...)
32
+ VertexAI::Models.models_dev_alias(...)
33
+ end
34
+
35
+ # models.dev pins Vertex AI models to a version (claude-haiku-4-5@20251001);
36
+ # Vertex AI serves them by bare name.
37
+ def models_dev_model_id(id)
38
+ id&.split('@')&.first
39
+ end
40
+ end
41
+
42
+ # Vertex AI hosts models from several publishers, each speaking its
43
+ # native protocol. Publisher-prefixed ids are MaaS models served
44
+ # through the OpenAI-compatible endpoint.
45
+ def protocol_for(model, operation: nil, **)
46
+ return protocols[:ranking] if operation == :rerank
47
+
48
+ transcription = transcription_protocol_for(model.id) if operation == :transcribe
49
+ return transcription if transcription
50
+
51
+ if operation == :embed && %w[gemini-embedding-2 gemini-embedding-2-preview].include?(model.id)
52
+ return protocols[:embed_content]
53
+ end
54
+
55
+ case model.id
56
+ when %r{/} then protocols[:chat_completions]
57
+ when /\Aclaude/ then protocols[:anthropic]
58
+ when VertexAI::Mistral::MODELS then protocols[:mistral]
59
+ else super
60
+ end
61
+ end
62
+
63
+ def location_path
64
+ "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}"
65
+ end
66
+
67
+ def model_path(model, publisher: 'google')
68
+ "#{location_path}/publishers/#{publisher}/models/#{model}"
69
+ end
70
+
18
71
  def initialize(config)
19
72
  super
20
73
  @authorizer = nil
21
74
  end
22
75
 
76
+ def batch_protocol
77
+ batch_protocol_for_name(:gemini)
78
+ end
79
+
80
+ def batch_protocol_for(requests)
81
+ kinds = requests.map { |request| request.key?(:text) }.uniq
82
+ raise Error, 'Vertex AI batches take chat or embedding requests, not both' unless kinds.size == 1
83
+ return protocols[:embedding_prediction] if kinds.first
84
+
85
+ models = requests.map { |request| request.fetch(:model) }.uniq
86
+ raise Error, 'vertexai batch requests must use one model per submission' unless models.one?
87
+
88
+ protocol_name = batch_protocol_name_for(models.first)
89
+ protocol = batch_protocol_for_name(protocol_name)
90
+ return protocol if protocol
91
+
92
+ raise Error, 'vertexai batch requests currently support Gemini, Anthropic, and MaaS chat models'
93
+ end
94
+ private :batch_protocol, :batch_protocol_for
95
+
96
+ def find_batch(id)
97
+ batch = super
98
+ protocol = batch_protocol_for_model_path(batch[:model])
99
+
100
+ protocol ? batch.merge(batch_protocol: protocol) : batch
101
+ end
102
+
103
+ def batch_cost_multiplier(model:, component:)
104
+ return if model.id.include?('/')
105
+ return 1 if !model.id.start_with?('claude') && %i[cache_read cache_write].include?(component)
106
+
107
+ 0.5
108
+ end
109
+
23
110
  def api_base
24
- if @config.vertexai_location.to_s == 'global'
111
+ api_base_for(@config.vertexai_location)
112
+ end
113
+
114
+ def api_base_for(location)
115
+ return @config.vertexai_api_base if @config.vertexai_api_base
116
+
117
+ if location.to_s == 'global'
25
118
  'https://aiplatform.googleapis.com/v1beta1'
26
119
  else
27
- "https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
120
+ "https://#{location}-aiplatform.googleapis.com/v1beta1"
28
121
  end
29
122
  end
30
123
 
31
- def headers
32
- if defined?(VCR) && !VCR.current_cassette.recording?
33
- { 'Authorization' => 'Bearer test-token' }
34
- else
35
- initialize_authorizer unless @authorizer
36
- @authorizer.apply({})
124
+ def ranking_config # :nodoc:
125
+ @config.vertexai_ranking_config ||
126
+ "projects/#{@config.vertexai_project_id}/locations/global/rankingConfigs/default_ranking_config"
127
+ end
128
+
129
+ def ranking_connection # :nodoc:
130
+ base = @config.vertexai_ranking_api_base || 'https://discoveryengine.googleapis.com/v1'
131
+ @ranking_connection ||= Transport::Connection.new(self, @config, api_base: base).tap do |connection|
132
+ connection.connection.headers['X-Goog-User-Project'] = @config.vertexai_project_id
37
133
  end
38
- rescue Google::Auth::AuthorizationError => e
39
- raise UnauthorizedError.new(nil, "Invalid Google Cloud credentials for Vertex AI: #{e.message}")
134
+ end
135
+
136
+ # The rescue can't name Google::Auth::AuthorizationError directly:
137
+ # when googleauth is missing, evaluating the constant would replace
138
+ # the helpful install error with a NameError.
139
+ def headers
140
+ initialize_authorizer unless @authorizer
141
+ @authorizer.apply({})
142
+ rescue StandardError => e
143
+ raise unless defined?(Google::Auth::AuthorizationError) && e.is_a?(Google::Auth::AuthorizationError)
144
+
145
+ raise UnauthorizedError, "Invalid Google Cloud credentials for Vertex AI: #{e.message}"
40
146
  end
41
147
 
42
148
  class << self
43
149
  def configuration_options
44
- %i[vertexai_project_id vertexai_location vertexai_service_account_key]
150
+ %i[
151
+ vertexai_project_id
152
+ vertexai_location
153
+ vertexai_service_account_key
154
+ vertexai_api_base
155
+ vertexai_batch_gcs_uri
156
+ vertexai_ranking_api_base
157
+ vertexai_ranking_config
158
+ ]
45
159
  end
46
160
 
47
161
  def configuration_requirements
@@ -51,6 +165,13 @@ module RubyLLM
51
165
 
52
166
  private
53
167
 
168
+ def transcription_protocol_for(id)
169
+ case id
170
+ when 'gemini-3.5-transcribe-preview' then protocols[:transcription]
171
+ when 'gemini-3.5-transcribe-live-preview' then protocols[:live_transcription]
172
+ end
173
+ end
174
+
54
175
  def initialize_authorizer
55
176
  require 'googleauth'
56
177
  @authorizer =
@@ -66,6 +187,32 @@ module RubyLLM
66
187
  raise Error,
67
188
  'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
68
189
  end
190
+
191
+ def batch_protocol_name_for(model)
192
+ case model
193
+ when %r{/} then :chat_completions
194
+ when /\Aclaude/ then :anthropic
195
+ when VertexAI::Mistral::MODELS then :mistral
196
+ else :gemini
197
+ end
198
+ end
199
+
200
+ def batch_protocol_for_model_path(model_path)
201
+ if Protocols::VertexAI::EmbeddingPrediction::MODELS.include?(model_path.to_s.split('/').last)
202
+ return protocols[:embedding_prediction]
203
+ end
204
+
205
+ case model_path.to_s
206
+ when %r{/publishers/google/models/}, %r{\Apublishers/google/models/}
207
+ batch_protocol_for_name(:gemini)
208
+ when %r{/publishers/anthropic/models/}, %r{\Apublishers/anthropic/models/}
209
+ batch_protocol_for_name(:anthropic)
210
+ when %r{/publishers/mistralai/models/}, %r{\Apublishers/mistralai/models/}
211
+ nil
212
+ when %r{/publishers/[^/]+/models/}, %r{\Apublishers/[^/]+/models/}
213
+ batch_protocol_for_name(:chat_completions)
214
+ end
215
+ end
69
216
  end
70
217
  end
71
218
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class XAI
6
+ # Feature capability gaps not represented in upstream model catalogs.
7
+ module Capabilities
8
+ def self.augment(capabilities, model_id:, modalities:)
9
+ return capabilities unless modalities[:output].include?('text')
10
+
11
+ additions = ['streaming']
12
+ additions.push('tool_choice', 'parallel_tool_calls') if model_id == 'grok-4.3'
13
+ capabilities | additions
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -9,6 +9,16 @@ module RubyLLM
9
9
  def format_role(role)
10
10
  role.to_s
11
11
  end
12
+
13
+ def format_content(content, attachments = [])
14
+ Protocols::ChatCompletions::Media.format_content(
15
+ content,
16
+ attachments,
17
+ document_attachments: :none,
18
+ image_attachments: true,
19
+ audio_attachments: false
20
+ )
21
+ end
12
22
  end
13
23
  end
14
24
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class XAI
6
+ class ChatCompletions
7
+ # xAI native batch containers. Requests submit in Responses or Chat
8
+ # Completions shape; results always come back as chat completions.
9
+ module Batches
10
+ include RubyLLM::Batch::Helpers
11
+
12
+ def create_batch(requests)
13
+ batch = @connection.post('batches', { name: "ruby_llm_#{SecureRandom.hex(8)}" },
14
+ idempotent: false).body
15
+ id = batch['batch_id'] || batch['id']
16
+ @connection.post("batches/#{id}/requests", {
17
+ batch_requests: requests.map { |request| xai_batch_request(request) }
18
+ }, idempotent: false)
19
+
20
+ find_batch(id)
21
+ end
22
+
23
+ def find_batch(id)
24
+ parse_batch_response @connection.get("batches/#{id}").body
25
+ end
26
+
27
+ def cancel_batch(id)
28
+ parse_batch_response @connection.post("batches/#{id}:cancel", {}).body
29
+ end
30
+
31
+ def batch_results(id)
32
+ results = []
33
+ token = nil
34
+
35
+ loop do
36
+ response = @connection.get("batches/#{id}/results") do |request|
37
+ request.params[:limit] = 100
38
+ request.params[:pagination_token] = token if token
39
+ end.body
40
+
41
+ page_results = Array(response['results'] || response['batch_results'])
42
+ results.concat(page_results.map { |result| parse_batch_result(result) })
43
+ token = response['pagination_token'] || response['next_page_token']
44
+ break unless token
45
+ end
46
+
47
+ results
48
+ end
49
+
50
+ private
51
+
52
+ def xai_batch_request(request)
53
+ payload = batch_payload(request)
54
+ {
55
+ batch_request_id: request[:custom_id],
56
+ batch_request: { batch_request_type(payload) => payload }
57
+ }
58
+ end
59
+
60
+ def batch_request_type(payload)
61
+ payload.key?(:input) || payload.key?('input') ? :responses : :chat_get_completion
62
+ end
63
+
64
+ def parse_batch_response(data)
65
+ state = data['state'] || {}
66
+ completed = completed_batch_state?(state)
67
+ {
68
+ id: data['batch_id'] || data['id'],
69
+ raw_status: data['state'] ? xai_batch_status(state, completed:) : data['status'],
70
+ completed:,
71
+ request_count: state['num_requests'],
72
+ request_counts: state
73
+ }
74
+ end
75
+
76
+ def parse_batch_status(raw_status, completed:)
77
+ return :pending unless completed
78
+
79
+ raw_status == 'failed' ? :failed : :succeeded
80
+ end
81
+
82
+ def xai_batch_status(state, completed:)
83
+ return 'failed' unless state['error'].to_s.empty?
84
+
85
+ completed ? 'completed' : 'processing'
86
+ end
87
+
88
+ def completed_batch_state?(state)
89
+ state['num_requests'].to_i.positive? && state['num_pending'].to_i.zero?
90
+ end
91
+
92
+ def parse_batch_result(result)
93
+ request_id = result['batch_request_id'] || result['custom_id']
94
+ index = batch_result_index(request_id)
95
+ body = result.dig('batch_result', 'response', 'chat_get_completion') ||
96
+ result.dig('response', 'chat_get_completion')
97
+
98
+ if body
99
+ [index, parse_completion_body(body, raw: body)]
100
+ else
101
+ [index, nil, batch_failure(request_id, batch_error_message(result))]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class XAI
6
+ # xAI's dialect of the Chat Completions API.
7
+ class ChatCompletions < Protocols::ChatCompletions
8
+ include XAI::Chat
9
+ include XAI::ReportedCost
10
+ include XAI::Images
11
+ include XAI::Models
12
+ include XAI::Speech
13
+ include XAI::Transcription
14
+ include Protocols::XAI::Tokenization
15
+ include Protocols::XAI::StreamingTranscription
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class XAI
6
+ # Image generation and editing for the xAI API. Generation rejects the
7
+ # size parameter. Editing takes JSON image references (URL, data URI,
8
+ # or file id) instead of multipart uploads, up to three per request,
9
+ # and has no mask support.
10
+ module Images
11
+ module_function
12
+
13
+ def images_url(with: nil, mask: nil)
14
+ editing?(with, mask) ? 'images/edits' : 'images/generations'
15
+ end
16
+
17
+ def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
18
+ return render_edit_payload(prompt, model:, count:, with:, provider_options:) if editing?(with, mask)
19
+
20
+ RubyLLM.logger.debug { "Ignoring size #{size}. xAI image generation does not support a size parameter." }
21
+ payload = { model: model, prompt: prompt }
22
+ payload[:n] = count if count
23
+
24
+ payload.merge(provider_options)
25
+ end
26
+
27
+ def render_edit_payload(prompt, model:, with:, provider_options:, count: nil)
28
+ payload = {
29
+ model: model,
30
+ prompt: prompt,
31
+ images: image_references(with)
32
+ }
33
+ payload[:n] = count if count
34
+
35
+ payload.merge(provider_options)
36
+ end
37
+
38
+ def image_references(sources)
39
+ Array(sources).filter_map do |source|
40
+ next if blank_attachment?(source)
41
+
42
+ { type: 'image_url', url: image_reference_url(source) }
43
+ end
44
+ end
45
+
46
+ def image_reference_url(source)
47
+ attachment = Attachment.new(source, config: @config)
48
+ return attachment.provider_file_id if attachment.provider_file?
49
+ return attachment.source.to_s if attachment.url?
50
+
51
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
52
+
53
+ attachment.for_llm
54
+ end
55
+
56
+ def parse_image_response(response, model:)
57
+ parse_image_responses(response, model:).first
58
+ end
59
+
60
+ def parse_image_responses(response, model:)
61
+ data = response.body
62
+ entries = Array(data['data'])
63
+
64
+ raise Error, 'Unexpected response format from xAI image API' if entries.empty?
65
+
66
+ entries.map.with_index do |image_data, index|
67
+ Image.new(
68
+ url: image_data['url'],
69
+ data: image_data['b64_json'],
70
+ mime_type: image_data['mime_type'] || 'image/png',
71
+ model: model,
72
+ usage: index.zero? ? (data['usage'] || {}) : {}
73
+ )
74
+ end
75
+ end
76
+
77
+ def validate_paint_inputs!(with:, mask:) # rubocop:disable Lint/UnusedMethodArgument
78
+ raise Error, 'xAI image editing does not support a mask parameter' if mask
79
+ end
80
+
81
+ def editing?(with, mask)
82
+ Protocols::ChatCompletions::Images.editing?(with, mask)
83
+ end
84
+
85
+ def blank_attachment?(value)
86
+ Protocols::ChatCompletions::Images.blank_attachment?(value)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end