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
@@ -7,7 +7,10 @@ module RubyLLM
7
7
  module Models
8
8
  module_function
9
9
 
10
- REGION_PREFIXES = %w[global us eu ap sa ca me af il].freeze
10
+ JP_REGIONS = %w[ap-northeast-1 ap-northeast-3].freeze
11
+ AU_REGIONS = %w[ap-southeast-2 ap-southeast-4].freeze
12
+
13
+ MANTLE_ENDPOINT = 'mantle'
11
14
 
12
15
  def models_api_base
13
16
  @config.bedrock_api_base || "https://bedrock.#{bedrock_region}.amazonaws.com"
@@ -17,24 +20,104 @@ module RubyLLM
17
20
  '/foundation-models'
18
21
  end
19
22
 
20
- def parse_list_models_response(response, slug, _capabilities)
23
+ def mantle_models_url
24
+ 'v1/models'
25
+ end
26
+
27
+ def models_dev_alias(model_id, models_dev_by_key, _provider_model = nil)
28
+ normalized_id = model_id.sub(/^[a-z]{2}\./, '')
29
+ context_override = nil
30
+ normalized_id = normalized_id.gsub(/:(\d+)k\b/) do
31
+ context_override = Regexp.last_match(1).to_i * 1000
32
+ ''
33
+ end
34
+ source = models_dev_by_key["bedrock:#{normalized_id}"]
35
+ return unless source
36
+
37
+ data = source.to_h.merge(id: model_id)
38
+ data[:context_window] = context_override if context_override
39
+ Model.new(data)
40
+ end
41
+
42
+ # The mantle catalog reports ids and nothing else, so entries carry
43
+ # only what the endpoint states. models.dev fills in limits and
44
+ # pricing for the ids it knows during a registry refresh.
45
+ def parse_mantle_models_response(response, slug)
46
+ Array(response.body['data']).map do |model_data|
47
+ Model.new(
48
+ id: model_data['id'],
49
+ name: model_data['id'],
50
+ provider: slug,
51
+ created_at: mantle_created_at(model_data['created']),
52
+ modalities: { input: ['text'], output: ['text'] },
53
+ capabilities: ['streaming'],
54
+ metadata: { endpoint: MANTLE_ENDPOINT }
55
+ )
56
+ end
57
+ end
58
+
59
+ # Models both catalogs list keep their Converse metadata and gain the
60
+ # mantle tag, because mantle is where RubyLLM sends them.
61
+ def merge_mantle_models(converse_models, mantle_models)
62
+ by_id = converse_models.to_h { |model| [model.id, model] }
63
+
64
+ tagged = mantle_models.map do |model|
65
+ listed = by_id.delete(model.id)
66
+ listed ? tag_mantle_endpoint(listed) : model
67
+ end
68
+
69
+ by_id.values + tagged
70
+ end
71
+
72
+ def tag_mantle_endpoint(model)
73
+ data = model.to_h
74
+ Model.new(data.merge(metadata: data[:metadata].merge(endpoint: MANTLE_ENDPOINT)))
75
+ end
76
+
77
+ def mantle_created_at(created)
78
+ Time.at(created).utc if created.is_a?(Numeric)
79
+ end
80
+
81
+ def parse_list_models_response(response, slug, profile_ids: [])
21
82
  Array(response.body['modelSummaries']).map do |model_data|
22
- create_model_info(model_data, slug)
83
+ create_model_info(model_data, slug, profile_ids:)
84
+ end
85
+ end
86
+
87
+ # The region's actual cross-region inference profile ids. Some
88
+ # geography prefixes cannot be synthesized from the region alone:
89
+ # Tokyo serves both apac. and jp. profiles.
90
+ def inference_profile_ids
91
+ ids = []
92
+ token = nil
93
+
94
+ loop do
95
+ url = +'/inference-profiles?maxResults=1000&typeEquals=SYSTEM_DEFINED'
96
+ url << "&nextToken=#{URI.encode_www_form_component(token)}" if token
97
+ body = signed_get(models_api_base, url).body
98
+ ids.concat(Array(body['inferenceProfileSummaries']).map { |summary| summary['inferenceProfileId'] })
99
+ token = body['nextToken']
100
+ break unless token
23
101
  end
