ruby_llm 1.16.0 → 2.0.0.rc2

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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  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 -8
  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 +23 -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 +101 -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 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  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 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  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 +1127 -198
  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 +261 -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 +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  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 +153 -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 +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -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 +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  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 +548 -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 +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -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/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  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 +15 -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 +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. 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
@@ -5,47 +5,157 @@ require 'stringio'
5
5
  module RubyLLM
6
6
  module Providers
7
7
  # Google Vertex AI implementation
8
- class VertexAI < Gemini
9
- include VertexAI::Chat
10
- include VertexAI::Streaming
11
- include VertexAI::Embeddings
12
- include VertexAI::Models
13
- 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
14
20
 
15
21
  SCOPES = [
16
22
  'https://www.googleapis.com/auth/cloud-platform',
17
23
  'https://www.googleapis.com/auth/generative-language.retriever'
18
24
  ].freeze
19
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
+
20
71
  def initialize(config)
21
72
  super
22
73
  @authorizer = nil
23
74
  end
24
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
+
25
110
  def api_base
111
+ api_base_for(@config.vertexai_location)
112
+ end
113
+
114
+ def api_base_for(location)
26
115
  return @config.vertexai_api_base if @config.vertexai_api_base
27
116
 
28
- if @config.vertexai_location.to_s == 'global'
117
+ if location.to_s == 'global'
29
118
  'https://aiplatform.googleapis.com/v1beta1'
30
119
  else
31
- "https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
120
+ "https://#{location}-aiplatform.googleapis.com/v1beta1"
32
121
  end
33
122
  end
34
123
 
35
- def headers
36
- if defined?(VCR) && !VCR.current_cassette.recording?
37
- { 'Authorization' => 'Bearer test-token' }
38
- else
39
- initialize_authorizer unless @authorizer
40
- @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
41
133
  end
42
- rescue Google::Auth::AuthorizationError => e
43
- 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}"
44
146
  end
45
147
 
46
148
  class << self
47
149
  def configuration_options
48
- %i[vertexai_project_id vertexai_location vertexai_service_account_key vertexai_api_base]
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
+ ]
49
159
  end
50
160
 
51
161
  def configuration_requirements
@@ -55,6 +165,13 @@ module RubyLLM
55
165
 
56
166
  private
57
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
+
58
175
  def initialize_authorizer
59
176
  require 'googleauth'
60
177
  @authorizer =
@@ -70,6 +187,32 @@ module RubyLLM
70
187
  raise Error,
71
188
  'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
72
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
73
216
  end
74
217
  end
75
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
@@ -10,9 +10,10 @@ module RubyLLM
10
10
  role.to_s
11
11
  end
12
12
 
13
- def format_content(content)
14
- OpenAI::Media.format_content(
13
+ def format_content(content, attachments = [])
14
+ Protocols::ChatCompletions::Media.format_content(
15
15
  content,
16
+ attachments,
16
17
  document_attachments: :none,
17
18
  image_attachments: true,
18
19
  audio_attachments: false
@@ -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