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,16 +3,14 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Anthropic
6
- # Provider-level capability checks used outside the model registry.
6
+ # Feature capability gaps not represented in upstream model catalogs.
7
7
  module Capabilities
8
- module_function
8
+ TOOL_CAPABILITIES = %w[tool_choice parallel_tool_calls].freeze
9
9
 
10
- def supports_tool_choice?(_model_id)
11
- true
12
- end
10
+ def self.augment(capabilities, **)
11
+ return capabilities unless capabilities.include?('function_calling')
13
12
 
14
- def supports_tool_parallel_control?(_model_id)
15
- true
13
+ capabilities | TOOL_CAPABILITIES
16
14
  end
17
15
  end
18
16
  end
@@ -4,12 +4,8 @@ module RubyLLM
4
4
  module Providers
5
5
  # Anthropic Claude API integration.
6
6
  class Anthropic < Provider
7
- include Anthropic::Chat
8
- include Anthropic::Embeddings
9
- include Anthropic::Media
10
- include Anthropic::Models
11
- include Anthropic::Streaming
12
- include Anthropic::Tools
7
+ protocol :anthropic, Protocols::Anthropic, batches: Protocols::Anthropic::Batches
8
+ protocol :files, Protocols::Anthropic::Files
13
9
 
14
10
  def api_base
15
11
  @config.anthropic_api_base || 'https://api.anthropic.com'
@@ -22,6 +18,8 @@ module RubyLLM
22
18
  }
23
19
  end
24
20
 
21
+ def batch_cost_multiplier(**) = 0.5
22
+
25
23
  class << self
26
24
  def capabilities
27
25
  Anthropic::Capabilities
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Audio endpoints for Azure OpenAI deployments and the v1 API.
7
+ module Audio
8
+ def speech_url(model:)
9
+ @provider.azure_media_url(super)
10
+ end
11
+
12
+ def transcription_url
13
+ @provider.azure_media_url(super)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Feature capability gaps not represented in upstream model catalogs.
7
+ module Capabilities
8
+ def self.augment(capabilities, model_id:, **)
9
+ return capabilities unless model_id == 'grok-4-1-fast-non-reasoning'
10
+
11
+ capabilities | %w[tool_choice parallel_tool_calls]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -9,15 +9,8 @@ module RubyLLM
9
9
  azure_endpoint(:chat)
10
10
  end
11
11
 
12
- def format_messages(messages)
13
- messages.map do |msg|
14
- {
15
- role: format_role(msg.role),
16
- content: Media.format_content(msg.content),
17
- tool_calls: format_tool_calls(msg.tool_calls),
18
- tool_call_id: msg.tool_call_id
19
- }.compact.merge(format_thinking(msg))
20
- end
12
+ def format_content(content, attachments = [])
13
+ Media.format_content(content, attachments)
21
14
  end
22
15
 
23
16
  def format_role(role)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ class ChatCompletions
