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
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Bedrock
6
+ # Luma Ray 2 video generation through Bedrock asynchronous invocation.
7
+ class AsyncVideos < Protocol
8
+ IMAGE_TYPES = %w[image/png image/jpeg].freeze
9
+
10
+ def video_url
11
+ '/async-invoke'
12
+ end
13
+
14
+ def render_video_payload(prompt, model:, with: [], provider_options: {})
15
+ output_uri = video_output_uri
16
+ unless prompt.is_a?(String) && (1..5000).cover?(prompt.length)
17
+ raise ArgumentError, 'Luma Ray 2 requires a prompt between 1 and 5000 characters'
18
+ end
19
+
20
+ options = Support::Utils.deep_symbolize_keys(provider_options)
21
+ outer = options.slice(:clientRequestToken, :tags)
22
+ input = { prompt: prompt }.merge(render_keyframes(with)).merge(options.except(*outer.keys))
23
+ { modelId: model, modelInput: input, clientRequestToken: SecureRandom.uuid,
24
+ outputDataConfig: { s3OutputDataConfig: { s3Uri: output_uri } } }.merge(outer)
25
+ end
26
+
27
+ def post_video(url, payload)
28
+ @connection.post(url, payload, idempotent: false) do |request|
29
+ request.headers.merge!(@provider.sign_headers('POST', url, JSON.generate(payload)))
30
+ end
31
+ end
32
+
33
+ def parse_video_job(response, model:)
34
+ id = response.body['invocationArn']
35
+ raise Error.new('Bedrock did not return a video invocation ARN', response:) unless id
36
+
37
+ VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
38
+ end
39
+
40
+ def video_job_url(job)
41
+ "/async-invoke/#{URI.encode_www_form_component(job.id)}"
42
+ end
43
+
44
+ def refresh_video_job(job)
45
+ url = video_job_url(job)
46
+ response = @connection.get(url) do |request|
47
+ request.headers.merge!(@provider.sign_headers('GET', url, ''))
48
+ end
49
+ parse_video_job_status(response, job:)
50
+ end
51
+
52
+ def parse_video_job_status(response, **)
53
+ body = response.body
54
+ status = case body['status']
55
+ when 'InProgress' then :pending
56
+ when 'Completed' then :completed
57
+ when 'Failed' then :failed
58
+ else raise Error.new("Unknown Bedrock video status: #{body['status'].inspect}", response:)
59
+ end
60
+ { status: status, raw: body, error: body['failureMessage'] }
61
+ end
62
+
63
+ def download_video(job)
64
+ prefix = job.raw.dig('outputDataConfig', 's3OutputDataConfig', 's3Uri')
65
+ raise Error, 'Bedrock video job has no output S3 URI' unless prefix
66
+
67
+ uris = @provider.list_file_uris("#{prefix.sub(%r{/+\z}, '')}/")
68
+ videos = uris.select { |uri| uri.downcase.end_with?('.mp4') }
69
+ raise Error, 'Expected exactly one MP4 in the Bedrock video output' unless videos.one?
70
+
71
+ Video.new(data: @provider.download_file(videos.first), mime_type: 'video/mp4', model: job.model, raw: job.raw)
72
+ end
73
+
74
+ private
75
+
76
+ def video_output_uri
77
+ uri = @config.bedrock_video_s3_uri
78
+ unless uri&.match?(%r{\As3://[a-z0-9][.\-a-z0-9]{1,61}[a-z0-9]/[^?#]+\z})
79
+ raise ConfigurationError, 'Set bedrock_video_s3_uri to the intended s3://bucket/output-prefix'
80
+ end
81
+
82
+ uri
83
+ end
84
+
85
+ def validate_animate_inputs!(with:)
86
+ raise ArgumentError, 'Luma Ray 2 accepts at most two reference images' if with.length > 2
87
+
88
+ with.each do |attachment|
89
+ if attachment.provider_file?
90
+ raise ArgumentError, 'Luma Ray 2 requires image bytes or URLs, not uploaded file ids'
91
+ end
92
+
93
+ raise UnsupportedAttachmentError, attachment.mime_type unless IMAGE_TYPES.include?(attachment.mime_type)
94
+ end
95
+ end
96
+
97
+ def render_keyframes(attachments)
98
+ return {} if attachments.empty?
99
+
100
+ frames = attachments.each_with_index.to_h do |attachment, index|
101
+ ["frame#{index}", { type: 'image', source: { type: 'base64', media_type: attachment.mime_type,
102
+ data: attachment.encoded } }]
103
+ end
104
+ { keyframes: frames }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'stringio'
5
+ require 'uri'
6
+
7
+ module RubyLLM
8
+ module Protocols
9
+ module Bedrock
10
+ # Shared Bedrock Model Invocation Job and S3 result lifecycle.
11
+ module Batches
12
+ include RubyLLM::Batch::Helpers
13
+
14
+ TERMINAL = %w[Completed PartiallyCompleted Failed Stopped Expired].freeze
15
+ private_constant :TERMINAL
16
+
17
+ def create_batch(requests)
18
+ model = single_batch_model!(requests, 'bedrock')
19
+ validate_bedrock_batch_requests!(requests)
20
+ role_arn = bedrock_batch_role_arn
21
+ input_uri, output_uri = bedrock_batch_storage_uris
22
+ @provider.upload_file(
23
+ StringIO.new(bedrock_batch_jsonl(requests)),
24
+ filename: 'input.jsonl',
25
+ uri: input_uri,
26
+ content_type: 'application/jsonl'
27
+ )
28
+
29
+ response = @provider.signed_post(@provider.control_api_base, '/model-invocation-job', {
30
+ clientRequestToken: Digest::SHA256.hexdigest(input_uri),
31
+ jobName: bedrock_job_name(input_uri, requests:),
32
+ roleArn: role_arn,
33
+ modelId: model,
34
+ modelInvocationType: bedrock_invocation_type,
35
+ inputDataConfig: {
36
+ s3InputDataConfig: { s3Uri: input_uri }
37
+ },
38
+ outputDataConfig: {
39
+ s3OutputDataConfig: { s3Uri: output_uri }
40
+ }
41
+ })
42
+
43
+ find_batch(response.body['jobArn'])
44
+ end
45
+
46
+ def find_batch(id)
47
+ data = @provider.signed_get(@provider.control_api_base, bedrock_job_url(id)).body
48
+ protocol = @provider.embedding_batch_protocol(data['modelId']) if data['modelInvocationType'] == 'InvokeModel'
49
+ parser = protocol ? protocol.new(@provider) : self
50
+ parser.send(:parse_batch_response, data).tap do |result|
51
+ result[:batch_protocol] = protocol if protocol
52
+ end
53
+ end
54
+
55
+ def cancel_batch(id)
56
+ @provider.signed_post(@provider.control_api_base, "#{bedrock_job_url(id)}/stop", {})
57
+ find_batch(id)
58
+ end
59
+
60
+ def batch_results(id)
61
+ job = @provider.signed_get(@provider.control_api_base, bedrock_job_url(id)).body
62
+ output_uri = job.dig('outputDataConfig', 's3OutputDataConfig', 's3Uri')
63
+ unless output_uri
64
+ status = parse_batch_status(job['status'], completed: TERMINAL.include?(job['status']))
65
+ return [] if %i[failed cancelled].include?(status)
66
+
67
+ raise Error, 'bedrock batch has no S3 output URI yet'
68
+ end
69
+
70
+ outputs = @provider.list_file_uris(output_uri)
71
+ .grep(/\.jsonl\.out\z/)
72
+ .reject { |uri| uri.end_with?('/manifest.json.out') }
73
+ .map { |uri| @provider.download_file(uri) }
74
+ parse_bedrock_outputs(outputs, model: job['modelId'])
75
+ end
76
+
77
+ private
78
+
79
+ def bedrock_batch_storage_uris
80
+ base = @config.bedrock_batch_s3_uri.to_s.sub(%r{/+\z}, '')
81
+ if base.empty?
82
+ raise ConfigurationError, 'Set bedrock_batch_s3_uri to an s3:// bucket prefix for Bedrock batches'
83
+ end
84
+
85
+ prefix = "#{base}/ruby_llm_batches/#{SecureRandom.hex(8)}"
86
+ ["#{prefix}/input.jsonl", "#{prefix}/output"]
87
+ end
88
+
89
+ def bedrock_batch_role_arn
90
+ @config.bedrock_batch_role_arn ||
91
+ raise(ConfigurationError, 'Set bedrock_batch_role_arn for Bedrock batches')
92
+ end
93
+
94
+ def bedrock_job_name(input_uri, **)
95
+ "ruby-llm-#{Digest::SHA256.hexdigest(input_uri)[0, 16]}"
96
+ end
97
+
98
+ def bedrock_job_url(id)
99
+ "/model-invocation-job/#{URI.encode_www_form_component(id)}"
100
+ end
101
+
102
+ def parse_batch_response(data)
103
+ status = data['status']
104
+ {
105
+ id: data['jobArn'] || data['jobIdentifier'],
106
+ raw_status: status,
107
+ completed: TERMINAL.include?(status),
108
+ request_counts: bedrock_request_counts(data)
109
+ }
110
+ end
111
+
112
+ def parse_batch_status(raw_status, completed:)
113
+ return :pending unless completed
114
+ return :succeeded if %w[Completed PartiallyCompleted].include?(raw_status)
115
+ return :cancelled if raw_status == 'Stopped'
116
+
117
+ :failed
118
+ end
119
+
120
+ def bedrock_request_counts(data)
121
+ {
122
+ 'submitted' => data['submitTime'],
123
+ 'completed' => data['endTime']
124
+ }.compact
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Bedrock
6
+ # S3-backed file storage for Bedrock batch input and output.
7
+ class Files < Protocols::Files
8
+ # rubocop:disable-next Lint/UnusedMethodArgument
9
+ def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
10
+ provider_options: {})
11
+ attachment = file_attachment(file, filename:)
12
+ target_uri = uri || storage_uri_for(attachment)
13
+ bucket, key = parse_s3_uri(target_uri)
14
+
15
+ with_file_body(attachment) do |body|
16
+ s3_client.put_object(bucket: bucket, key: key, body: body,
17
+ content_type: content_type || file_content_type(attachment))
18
+ end
19
+
20
+ uploaded_file(
21
+ { 'uri' => target_uri },
22
+ id: target_uri,
23
+ uri: target_uri,
24
+ filename: attachment.filename,
25
+ byte_size: file_size(attachment),
26
+ mime_type: content_type || file_content_type(attachment)
27
+ )
28
+ end
29
+
30
+ def find(file_id)
31
+ bucket, key = parse_s3_uri(file_id)
32
+ response = s3_client.head_object(bucket: bucket, key: key)
33
+
34
+ uploaded_file(
35
+ { 'uri' => file_id },
36
+ id: file_id,
37
+ uri: file_id,
38
+ filename: File.basename(key),
39
+ byte_size: response.content_length,
40
+ created_at: response.last_modified,
41
+ mime_type: response.content_type
42
+ )
43
+ end
44
+
45
+ def download(file_id)
46
+ bucket, key = parse_s3_uri(file_id)
47
+ s3_client.get_object(bucket: bucket, key: key).body.read
48
+ end
49
+
50
+ def list_uris(prefix_uri)
51
+ bucket, prefix = parse_s3_uri(prefix_uri)
52
+ uris = []
53
+ token = nil
54
+
55
+ loop do
56
+ options = { bucket: bucket, prefix: prefix }
57
+ options[:continuation_token] = token if token
58
+ response = s3_client.list_objects_v2(**options)
59
+ uris.concat(Array(response.contents).map { |object| "s3://#{bucket}/#{object.key}" })
60
+ token = response.next_continuation_token
61
+ break unless token
62
+ end
63
+
64
+ uris
65
+ end
66
+
67
+ private
68
+
69
+ def storage_uri_for(attachment)
70
+ base = @config.bedrock_batch_s3_uri.to_s.sub(%r{/+\z}, '')
71
+ raise ConfigurationError, 'Set bedrock_batch_s3_uri to an s3:// bucket prefix' if base.empty?
72
+
73
+ "#{base}/ruby_llm_uploads/#{SecureRandom.hex(8)}/#{attachment.filename}"
74
+ end
75
+
76
+ def s3_client
77
+ require 'aws-sdk-s3'
78
+
79
+ ::Aws::S3::Client.new(**s3_client_options)
80
+ rescue LoadError
81
+ raise Error, 'The aws-sdk-s3 gem is required for Bedrock file uploads. ' \
82
+ 'Please add it to your Gemfile: gem "aws-sdk-s3"'
83
+ end
84
+
85
+ def s3_client_options
86
+ options = { region: @config.bedrock_region }
87
+
88
+ if @config.bedrock_credential_provider
89
+ options[:credentials] = @config.bedrock_credential_provider
90
+ else
91
+ options.merge!(
92
+ access_key_id: @config.bedrock_api_key,
93
+ secret_access_key: @config.bedrock_secret_key,
94
+ session_token: @config.bedrock_session_token
95
+ )
96
+ end
97
+
98
+ options
99
+ end
100
+
101
+ def parse_s3_uri(uri)
102
+ parsed = URI(uri)
103
+ raise ArgumentError, "Expected an s3:// URI, got: #{uri}" unless parsed.scheme == 's3'
104
+
105
+ [parsed.host, parsed.path.delete_prefix('/')]
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Bedrock
6
+ # Moderation through an explicitly configured Bedrock guardrail.
7
+ class Guardrails < Protocol
8
+ POLICY_FILTERS = {
9
+ 'contentPolicy' => %w[filters],
10
+ 'topicPolicy' => %w[topics],
11
+ 'wordPolicy' => %w[customWords managedWordLists],
12
+ 'sensitiveInformationPolicy' => %w[piiEntities regexes],
13
+ 'contextualGroundingPolicy' => %w[filters]
14
+ }.freeze
15
+ private_constant :POLICY_FILTERS
16
+
17
+ IMAGE_TYPES = %w[image/png image/jpeg].freeze
18
+ INTERVENTIONS = %w[BLOCKED ANONYMIZED].freeze
19
+ private_constant :IMAGE_TYPES, :INTERVENTIONS
20
+
21
+ def moderate(input, model:, with: [], provider_options: {})
22
+ raise ArgumentError, 'Bedrock guardrails do not accept a model' unless model.nil?
23
+
24
+ url = @provider.guardrail_url
25
+ attachments = Attachment.wrap(with, config: @config)
26
+ validate_moderation_input(input, attachments)
27
+ payloads = (input.is_a?(Array) ? input : [input]).map do |text|
28
+ render_moderation_payload(text, attachments, provider_options)
29
+ end
30
+ track_usage(:moderation) do
31
+ responses = payloads.map do |payload|
32
+ response = post_moderation(url, payload)
33
+ result = parse_moderation_result(response)
34
+ @usage_tracker.succeed_attempts(tokens: [Tokens.new])
35
+ [response, result]
36
+ end
37
+ parse_moderation_responses(responses)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def parse_moderation_responses(responses)
44
+ RubyLLM::Moderation.new(
45
+ id: responses.one? ? responses.first.first.headers['x-amzn-requestid'] : nil,
46
+ model: nil,
47
+ results: responses.map(&:last),
48
+ raw: responses.one? ? responses.first.first.body : responses.map { |response, _result| response.body }
49
+ )
50
+ end
51
+
52
+ def validate_moderation_input(input, attachments)
53
+ unless valid_text_input?(input)
54
+ raise ArgumentError, 'Bedrock moderation requires text or a nonempty array of texts'
55
+ end
56
+ if input.is_a?(Array) && attachments.any?
57
+ raise ArgumentError, 'Bedrock moderation accepts image attachments with one text input at a time'
58
+ end
59
+ raise ArgumentError, 'Bedrock moderation requires text or an image' if input.nil? && attachments.empty?
60
+
61
+ attachments.each do |attachment|
62
+ next if IMAGE_TYPES.include?(attachment.mime_type)
63
+
64
+ raise UnsupportedAttachmentError, attachment.mime_type
65
+ end
66
+ end
67
+
68
+ def valid_text_input?(input)
69
+ input.nil? || input.is_a?(String) || (input.is_a?(Array) && input.any? && input.all?(String))
70
+ end
71
+
72
+ def render_moderation_payload(input, attachments, provider_options)
73
+ content = input.nil? ? [] : [{ text: { text: input } }]
74
+ content.concat(attachments.map do |attachment|
75
+ { image: { format: attachment.mime_type.delete_prefix('image/'), source: { bytes: attachment.encoded } } }
76
+ end)
77
+ { source: 'INPUT', content: }.merge(render_guardrail_options(provider_options))
78
+ end
79
+
80
+ def render_guardrail_options(provider_options)
81
+ options = provider_options.transform_keys(&:to_sym)
82
+ unknown = options.keys - %i[source outputScope]
83
+ unless unknown.empty?
84
+ raise ArgumentError, "Bedrock moderation options only support source and outputScope: #{unknown.join(', ')}"
85
+ end
86
+
87
+ options
88
+ end
89
+
90
+ def post_moderation(url, payload)
91
+ @connection.post(url, payload, usage: @usage_tracker) do |request|
92
+ request.headers.merge!(@provider.sign_headers('POST', url, JSON.generate(payload)))
93
+ end
94
+ end
95
+
96
+ def parse_moderation_result(response)
97
+ data = response.body
98
+ unless data.is_a?(Hash) && %w[NONE GUARDRAIL_INTERVENED].include?(data['action'])
99
+ raise Error.new('Bedrock moderation returned no recognized verdict', response:)
100
+ end
101
+
102
+ categories = Array(data['assessments']).flat_map { |assessment| parse_moderation_categories(assessment) }
103
+ RubyLLM::Moderation::Result.new(
104
+ flagged: data['action'] == 'GUARDRAIL_INTERVENED', categories: categories.uniq, category_scores: {}
105
+ )
106
+ end
107
+
108
+ def parse_moderation_categories(assessment)
109
+ POLICY_FILTERS.flat_map do |policy, groups|
110
+ groups.flat_map do |group|
111
+ Array(assessment.dig(policy, group)).filter_map do |filter|
112
+ next unless INTERVENTIONS.include?(filter['action'])
113
+
114
+ filter['name'] || filter['type'] || "#{policy}.#{group}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Bedrock
6
+ # Bedrock's document reranking API.
7
+ class Rerank < Protocol
8
+ def rerank(query, documents, model:, top_n: nil, provider_options: {})
9
+ track_usage(:rerank) do
10
+ payload = render_rerank_payload(query, documents, model:, top_n:, provider_options:)
11
+ pages = rerank_pages(payload)
12
+ parse_rerank_pages(pages, model:, documents:)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def render_rerank_payload(query, documents, model:, top_n:, provider_options:)
19
+ validate_rerank_input(query, documents, top_n)
20
+ payload = {
21
+ queries: [{ type: 'TEXT', textQuery: { text: query } }],
22
+ sources: documents.map { |document| render_rerank_source(document) },
23
+ rerankingConfiguration: {
24
+ type: 'BEDROCK_RERANKING_MODEL',
25
+ bedrockRerankingConfiguration: {
26
+ modelConfiguration: { modelArn: @provider.rerank_model_arn(model) },
27
+ numberOfResults: top_n || documents.length
28
+ }
29
+ }
30
+ }
31
+ Support::Utils.deep_merge(payload, provider_options)
32
+ end
33
+
34
+ def validate_rerank_input(query, documents, top_n)
35
+ unless query.is_a?(String) && !query.empty?
36
+ raise ArgumentError, 'Bedrock reranking requires one nonempty text query'
37
+ end
38
+ unless documents.is_a?(Array) && (1..1000).cover?(documents.length)
39
+ raise ArgumentError, 'Bedrock reranking accepts between 1 and 1000 documents'
40
+ end
41
+ return if top_n.nil? || (top_n.is_a?(Integer) && (1..1000).cover?(top_n))
42
+
43
+ raise ArgumentError, 'Bedrock reranking top_n must be between 1 and 1000'
44
+ end
45
+
46
+ def render_rerank_source(document)
47
+ content = case document
48
+ when String then { type: 'TEXT', textDocument: { text: document } }
49
+ when Hash then { type: 'JSON', jsonDocument: document }
50
+ else raise ArgumentError, 'Bedrock reranking documents must be text or JSON objects'
51
+ end
52
+ { type: 'INLINE', inlineDocumentSource: content }
53
+ end
54
+
55
+ def rerank_pages(payload)
56
+ pages = []
57
+ seen = []
58
+ loop do
59
+ response = post_rerank(payload)
60
+ @usage_tracker.succeed_attempts(tokens: [Tokens.new])
61
+ pages << response.body
62
+ token = response.body['nextToken']
63
+ break if token.nil? || token.empty?
64
+ raise Error.new('Bedrock reranking returned a repeated pagination token', response:) if seen.include?(token)
65
+
66
+ seen << token
67
+ payload = payload.merge(nextToken: token)
68
+ end
69
+ pages
70
+ end
71
+
72
+ def post_rerank(payload)
73
+ @provider.agent_connection.post('/rerank', payload, usage: @usage_tracker) do |request|
74
+ request.headers.merge!(@provider.sign_headers('POST', '/rerank', JSON.generate(payload),
75
+ base_url: @provider.agent_api_base))
76
+ end
77
+ end
78
+
79
+ def parse_rerank_pages(pages, model:, documents:)
80
+ results = pages.flat_map do |page|
81
+ Array(page['results']).map do |item|
82
+ index = item['index']
83
+ unless index.is_a?(Integer) && index.between?(0, documents.length - 1)
84
+ raise Error, 'Bedrock reranking returned an invalid document index'
85
+ end
86
+
87
+ RubyLLM::Rerank::Result.new(index:, document: documents[index], score: item['relevanceScore'])
88
+ end
89
+ end
90
+ RubyLLM::Rerank.new(results:, model:, raw: pages.length == 1 ? pages.first : pages)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # OpenAI-compatible file-backed Batch API for Chat Completions.
7
+ module Batches
8
+ include Protocols::OpenAI::Batches
9
+
10
+ private
11
+
12
+ def batch_endpoint
13
+ '/v1/chat/completions'
14
+ end
15
+
16
+ def validate_batch_requests!(requests)
17
+ return if requests.all? { |request| chat_completion_payload?(request.fetch(:payload)) }
18
+
19
+ raise Error, "#{@provider.slug} batch requests require chat completion payloads"
20
+ end
21
+
22
+ def chat_completion_payload?(payload)
23
+ payload.key?(:messages) || payload.key?('messages')
24
+ end
25
+
26
+ def parse_batch_completion_response(body)
27
+ parse_completion_body(body, raw: body)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end