102
+
103
+ ids.compact
104
+ rescue StandardError => e
105
+ RubyLLM.logger.debug { "Error fetching Bedrock inference profiles: #{e.message}" }
106
+ []
24
107
  end
25
108
 
26
- def create_model_info(model_data, slug, _capabilities = nil)
27
- model_id = model_id_with_region(model_data['modelId'], model_data)
109
+ def create_model_info(model_data, slug, _capabilities = nil, profile_ids: [])
110
+ model_id = model_id_with_region(model_data['modelId'], model_data, profile_ids)
28
111
  converse_data = model_data['converse'] || {}
29
112
 
30
- Model::Info.new(
113
+ Model.new(
31
114
  id: model_id,
32
115
  name: model_data['modelName'],
33
116
  provider: slug,
34
117
  family: model_data['modelFamily'] || model_data['providerName']&.downcase,
35
118
  created_at: nil,
36
119
  context_window: parse_context_window(model_data),
37
- max_output_tokens: converse_data['maxTokensDefault'] || converse_data['maxTokensMaximum'],
120
+ max_output_tokens: converse_data['maxTokensMaximum'] || converse_data['maxTokensDefault'],
38
121
  modalities: {
39
122
  input: normalize_modalities(model_data['inputModalities']),
40
123
  output: normalize_modalities(model_data['outputModalities'])
@@ -50,36 +133,123 @@ module RubyLLM
50
133
  )
51
134
  end
52
135
 
53
- def model_id_with_region(model_id, model_data)
136
+ def model_id_with_region(model_id, model_data, profile_ids = [])
54
137
  inference_types = Array(model_data['inferenceTypesSupported'])
55
- normalize_inference_profile_id(model_id, inference_types, @config.bedrock_region)
138
+ return model_id unless inference_profile_only?(inference_types)
139
+
140
+ listed_profile_id(model_id, profile_ids) || with_region_prefix(model_id, @config.bedrock_region)
141
+ end
142
+
143
+ def listed_profile_id(model_id, profile_ids)
144
+ candidates = profile_ids.select { |profile_id| profile_id.end_with?(".#{model_id}") }
145
+ preferred = region_prefix_candidates(@config.bedrock_region).filter_map do |prefix|
146
+ candidates.find { |profile_id| profile_id == "#{prefix}.#{model_id}" }
147
+ end.first
148
+
149
+ preferred || candidates.first
150
+ end
151
+
152
+ # Which endpoint serves a model is a fact about the catalogs, not
153
+ # about the id: mantle spells some Converse models differently and
154
+ # lists models Converse has never heard of. The registry records it,
155
+ # so ask the registry first and read the id only for models it does
156
+ # not list.
157
+ #
158
+ # Claude is the exception. Converse only ever serves it under a dated
159
+ # and versioned id, so a bare anthropic. id means mantle even on
160
+ # accounts whose catalog listing hides Claude behind the AWS Sales
161
+ # agreement.
162
+ def mantle_model?(model_id, models)
163
+ return mantle_model_id?(model_id) if model_id.start_with?('anthropic.')
164
+
165
+ listed = registered_model(model_id, models)
166
+ return listed.metadata[:endpoint].to_s == MANTLE_ENDPOINT if listed
167
+
168
+ mantle_model_id?(model_id)
169
+ end
170
+
171
+ def registered_model(model_id, models)
172
+ models.all_including_unlisted.find { |model| model.provider == 'bedrock' && model.id == model_id }
173
+ end
174
+
175
+ # The bedrock-mantle endpoint serves bare vendor.model ids under
176
+ # exactly that id. Converse ids carry a date or :N version suffix, a
177
+ # cross-region inference prefix, or both.
178
+ def mantle_model_id?(model_id)
179
+ return false if model_id.include?(':') || model_id.match?(/\d{8}/)
180
+ return false if region_prefixed?(model_id)
181
+
182
+ model_id.include?('.')
183
+ end
184
+
185
+ def resolve_registry_id(model_id, models, config)
186
+ region = config.bedrock_region.to_s
187
+ return model_id if region.empty?
188
+ return model_id if mantle_model_id?(model_id)
189
+
190
+ candidate = registered_profile_candidate(model_id, models, region)
191
+ return model_id unless candidate
192
+
193
+ inference_types = Array(candidate.metadata[:inference_types] || candidate.metadata['inference_types'])
194
+ inference_profile_only?(inference_types) ? candidate.id : model_id
195
+ end
196
+
197
+ def registered_profile_candidate(model_id, models, region)
198
+ region_prefix_candidates(region).each do |prefix|
199
+ prefixed = prefixed_with(model_id, prefix)
200
+ next if prefixed == model_id
201
+
202
+ candidate = models.all_including_unlisted.find { |m| m.provider == 'bedrock' && m.id == prefixed }
203
+ return candidate if candidate
204
+ end
205
+
206
+ nil
207
+ end
208
+
209
+ def inference_profile_only?(inference_types)
210
+ inference_types.include?('INFERENCE_PROFILE') && !inference_types.include?('ON_DEMAND')
56
211
  end
57
212
 
58
213
  def normalize_inference_profile_id(model_id, inference_types, region)
59
- return model_id unless inference_types.include?('INFERENCE_PROFILE')
60
- return model_id if inference_types.include?('ON_DEMAND')
214
+ return model_id unless inference_profile_only?(inference_types)
61
215
 
62
216
  with_region_prefix(model_id, region)
63
217
  end
64
218
 
65
219
  def with_region_prefix(model_id, region)
66
- prefix = region_prefix(region)
220
+ prefixed_with(model_id, region_prefix(region))
221
+ end
67
222
 
223
+ def prefixed_with(model_id, prefix)
68
224
  if region_prefixed?(model_id)
69
- model_id.sub(/\A(?:#{REGION_PREFIXES.join('|')})\./, "#{prefix}.")
225
+ model_id.sub(/\A(?:#{Protocols::Converse::REGION_PREFIXES.join('|')})\./, "#{prefix}.")
70
226
  else
71
227
  "#{prefix}.#{model_id}"
72
228
  end
73
229
  end
74
230
 
231
+ # Inference profile ids use geography prefixes, not region name
232
+ # segments: ap-* regions are apac., GovCloud is us-gov.
75
233
  def region_prefix(region)
76
- prefix = region.to_s.split('-').first
77
- prefix = '' if prefix.nil?
78
- prefix.empty? ? 'us' : prefix
234
+ region = region.to_s
235
+ return 'us' if region.empty?
236
+ return 'us-gov' if region.start_with?('us-gov')
237
+
238
+ geography = region.split('-').first
239
+ geography == 'ap' ? 'apac' : geography
240
+ end
241
+
242
+ # Countries with their own residency-preserving profiles come before
243
+ # the broad geography, so Tokyo prefers jp. over apac. Some models
244
+ # ship with only a global. profile, so that is the last resort.
245
+ def region_prefix_candidates(region)
246
+ region = region.to_s
247
+ specific = ('jp' if JP_REGIONS.include?(region)) || ('au' if AU_REGIONS.include?(region))
248
+ [specific, region_prefix(region), 'global'].compact
79
249
  end
80
250
 
81
251
  def region_prefixed?(model_id)
82
- model_id.match?(/\A(?:#{REGION_PREFIXES.join('|')})\./)
252
+ model_id.match?(/\A(?:#{Protocols::Converse::REGION_PREFIXES.join('|')})\./)
83
253
  end
84
254
 
85
255
  def normalize_modalities(modalities)
@@ -93,40 +263,22 @@ module RubyLLM
93
263
  end
94
264
  end
95
265
 
266
+ # A summary's converse block is metadata Bedrock fills in for some
267
+ # models, not a statement that Converse serves them: Nova Lite,
268
+ # Llama 3.3 and Mistral Large take tool calls without one. Whether
269
+ # Converse accepts the model at all is the closest thing the listing
270
+ # has to a tool-use flag.
96
271
  def parse_capabilities(model_data)
97
272
  capabilities = []
98
273
  capabilities << 'streaming' if model_data['responseStreamingSupported']
99
274
 
100
275
  converse = model_data['converse'] || {}
101
- capabilities << 'function_calling' if converse.is_a?(Hash)
276
+ capabilities << 'function_calling' if model_data.dig('inferenceAPIsSupported', 'converse', 'sync')
102
277
  capabilities << 'reasoning' if converse.dig('reasoningSupported', 'embedded')
103
- capabilities << 'structured_output' if supports_structured_output?(model_data['modelId'])
104
278
 
105
279
  capabilities
106
280
  end
107
281
 
108
- # Structured output supported on Claude 4.5+ and assumed for future major versions.
109
- # Bedrock IDs look like: us.anthropic.claude-haiku-4-5-20251001-v1:0
110
- # Must handle optional region prefix (us./eu./global.) and anthropic. prefix.
111
- def supports_structured_output?(model_id)
112
- return false unless model_id
113
-
114
- normalized = model_id.sub(/\A(?:#{REGION_PREFIXES.join('|')})\./, '').delete_prefix('anthropic.')
115
- match = normalized.match(/claude-(?:opus|sonnet|haiku)-(\d+)-(\d{1,2})(?:\b|-)/)
116
- return false unless match
117
-
118
- major = match[1].to_i
119
- minor = match[2].to_i
120
- major > 4 || (major == 4 && minor >= 5)
121
- end
122
-
123
- def reasoning_embedded?(model)
124
- metadata = RubyLLM::Utils.deep_symbolize_keys(model.metadata || {})
125
- converse = metadata[:converse] || {}
126
- reasoning_supported = converse[:reasoningSupported] || {}
127
- reasoning_supported[:embedded] || false
128
- end
129
-
130
282
  def parse_context_window(model_data)
131
283
  value = model_data.dig('description', 'maxContextWindow')
132
284
  return unless value.is_a?(String)
@@ -2,97 +2,268 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- # AWS Bedrock Converse API integration.
5
+ # AWS Bedrock integration.
6
6
  class Bedrock < Provider
7
7
  include Bedrock::Auth
8
- include Bedrock::Chat
9
- include Bedrock::Media
10
8
  include Bedrock::Models
11
- include Bedrock::Streaming
9
+
10
+ protocol :converse, Protocols::Converse, batches: Protocols::Converse::Batches
11
+ protocol :mantle_anthropic, Bedrock::Mantle::Anthropic
12
+ protocol :mantle_responses, Bedrock::Mantle::Responses
13
+ protocol :mantle_chat_completions, Bedrock::Mantle::ChatCompletions
14
+ protocol :voxtral_transcription, Bedrock::Mantle::Voxtral
15
+ protocol :titan_text_embeddings, Protocols::InvokeModel::TitanTextEmbeddings,
16
+ batches: Protocols::InvokeModel::EmbeddingBatches
17
+ protocol :titan_multimodal_embeddings, Protocols::InvokeModel::TitanMultimodalEmbeddings,
18
+ batches: Protocols::InvokeModel::EmbeddingBatches
19
+ protocol :cohere_embeddings, Protocols::InvokeModel::CohereEmbeddings
20
+ protocol :nova_embeddings, Protocols::InvokeModel::NovaEmbeddings
21
+ protocol :stability_images, Protocols::InvokeModel::StabilityImages
22
+ protocol :rerank, Protocols::Bedrock::Rerank
23
+ protocol :guardrails, Protocols::Bedrock::Guardrails
24
+ protocol :async_videos, Protocols::Bedrock::AsyncVideos
25
+ protocol :files, Protocols::Bedrock::Files
26
+
27
+ def self.capabilities
28
+ Bedrock::Capabilities
29
+ end
30
+
31
+ def self.resolve_registry_id(model_id, models, config = RubyLLM.config)
32
+ Models.resolve_registry_id(model_id, models, config)
33
+ end
34
+
35
+ def self.models_dev_alias(...)
36
+ Models.models_dev_alias(...)
37
+ end
38
+
39
+ def protocol_for(model, operation: nil, **)
40
+ return protocols[:guardrails] if operation == :moderate
41
+
42
+ model_id = model_id_for(model)
43
+ return embedding_protocol_for(model_id) if operation == :embed
44
+ return image_protocol_for(model_id) if operation == :paint
45
+ return protocols[:rerank] if operation == :rerank
46
+ return video_protocol_for(model_id) if operation == :animate
47
+ return protocols[:voxtral_transcription] if voxtral_transcription?(operation, model_id)
48
+ return mantle_protocol_for(model_id) if mantle_model?(model_id)
49
+
50
+ super
51
+ end
52
+
53
+ # Un-versioned ids such as anthropic.claude-sonnet-5 and
54
+ # openai.gpt-oss-20b are served by the bedrock-mantle endpoint rather
55
+ # than by Converse.
56
+ def mantle_model?(model_id)
57
+ Models.mantle_model?(model_id, RubyLLM.models)
58
+ end
59
+
60
+ def mantle_api_base
61
+ @config.bedrock_mantle_api_base || "https://bedrock-mantle.#{bedrock_region}.api.aws"
62
+ end
63
+
64
+ def mantle_connection
65
+ @mantle_connection ||= Transport::Connection.new(self, @config, api_base: mantle_api_base)
66
+ end
12
67
 
13
68
  def api_base
14
69
  @config.bedrock_api_base || "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
15
70
  end
16
71
 
72
+ def control_api_base
73
+ @config.bedrock_api_base || "https://bedrock.#{bedrock_region}.amazonaws.com"
74
+ end
75
+
76
+ def agent_api_base # :nodoc:
77
+ @config.bedrock_api_base || "https://bedrock-agent-runtime.#{bedrock_region}.amazonaws.com"
78
+ end
79
+
80
+ def agent_connection # :nodoc:
81
+ @agent_connection ||= Transport::Connection.new(self, @config, api_base: agent_api_base)
82
+ end
83
+
84
+ def rerank_model_arn(model_id) # :nodoc:
85
+ return model_id if model_id.start_with?('arn:')
86
+
87
+ unless %w[amazon.rerank-v1:0 cohere.rerank-v3-5:0].include?(model_id)
88
+ raise Error, "Bedrock reranking is not supported for #{model_id.inspect}"
89
+ end
90
+
91
+ "arn:aws:bedrock:#{bedrock_region}::foundation-model/#{model_id}"
92
+ end
93
+
17
94
  def headers
18
95
  {}
19
96
  end
20
97
 
21
- # rubocop:disable Metrics/ParameterLists
22
- def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
23
- tool_prefs: nil, &)
24
- normalized_params = normalize_params(params, model:)
98
+ def guardrail_url # :nodoc:
99
+ identifier = @config.bedrock_guardrail_id
100
+ version = @config.bedrock_guardrail_version
101
+ if identifier.to_s.empty? || version.to_s.empty?
102
+ raise ConfigurationError, 'Bedrock moderation requires bedrock_guardrail_id and bedrock_guardrail_version'
103
+ end
104
+
105
+ identifier = URI.encode_www_form_component(identifier)
106
+ version = URI.encode_www_form_component(version)
107
+ "/guardrail/#{identifier}/version/#{version}/apply"
108
+ end
109
+
110
+ def batch_cost_multiplier(**) = 0.5
25
111
 
26
- super(
27
- messages,
28
- tools: tools,
29
- tool_prefs: tool_prefs,
30
- temperature: temperature,
31
- model: model,
32
- params: normalized_params,
33
- headers: headers,
34
- schema: schema,
35
- thinking: thinking,
36
- &
37
- )
112
+ def embedding_batch_protocol(model_id) # :nodoc:
113
+ case model_id
114
+ when 'amazon.titan-embed-text-v2:0' then protocols[:titan_text_embeddings]
115
+ when 'amazon.titan-embed-image-v1' then protocols[:titan_multimodal_embeddings]
116
+ end
38
117
  end
39
- # rubocop:enable Metrics/ParameterLists
40
118
 
41
119
  def parse_error(response)
42
- return if response.body.nil? || response.body.empty?
120
+ body = parse_error_body(response)
121
+ return unless body
122
+ return super unless body.is_a?(Hash)
43
123
 
44
- body = try_parse_json(response.body)
45
- return body if body.is_a?(String)
124
+ body['message'] || body['Message'] || nested_error_message(body) || body['__type'] || super
125
+ end
46
126
 
47
- body['message'] || body['Message'] || body['error'] || body['__type'] || super
127
+ # bedrock-mantle nests code, message, and type under "error" rather
128
+ # than answering with a bare string.
129
+ def nested_error_message(body)
130
+ error = body['error']
131
+ error.is_a?(Hash) ? error['message'] || error : error
48
132
  end
49
133
 
50
134
  def list_models
51
- response = signed_get(models_api_base, models_url)
52
- parse_list_models_response(response, slug, capabilities)
135
+ merge_mantle_models(list_converse_models, list_mantle_models)
53
136
  end
54
137
 
55
138
  class << self
56
139
  def configuration_options
57
- %i[bedrock_api_key bedrock_secret_key bedrock_region bedrock_session_token bedrock_api_base]
140
+ %i[
141
+ bedrock_api_key
142
+ bedrock_secret_key
143
+ bedrock_region
144
+ bedrock_session_token
145
+ bedrock_credential_provider
146
+ bedrock_api_base
147
+ bedrock_mantle_api_base
148
+ bedrock_batch_s3_uri
149
+ bedrock_batch_role_arn
150
+ bedrock_video_s3_uri
151
+ bedrock_guardrail_id
152
+ bedrock_guardrail_version
153
+ ]
58
154
  end
59
155
 
60
156
  def configuration_requirements
61
- %i[bedrock_api_key bedrock_secret_key bedrock_region]
157
+ %i[bedrock_region]
158
+ end
159
+
160
+ def model_required?(operation:)
161
+ operation != :moderate
162
+ end
163
+
164
+ def configured?(config)
165
+ !!(config.bedrock_region && credentials_configured?(config))
166
+ end
167
+
168
+ def credentials_configured?(config)
169
+ return credential_provider?(config) if config.bedrock_credential_provider
170
+
171
+ !!(config.bedrock_api_key && config.bedrock_secret_key)
62
172
  end
173
+
174
+ private
175
+
176
+ def credential_provider?(config)
177
+ config.bedrock_credential_provider&.respond_to?(:credentials)
178
+ end
179
+ end
180
+
181
+ def ensure_configured!
182
+ return if configured?
183
+
184
+ missing = []
185
+ missing << :bedrock_region unless @config.bedrock_region
186
+ missing << bedrock_credentials_requirement unless self.class.credentials_configured?(@config)
187
+
188
+ raise ConfigurationError, "Missing configuration for Bedrock: #{missing.join(', ')}"
63
189
  end
64
190
 
65
191
  private
66
192
 
193
+ def voxtral_transcription?(operation, model_id)
194
+ operation == :transcribe && model_id == 'mistral.voxtral-small-24b-2507'
195
+ end
196
+
197
+ def batch_protocol_for(requests)
198
+ return super unless requests.any? { |request| request.key?(:text) }
199
+
200
+ kinds = requests.map { |request| embedding_batch_protocol(request.fetch(:model)) }.uniq
201
+ unless requests.all? { |request| request.key?(:text) } && kinds.one? && kinds.first
202
+ raise Error, 'Bedrock embedding batches require one supported Titan embedding model'
203
+ end
204
+
205
+ kinds.first
206
+ end
207
+
67
208
  def bedrock_region
68
209
  @config.bedrock_region
69
210
  end
70
211
 
71
- def sync_response(connection, payload, additional_headers = {})
72
- signed_post(connection, completion_url, payload, additional_headers)
212
+ def bedrock_credentials_requirement
213
+ if @config.bedrock_credential_provider
214
+ 'bedrock_credential_provider responding to #credentials'
215
+ else
216
+ 'bedrock_credential_provider or bedrock_api_key + bedrock_secret_key'
217
+ end
218
+ end
219
+
220
+ def list_converse_models
221
+ parse_list_models_response(signed_get(models_api_base, models_url), slug, profile_ids: inference_profile_ids)
222
+ end
223
+
224
+ def list_mantle_models
225
+ response = signed_get(mantle_api_base, mantle_models_url, service: Bedrock::Mantle::SIGNING_SERVICE)
226
+ parse_mantle_models_response(response, slug)
73
227
  end
74
228
 
75
- def normalize_params(params, model:)
76
- normalized = RubyLLM::Utils.deep_symbolize_keys(params || {})
77
- additional_fields = normalized[:additionalModelRequestFields] || {}
229
+ def mantle_protocol_for(model_id)
230
+ return protocols[:mantle_anthropic] if model_id.start_with?('anthropic.')
231
+ return protocols[:mantle_responses] if Bedrock::Mantle::RESPONSES_MODELS.include?(model_id)
232
+
233
+ protocols[:mantle_chat_completions]
234
+ end
78
235
 
79
- top_k = normalized.delete(:top_k)
80
- if !top_k.nil? && model_supports_top_k?(model)
81
- additional_fields = RubyLLM::Utils.deep_merge(additional_fields, { top_k: top_k })
236
+ def embedding_protocol_for(model_id)
237
+ case model_id
238
+ when bedrock_model_id_pattern('amazon.titan-embed-image')
239
+ protocols[:titan_multimodal_embeddings]
240
+ when bedrock_model_id_pattern('amazon.titan-embed-g1-text'),
241
+ bedrock_model_id_pattern('amazon.titan-embed-text')
242
+ protocols[:titan_text_embeddings]
243
+ when bedrock_model_id_pattern('cohere.embed')
244
+ protocols[:cohere_embeddings]
245
+ when bedrock_model_id_pattern('amazon.nova-2-multimodal-embeddings')
246
+ protocols[:nova_embeddings]
247
+ else
248
+ raise Error, "Bedrock embeddings are not supported for #{model_id.inspect}"
82
249
  end
250
+ end
83
251
 
84
- normalized[:additionalModelRequestFields] = additional_fields unless additional_fields.empty?
85
- normalized
252
+ def image_protocol_for(model_id)
253
+ base_id = model_id.sub(/\A(?:#{Protocols::Converse::REGION_PREFIXES.join('|')})\./, '')
254
+ return protocols[:stability_images] if Protocols::InvokeModel::StabilityImages::MODELS.include?(base_id)
255
+
256
+ raise Error, "Bedrock image generation is not supported for #{model_id.inspect}"
86
257
  end
87
258
 
88
- def model_supports_top_k?(model)
89
- Bedrock::Models.reasoning_embedded?(model)
259
+ def video_protocol_for(model_id)
260
+ return protocols[:async_videos] if model_id == 'luma.ray-v2:0'
261
+
262
+ raise Error, "Bedrock video generation is not supported for #{model_id.inspect}"
90
263
  end
91
264
 
92
- def api_payload(payload)
93
- cleaned = RubyLLM::Utils.deep_symbolize_keys(RubyLLM::Utils.deep_dup(payload))
94
- cleaned.delete(:tools)
95
- cleaned
265
+ def bedrock_model_id_pattern(prefix)
266
+ /\A(?:(?:#{Protocols::Converse::REGION_PREFIXES.join('|')})\.)?#{Regexp.escape(prefix)}/
96
267
  end
97
268
  end
98
269
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Cohere API integration.
6
+ class Cohere < Provider
7
+ protocol :cohere, Protocols::Cohere
8
+ protocol :files, Protocols::Cohere::Datasets
9
+
10
+ def api_base
11
+ @config.cohere_api_base || 'https://api.cohere.com'
12
+ end
13
+
14
+ def headers
15
+ {
16
+ 'Authorization' => "Bearer #{@config.cohere_api_key}"
17
+ }
18
+ end
19
+
20
+ class << self
21
+ def configuration_options
22
+ %i[cohere_api_key cohere_api_base]
23
+ end
24
+
25
+ def configuration_requirements
26
+ %i[cohere_api_key]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Integrates Deepgram transcription and speech synthesis.
6
+ class Deepgram < Provider
7
+ protocol :deepgram, Protocols::Deepgram
8
+
9
+ def api_base
10
+ @config.deepgram_api_base || 'https://api.deepgram.com'
11
+ end
12
+
13
+ def headers
14
+ { 'Authorization' => "Token #{@config.deepgram_api_key}" }
15
+ end
16
+
17
+ # Deepgram reports failures as an err_msg alongside an err_code, which
18
+ # the generic error parser does not know to look for.
19
+ def parse_error(response)
20
+ body = parse_error_body(response)
21
+ return super unless body.is_a?(Hash)
22
+
23
+ body['err_msg'] || super
24
+ end
25
+
26
+ class << self
27
+ def configuration_options
28
+ %i[deepgram_api_key deepgram_api_base]
29
+ end
30
+
31
+ def configuration_requirements
32
+ %i[deepgram_api_key]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end