7
+ # Azure OpenAI exposes the OpenAI file-backed Batch API under /openai/v1,
8
+ # but batch endpoint names omit the /v1 prefix inside the create body.
9
+ module Batches
10
+ include Protocols::ChatCompletions::Batches
11
+
12
+ private
13
+
14
+ def batch_endpoint
15
+ super.delete_prefix('/v1')
16
+ end
17
+
18
+ def batch_create_url
19
+ "#{@provider.azure_openai_v1_base}/batches"
20
+ end
21
+
22
+ def batch_url(id)
23
+ "#{@provider.azure_openai_v1_base}/batches/#{id}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure < Provider
6
+ # Azure's dialect of the Chat Completions API. Endpoints depend on how
7
+ # the configured api_base is shaped (deployment, resource, or openai/v1).
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include Azure::Chat
10
+ include Azure::Embeddings
11
+ include Azure::Media
12
+ include Azure::Models
13
+ include Azure::Videos
14
+ include Azure::Images
15
+ include Azure::Audio
16
+
17
+ def azure_endpoint(kind)
18
+ parts = azure_base_parts
19
+
20
+ case kind
21
+ when :chat
22
+ chat_endpoint(parts)
23
+ when :embeddings
24
+ embeddings_endpoint(parts)
25
+ when :models
26
+ models_endpoint(parts)
27
+ else
28
+ raise ArgumentError, "Unknown Azure endpoint kind: #{kind.inspect}"
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def azure_base_parts
35
+ @provider.azure_base_parts
36
+ end
37
+
38
+ def chat_endpoint(parts)
39
+ case parts[:mode]
40
+ when :chat_endpoint
41
+ ''
42
+ when :deployment_base
43
+ with_api_version('chat/completions', parts[:version] || Azure::DEFAULT_CHAT_API_VERSION)
44
+ when :openai_v1_base
45
+ with_api_version('chat/completions', parts[:version])
46
+ else
47
+ with_api_version('models/chat/completions', parts[:version] || Azure::DEFAULT_CHAT_API_VERSION)
48
+ end
49
+ end
50
+
51
+ def embeddings_endpoint(parts)
52
+ case parts[:mode]
53
+ when :deployment_base
54
+ with_api_version('embeddings', parts[:version] || Azure::DEFAULT_EMBEDDINGS_API_VERSION)
55
+ when :openai_v1_base
56
+ with_api_version('embeddings', parts[:version])
57
+ else
58
+ "#{parts[:root]}/openai/v1/embeddings"
59
+ end
60
+ end
61
+
62
+ def models_endpoint(parts)
63
+ case parts[:mode]
64
+ when :openai_v1_base
65
+ with_api_version('models', parts[:version] || Azure::DEFAULT_MODELS_API_VERSION)
66
+ else
67
+ "#{parts[:root]}/openai/v1/models?api-version=#{parts[:version] || Azure::DEFAULT_MODELS_API_VERSION}"
68
+ end
69
+ end
70
+
71
+ def with_api_version(path, version)
72
+ return path unless version
73
+
74
+ separator = path.include?('?') ? '&' : '?'
75
+ "#{path}#{separator}api-version=#{version}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Azure endpoints and deployment names for the native Cohere APIs.
7
+ class Cohere < Protocols::Cohere
8
+ abstract :render_payload, :completion_url, :models_url, :ocr_url, :transcription_url, :tokenization_url
9
+ undef_method :create_batch, :find_batch, :cancel_batch, :batch_results
10
+
11
+ EMBEDDING_MODEL_ALIASES = {
12
+ 'Cohere-embed-v3-english' => 'embed-english-v3.0',
13
+ 'Cohere-embed-v3-multilingual' => 'embed-multilingual-v3.0'
14
+ }.freeze
15
+ private_constant :EMBEDDING_MODEL_ALIASES
16
+
17
+ def embedding_url(...)
18
+ @provider.azure_cohere_url('embed')
19
+ end
20
+
21
+ def rerank_url
22
+ @provider.azure_cohere_url('rerank')
23
+ end
24
+
25
+ private
26
+
27
+ def separate_image_embeddings?(model)
28
+ super(EMBEDDING_MODEL_ALIASES.fetch(model, model))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -11,12 +11,13 @@ module RubyLLM
11
11
  azure_endpoint(:embeddings)
12
12
  end
13
13
 
14
- def render_embedding_payload(text, model:, dimensions:)
14
+ # rubocop:disable-next Lint/UnusedMethodArgument
15
+ def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
15
16
  {
16
17
  model: model,
17
18
  input: [text].flatten,
18
19
  dimensions: dimensions
19
- }.compact
20
+ }.compact.merge(provider_options)
20
21
  end
21
22
  end
22
23
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Azure image endpoints use multipart uploads for edits.
7
+ module Images
8
+ def images_url(with: nil, mask: nil)
9
+ @provider.azure_media_url(super)
10
+ end
11
+
12
+ def render_edit_payload(prompt, size:, provider_options:, **options)
13
+ super(prompt, size:, provider_options: { size: size }.compact.merge(provider_options), **options)
14
+ end
15
+
16
+ def json_image_references?(_model)
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -7,28 +7,19 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
- return content.value if content.is_a?(RubyLLM::Content::Raw)
12
- return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
13
- return content unless content.is_a?(Content)
14
-
15
- parts = []
16
- parts << OpenAI::Media.format_text(content.text) if content.text
17
-
18
- content.attachments.each do |attachment|
10
+ def format_content(content, attachments = [])
11
+ Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
19
12
  case attachment.type
20
13
  when :image
21
- parts << format_image(attachment)
14
+ format_image(attachment)
22
15
  when :audio
23
- parts << OpenAI::Media.format_audio(attachment)
16
+ Protocols::ChatCompletions::Media.format_audio(attachment)
24
17
  when :text
25
- parts << OpenAI::Media.format_text_file(attachment)
18
+ Protocols::ChatCompletions::Media.format_text_file(attachment)
26
19
  else
27
20
  raise UnsupportedAttachmentError, attachment.mime_type
28
21
  end
29
22
  end
30
-
31
- parts
32
23
  end
33
24
 
34
25
  def format_image(image)
@@ -8,6 +8,41 @@ module RubyLLM
8
8
  def models_url
9
9
  azure_endpoint(:models)
10
10
  end
11
+
12
+ # Azure reports the same id once per region it is offered in, and keeps
13
+ # returning models whose inference deprecation date has passed.
14
+ def parse_list_models_response(response, slug)
15
+ Array(response.body['data'])
16
+ .uniq { |model_data| model_data['id'] }
17
+ .reject { |model_data| azure_inference_retired?(model_data) }
18
+ .map { |model_data| azure_model(model_data, slug) }
19
+ end
20
+
21
+ def azure_inference_retired?(model_data)
22
+ retires_at = model_data.dig('deprecation', 'inference')
23
+ retires_at.is_a?(Numeric) && Time.at(retires_at) < Time.now
24
+ end
25
+
26
+ def azure_model(model_data, slug)
27
+ Model.new(
28
+ id: model_data['id'],
29
+ name: model_data['id'],
30
+ provider: slug,
31
+ created_at: model_data['created_at'] ? Time.at(model_data['created_at']) : nil,
32
+ modalities: azure_modalities(model_data['capabilities'] || {}),
33
+ metadata: {
34
+ object: model_data['object'],
35
+ lifecycle_status: model_data['lifecycle_status'],
36
+ inference_retires_at: model_data.dig('deprecation', 'inference')
37
+ }.compact
38
+ )
39
+ end
40
+
41
+ def azure_modalities(capabilities)
42
+ return { input: ['text'], output: ['embeddings'] } if capabilities['embeddings']
43
+
44
+ { input: ['text'], output: ['text'] }
45
+ end
11
46
  end
12
47
  end
13
48
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure < Provider
6
+ # Azure's dialect of the OpenAI Responses API, served from the
7
+ # implicitly versioned openai/v1 base. The model field carries the
8
+ # deployment name, as on Chat Completions. Azure rejects the
9
+ # user_data file upload purpose.
10
+ class Responses < Protocols::Responses
11
+ include Azure::Videos
12
+ include Azure::Images
13
+ include Azure::Audio
14
+ include Protocols::Responses::Compaction
15
+
16
+ def completion_url
17
+ "#{@provider.azure_openai_v1_base}/responses"
18
+ end
19
+
20
+ def provider_file_upload_options(_attachment)
21
+ { purpose: 'assistants' }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Sora video generation jobs on Azure OpenAI. The API lives under
7
+ # the resource's /openai/v1 base and is versioned 'preview' while
8
+ # Sora is in preview.
9
+ module Videos
10
+ API_VERSION = 'preview'
11
+
12
+ def video_url
13
+ azure_video_url('video/generations/jobs')
14
+ end
15
+
16
+ def render_video_payload(prompt, model:, with: [], provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
17
+ { model: model, prompt: prompt }.merge(provider_options)
18
+ end
19
+
20
+ def parse_video_job(response, model:)
21
+ id = response.body['id']
22
+ raise Error.new('Azure did not return a video generation job', response:) unless id
23
+
24
+ VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
25
+ end
26
+
27
+ def video_job_url(job)
28
+ azure_video_url("video/generations/jobs/#{job.id}")
29
+ end
30
+
31
+ def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
32
+ body = response.body
33
+ case body['status']
34
+ when 'succeeded' then { status: :completed, raw: body }
35
+ when 'failed', 'cancelled'
36
+ { status: :failed, raw: body, error: body['failure_reason'] || body['status'] }
37
+ else { status: :pending, raw: body }
38
+ end
39
+ end
40
+
41
+ def download_video(job)
42
+ generation_id = job.raw.dig('generations', 0, 'id')
43
+ raise Error, 'Azure video job has no generations' unless generation_id
44
+
45
+ response = @connection.get azure_video_url("video/generations/#{generation_id}/content/video")
46
+
47
+ Video.new(
48
+ data: response.body,
49
+ mime_type: response.headers['content-type'] || 'video/mp4',
50
+ model: job.raw['model'] || job.model,
51
+ duration: job.raw['n_seconds'],
52
+ raw: job.raw
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ def azure_video_url(path)
59
+ "#{@provider.azure_openai_v1_base}/#{path}?api-version=#{API_VERSION}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -3,79 +3,80 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # Azure AI Foundry / OpenAI-compatible API integration.
6
- class Azure < OpenAI
7
- AZURE_DEFAULT_CHAT_API_VERSION = '2024-05-01-preview'
8
- AZURE_DEFAULT_MODELS_API_VERSION = 'preview'
6
+ class Azure < Provider
7
+ DEFAULT_CHAT_API_VERSION = '2024-05-01-preview'
8
+ DEFAULT_EMBEDDINGS_API_VERSION = '2024-05-01-preview'
9
+ DEFAULT_MODELS_API_VERSION = 'preview'
9
10
 
10
- include Azure::Chat
11
- include Azure::Embeddings
12
- include Azure::Media
13
- include Azure::Models
11
+ protocol :chat_completions, Azure::ChatCompletions, batches: Azure::ChatCompletions::Batches
12
+ protocol :responses, Azure::Responses
13
+ protocol :files, Protocols::Azure::Files
14
+ protocol :cohere, Azure::Cohere
14
15
 
15
16
  def api_base
16
17
  @config.azure_api_base
17
18
  end
18
19
 
20
+ # Deployments named after gpt-5.4+ models need the Responses API for
21
+ # tool use, so they route there automatically. Deployment names often
22
+ # differ from model ids, so the routing stays conservative; an explicit
23
+ # protocol: or the azure_protocol configuration option overrides it.
24
+ def protocol_for(model, operation: nil, **)
25
+ return protocols[:cohere] if operation == :rerank
26
+ if operation == :embed &&
27
+ %w[Cohere-embed-v3-english Cohere-embed-v3-multilingual embed-v-4-0].include?(model.id)
28
+ return protocols[:cohere]
29
+ end
30
+
31
+ model.id.match?(/gpt-5\.[4-9]|gpt-5\d/) ? protocols[:responses] : super
32
+ end
33
+
19
34
  def headers
20
- if @config.azure_api_key
35
+ if @config.azure_api_key && URI.parse(api_base).host&.end_with?('.models.ai.azure.com')
36
+ { 'Authorization' => "Bearer #{@config.azure_api_key}" }
37
+ elsif @config.azure_api_key
21
38
  { 'api-key' => @config.azure_api_key }
22
39
  else
23
40
  { 'Authorization' => "Bearer #{@config.azure_ai_auth_token}" }
24
41
  end
25
42
  end
26
43
 
27
- def configured?
28
- self.class.configured?(@config)
44
+ def batch_cost_multiplier(**) = 0.5
45
+
46
+ def azure_cohere_url(operation) # :nodoc:
47
+ base = azure_base_parts[:path_base]
48
+ base = if base.include?('/providers/cohere')
49
+ base.sub(%r{/providers/cohere.*\z}, '/providers/cohere')
50
+ elsif URI.parse(base).host&.end_with?('.models.ai.azure.com')
51
+ base.sub(%r{/v2(?:/.*)?\z}, '')
52
+ else
53
+ "#{azure_base_parts[:root]}/providers/cohere"
54
+ end
55
+ "#{base}/v2/#{operation}"
29
56
  end
30
57
 
31
- def azure_endpoint(kind)
58
+ def azure_openai_v1_base
32
59
  parts = azure_base_parts
33
-
34
- case kind
35
- when :chat
36
- chat_endpoint(parts)
37
- when :embeddings
38
- embeddings_endpoint(parts)
39
- when :models
40
- models_endpoint(parts)
60
+ if parts[:mode] == :openai_v1_base
61
+ parts[:path_base]
41
62
  else
42
- raise ArgumentError, "Unknown Azure endpoint kind: #{kind.inspect}"
63
+ "#{parts[:root]}/openai/v1"
43
64
  end
44
65
  end
45
66
 
46
- class << self
47
- def configuration_options
48
- %i[azure_api_base azure_api_key azure_ai_auth_token]
49
- end
50
-
51
- def configuration_requirements
52
- %i[azure_api_base]
53
- end
54
-
55
- def configured?(config)
56
- config.azure_api_base && (config.azure_api_key || config.azure_ai_auth_token)
57
- end
58
-
59
- # Azure works with deployment names, instead of model names
60
- def assume_models_exist?
61
- true
62
- end
63
- end
64
-
65
- def ensure_configured!
66
- missing = []
67
- missing << :azure_api_base unless @config.azure_api_base
68
- if @config.azure_api_key.nil? && @config.azure_ai_auth_token.nil?
69
- missing << 'azure_api_key or azure_ai_auth_token'
67
+ def azure_media_url(path) # :nodoc:
68
+ parts = azure_base_parts
69
+ if parts[:path_base].include?('/openai/deployments/')
70
+ base = parts[:path_base].sub(%r{/chat/completions/?\z}, '')
71
+ version = parts[:version] || '2025-04-01-preview'
72
+ else
73
+ base = azure_openai_v1_base
74
+ version = (parts[:version] if parts[:mode] == :openai_v1_base) || 'preview'
70
75
  end
71
- return if missing.empty?
72
76
 
73
- raise ConfigurationError,
74
- "Missing configuration for Azure: #{missing.join(', ')}"
77
+ "#{base.sub(%r{/+\z}, '')}/#{path}?api-version=#{version}"
75
78
  end
76
79
 
77
- private
78
-
79
80
  def azure_base_parts
80
81
  @azure_base_parts ||= begin
81
82
  raw_base = api_base.to_s.sub(%r{/+\z}, '')
@@ -93,7 +94,6 @@ module RubyLLM
93
94
  end
94
95
 
95
96
  {
96
- raw_base: raw_base,
97
97
  path_base: path_base,
98
98
  root: azure_host_root(path_base),
99
99
  mode: mode,
@@ -102,44 +102,43 @@ module RubyLLM
102
102
  end
103
103
  end
104
104
 
105
- def chat_endpoint(parts)
106
- case parts[:mode]
107
- when :chat_endpoint
108
- ''
109
- when :deployment_base
110
- with_api_version('chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
111
- when :openai_v1_base
112
- with_api_version('chat/completions', parts[:version])
113
- else
114
- with_api_version('models/chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
105
+ class << self
106
+ def capabilities
107
+ Azure::Capabilities
115
108
  end
116
- end
117
109
 
118
- def embeddings_endpoint(parts)
119
- case parts[:mode]
120
- when :deployment_base, :openai_v1_base
121
- with_api_version('embeddings', parts[:version])
122
- else
123
- "#{parts[:root]}/openai/v1/embeddings"
110
+ def configuration_options
111
+ %i[azure_api_base azure_api_key azure_ai_auth_token]
124
112
  end
125
- end
126
113
 
127
- def models_endpoint(parts)
128
- case parts[:mode]
129
- when :openai_v1_base
130
- with_api_version('models', parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION)
131
- else
132
- "#{parts[:root]}/openai/v1/models?api-version=#{parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION}"
114
+ def configuration_requirements
115
+ %i[azure_api_base]
116
+ end
117
+
118
+ def configured?(config)
119
+ config.azure_api_base && (config.azure_api_key || config.azure_ai_auth_token)
120
+ end
121
+
122
+ # Azure works with deployment names, instead of model names
123
+ def assume_models_exist?
124
+ true
133
125
  end
134
126
  end
135
127
 
136
- def with_api_version(path, version)
137
- return path unless version
128
+ def ensure_configured!
129
+ missing = []
130
+ missing << :azure_api_base unless @config.azure_api_base
131
+ if @config.azure_api_key.nil? && @config.azure_ai_auth_token.nil?
132
+ missing << 'azure_api_key or azure_ai_auth_token'
133
+ end
134
+ return if missing.empty?
138
135
 
139
- separator = path.include?('?') ? '&' : '?'
140
- "#{path}#{separator}api-version=#{version}"
136
+ raise ConfigurationError,
137
+ "Missing configuration for Azure: #{missing.join(', ')}"
141
138
  end
142
139
 
140
+ private
141
+
143
142
  def azure_host_root(base_without_query)
144
143
  base_without_query.sub(%r{/(models|openai)/.*\z}, '').sub(%r{/+\z}, '')
145
144
  end