ruby_llm 1.15.0 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -6
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1208 -150
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +274 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +155 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +540 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/protocols/chat_completions/media.rb +121 -0
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +17 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Converse
6
+ # Media formatting for Bedrock Converse content blocks.
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content, attachments = [], used_document_names: nil, citations: false)
11
+ used_document_names ||= {}
12
+ blocks = []
13
+ blocks << { text: content } unless content.nil? || content.empty?
14
+ attachments.each do |attachment|
15
+ blocks << format_attachment(attachment, used_document_names:, citations:)
16
+ end
17
+ blocks
18
+ end
19
+
20
+ def format_attachment(attachment, used_document_names:, citations: false)
21
+ if attachment.provider_file?
22
+ return format_provider_file_attachment(attachment, used_document_names:, citations:)
23
+ end
24
+
25
+ case attachment.type
26
+ when :image
27
+ format_image_attachment(attachment)
28
+ when :audio
29
+ format_audio_attachment(attachment)
30
+ when :video
31
+ format_video_attachment(attachment)
32
+ when :pdf, :document
33
+ format_document_attachment(attachment, used_document_names:, citations:)
34
+ when :text
35
+ if citations
36
+ format_text_document_attachment(attachment, used_document_names:)
37
+ else
38
+ format_text_attachment(attachment)
39
+ end
40
+ else
41
+ raise UnsupportedAttachmentError, attachment.mime_type
42
+ end
43
+ end
44
+
45
+ SUPPORTED_DOCUMENT_FORMATS = %w[pdf csv doc docx xls xlsx html txt md].freeze
46
+ SUPPORTED_AUDIO_FORMATS = %w[mp3 opus wav aac flac mp4 ogg mkv mka x-aac m4a mpeg mpga pcm webm].freeze
47
+ SUPPORTED_VIDEO_FORMATS = %w[mkv mov mp4 webm flv mpeg mpg wmv three_gp].freeze
48
+ AUDIO_FORMAT_ALIASES = {
49
+ 'x-flac' => 'flac',
50
+ 'x-m4a' => 'm4a'
51
+ }.freeze
52
+ VIDEO_FORMAT_ALIASES = {
53
+ 'quicktime' => 'mov',
54
+ 'x-matroska' => 'mkv',
55
+ 'x-ms-wmv' => 'wmv',
56
+ '3gp' => 'three_gp',
57
+ '3gpp' => 'three_gp'
58
+ }.freeze
59
+
60
+ def format_image_attachment(attachment)
61
+ {
62
+ image: {
63
+ format: attachment.format,
64
+ source: {
65
+ bytes: attachment.encoded
66
+ }
67
+ }
68
+ }
69
+ end
70
+
71
+ def format_audio_attachment(attachment)
72
+ {
73
+ audio: {
74
+ format: media_format(attachment, AUDIO_FORMAT_ALIASES, SUPPORTED_AUDIO_FORMATS),
75
+ source: {
76
+ bytes: attachment.encoded
77
+ }
78
+ }
79
+ }
80
+ end
81
+
82
+ def format_video_attachment(attachment)
83
+ {
84
+ video: {
85
+ format: media_format(attachment, VIDEO_FORMAT_ALIASES, SUPPORTED_VIDEO_FORMATS),
86
+ source: {
87
+ bytes: attachment.encoded
88
+ }
89
+ }
90
+ }
91
+ end
92
+
93
+ def media_format(attachment, aliases, supported)
94
+ format = attachment.format.to_s
95
+ format = aliases.fetch(format, format)
96
+ raise UnsupportedAttachmentError, attachment.mime_type unless supported.include?(format)
97
+
98
+ format
99
+ end
100
+
101
+ def format_text_attachment(attachment)
102
+ { text: attachment.for_llm }
103
+ end
104
+
105
+ def format_document_attachment(attachment, used_document_names:, citations: false)
106
+ format = document_format(attachment)
107
+
108
+ raise UnsupportedAttachmentError, attachment.mime_type unless supported_document_format?(attachment)
109
+
110
+ document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
111
+ document = {
112
+ format: format,
113
+ name: document_name,
114
+ source: {
115
+ bytes: attachment.encoded
116
+ }
117
+ }
118
+ document[:citations] = { enabled: true } if citations
119
+ { document: document }
120
+ end
121
+
122
+ # Text attachments become citable documents when citations are
123
+ # enabled. Bedrock rejects text-format documents sent as bytes when
124
+ # citations are on, so the text rides in the source's text member.
125
+ def format_text_document_attachment(attachment, used_document_names:)
126
+ document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
127
+ {
128
+ document: {
129
+ format: 'txt',
130
+ name: document_name,
131
+ source: { text: attachment.content },
132
+ citations: { enabled: true }
133
+ }
134
+ }
135
+ end
136
+
137
+ def format_provider_file_attachment(attachment, used_document_names:, citations: false)
138
+ raise UnsupportedAttachmentError, attachment.mime_type unless supported_document_format?(attachment)
139
+
140
+ document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
141
+ document = {
142
+ format: document_format(attachment),
143
+ name: document_name,
144
+ source: {
145
+ s3Location: {
146
+ uri: attachment.provider_file_uri || attachment.provider_file_id
147
+ }
148
+ }
149
+ }
150
+ document[:citations] = { enabled: true } if citations
151
+ { document: document }
152
+ end
153
+
154
+ def supported_document_format?(attachment)
155
+ SUPPORTED_DOCUMENT_FORMATS.include?(document_format(attachment))
156
+ end
157
+
158
+ def document_format(attachment)
159
+ attachment.extension || attachment.format
160
+ end
161
+
162
+ def sanitize_document_name(filename)
163
+ base = File.basename(filename.to_s, '.*')
164
+ safe = base.gsub(/[^a-zA-Z0-9_-]/, '_')
165
+ safe.empty? ? 'document' : safe
166
+ end
167
+
168
+ def unique_document_name(base_name, used_names)
169
+ count = used_names[base_name].to_i
170
+ used_names[base_name] = count + 1
171
+ return base_name if count.zero?
172
+
173
+ "#{base_name}_#{count + 1}"
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'faraday'
5
+ require 'json'
6
+
7
+ module RubyLLM
8
+ module Protocols
9
+ class Converse
10
+ # Streaming implementation for Bedrock ConverseStream (AWS Event Stream).
11
+ module Streaming
12
+ ErrorResponse = Struct.new(:body, :status)
13
+
14
+ private
15
+
16
+ def stream_url
17
+ "/model/#{escape_model_id(@model.id)}/converse-stream"
18
+ end
19
+
20
+ def stream_response(payload, additional_headers = {}, &block)
21
+ accumulator = RubyLLM::Protocol::StreamAccumulator.new
22
+ decoder = event_stream_decoder
23
+ body = JSON.generate(payload)
24
+ progress = {}
25
+
26
+ faraday_v1 = Faraday::VERSION.start_with?('1')
27
+ on_data = RubyLLM::Protocol::Streaming::FaradayHandlers.build(
28
+ faraday_v1: faraday_v1,
29
+ on_chunk: ->(chunk, _env) { parse_stream_chunk(decoder, chunk, accumulator, progress, &block) },
30
+ on_failed_response: ->(chunk, env) { handle_failed_stream(chunk, env) }
31
+ )
32
+
33
+ response = @connection.post(stream_url, payload, usage: @usage_tracker) do |req|
34
+ req.headers.merge!(@provider.sign_headers('POST', stream_url, body))
35
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
36
+ req.headers['Accept'] = 'application/vnd.amazon.eventstream'
37
+ (req.options.context ||= {})[RubyLLM::Transport::Connection::STREAM_PROGRESS_KEY] = progress
38
+
39
+ if faraday_v1
40
+ req.options[:on_data] = on_data
41
+ else
42
+ req.options.on_data = on_data
43
+ end
44
+ end
45
+
46
+ message = accumulator.to_message(response)
47
+ RubyLLM.logger.debug { "Stream completed: #{message.content}" }
48
+ message
49
+ end
50
+
51
+ def event_stream_decoder
52
+ require 'aws-eventstream'
53
+ Aws::EventStream::Decoder.new
54
+ rescue LoadError
55
+ raise Error,
56
+ 'The aws-eventstream gem is required for Bedrock streaming. ' \
57
+ 'Please add it to your Gemfile: gem "aws-eventstream"'
58
+ end
59
+
60
+ # The error body arrives in as many reads as the adapter hands out, so
61
+ # it accumulates on the env, which ErrorMiddleware clears per attempt.
62
+ def handle_failed_stream(chunk, env)
63
+ buffer = failed_stream_buffer(env)
64
+ buffer << chunk
65
+ error_response = env.merge(body: JSON.parse(buffer))
66
+ Transport::ErrorMiddleware.parse_error(provider: self, response: error_response)
67
+ rescue JSON::ParserError
68
+ RubyLLM.logger.debug { "Accumulating Bedrock stream error chunk: #{chunk}" }
69
+ end
70
+
71
+ def failed_stream_buffer(env)
72
+ (env[:streaming_state] ||= RubyLLM::Protocol::Streaming::StreamState.new).buffer
73
+ end
74
+
75
+ def parse_stream_chunk(decoder, raw_chunk, accumulator, progress)
76
+ handle_non_eventstream_error_chunk(raw_chunk)
77
+
78
+ decode_events(decoder, raw_chunk).each do |event|
79
+ chunk = build_chunk(event)
80
+ next unless chunk
81
+
82
+ accumulator.add(chunk)
83
+ progress[:started] = true
84
+ yield chunk
85
+ end
86
+ end
87
+
88
+ def handle_non_eventstream_error_chunk(raw_chunk)
89
+ text = raw_chunk.to_s
90
+
91
+ if text.start_with?('event: error')
92
+ payload = text.lines.find { |line| line.start_with?('data:') }&.delete_prefix('data:')&.strip
93
+ raise_streaming_chunk_error(payload) if payload
94
+ return
95
+ end
96
+
97
+ return unless text.lstrip.start_with?('{') && text.include?('"error"')
98
+
99
+ raise_streaming_chunk_error(text)
100
+ end
101
+
102
+ def raise_streaming_chunk_error(payload)
103
+ parsed = JSON.parse(payload)
104
+ message = parsed.dig('error', 'message') || parsed['message'] || 'Bedrock streaming error'
105
+ response = ErrorResponse.new({ 'message' => message }, 500)
106
+ Transport::ErrorMiddleware.parse_error(provider: self, response: response)
107
+ rescue JSON::ParserError
108
+ nil
109
+ end
110
+
111
+ def decode_events(decoder, raw_chunk)
112
+ events = []
113
+ message, eof = decoder.decode_chunk(raw_chunk)
114
+
115
+ while message
116
+ event = decode_event(message)
117
+ if event && RubyLLM.config.log_stream_debug
118
+ RubyLLM.logger.debug do
119
+ "Bedrock stream event keys: #{event.keys}"
120
+ end
121
+ end
122
+ events << event if event
123
+ break if eof
124
+
125
+ message, eof = decoder.decode_chunk
126
+ end
127
+
128
+ events
129
+ end
130
+
131
+ # ConverseStream delivers modeled errors as a frame after the 200, with
132
+ # the type in the frame headers and a bare message as the payload. Rewrap
133
+ # it under the exception name so raise_stream_error classifies it instead
134
+ # of the frame passing for an empty chunk.
135
+ def decode_event(message)
136
+ return decode_error_frame(message.headers) if stream_error_frame?(message)
137
+
138
+ exception_type = stream_exception_type(message)
139
+ payload = message.payload.read
140
+ event = decode_event_payload(payload)
141
+ return event unless exception_type
142
+
143
+ { exception_type => event || { 'message' => payload.to_s } }
144
+ end
145
+
146
+ # Failures the API does not model arrive as an error frame instead,
147
+ # with the code and message in the headers and nothing in the payload.
148
+ def stream_error_frame?(message)
149
+ event_header(message.headers, ':message-type') == 'error'
150
+ end
151
+
152
+ def decode_error_frame(headers)
153
+ detail = [event_header(headers, ':error-code'), event_header(headers, ':error-message')].compact.join(': ')
154
+ event = { 'type' => 'error' }
155
+ event['error'] = { 'message' => detail } unless detail.empty?
156
+ event
157
+ end
158
+
159
+ def stream_exception_type(message)
160
+ headers = message.headers
161
+ return nil unless event_header(headers, ':message-type') == 'exception'
162
+
163
+ event_header(headers, ':exception-type') || 'streamingException'
164
+ end
165
+
166
+ def event_header(headers, name)
167
+ header = headers[name]
168
+ header.respond_to?(:value) ? header.value : header
169
+ end
170
+
171
+ def decode_event_payload(payload)
172
+ outer = JSON.parse(payload)
173
+
174
+ if outer['bytes'].is_a?(String)
175
+ JSON.parse(Base64.decode64(outer['bytes']))
176
+ else
177
+ outer
178
+ end
179
+ rescue JSON::ParserError => e
180
+ RubyLLM.logger.debug { "Failed to decode Bedrock stream event payload: #{e.message}" }
181
+ nil
182
+ end
183
+
184
+ def build_chunk(event)
185
+ raise_stream_error(event) if stream_error_event?(event)
186
+
187
+ metadata_usage, usage = event_usage(event)
188
+
189
+ Chunk.new(
190
+ role: :assistant,
191
+ model: event['modelId'] || @model&.id,
192
+ content: extract_content_delta(event),
193
+ citations: extract_citations_delta(event),
194
+ thinking: Thinking.build(
195
+ text: extract_thinking_delta(event),
196
+ signature: extract_thinking_signature(event)
197
+ ),
198
+ tool_calls: extract_tool_calls(event),
199
+ server_tool_calls: extract_server_tool_call_events(event),
200
+ input_tokens: extract_input_tokens(metadata_usage, usage),
201
+ output_tokens: extract_output_tokens(metadata_usage, usage),
202
+ cache_read_tokens: extract_cache_read_tokens(metadata_usage, usage),
203
+ cache_write_tokens: extract_cache_write_tokens(metadata_usage, usage),
204
+ thinking_tokens: extract_reasoning_tokens(metadata_usage, usage),
205
+ finish_reason: extract_finish_reason(event)
206
+ )
207
+ end
208
+
209
+ def extract_finish_reason(event)
210
+ normalize_finish_reason(event.dig('messageStop', 'stopReason') || event['stopReason'])
211
+ end
212
+
213
+ def event_usage(event)
214
+ [
215
+ event.dig('metadata', 'usage') || {},
216
+ event['usage'] || {}
217
+ ]
218
+ end
219
+
220
+ def extract_input_tokens(metadata_usage, usage)
221
+ bedrock_usage = metadata_usage['inputTokens'] ? metadata_usage : usage
222
+ Chat.input_tokens(bedrock_usage) if bedrock_usage['inputTokens']
223
+ end
224
+
225
+ def extract_output_tokens(metadata_usage, usage)
226
+ metadata_usage['outputTokens'] || usage['outputTokens']
227
+ end
228
+
229
+ def extract_cache_read_tokens(metadata_usage, usage)
230
+ metadata_usage['cacheReadInputTokens'] || usage['cacheReadInputTokens']
231
+ end
232
+
233
+ def extract_cache_write_tokens(metadata_usage, usage)
234
+ metadata_usage['cacheWriteInputTokens'] || usage['cacheWriteInputTokens']
235
+ end
236
+
237
+ def extract_reasoning_tokens(metadata_usage, usage)
238
+ Chat.reasoning_tokens(metadata_usage) || Chat.reasoning_tokens(usage)
239
+ end
240
+
241
+ def stream_error_event?(event)
242
+ event.keys.any? { |key| key.end_with?('Exception') } || event['type'] == 'error'
243
+ end
244
+
245
+ def raise_stream_error(event)
246
+ if event['type'] == 'error'
247
+ message = event.dig('error', 'message') || 'Bedrock streaming error'
248
+ response = ErrorResponse.new({ 'message' => message }, 500)
249
+ Transport::ErrorMiddleware.parse_error(provider: self, response: response)
250
+ return
251
+ end
252
+
253
+ key = event.keys.find { |candidate| candidate.end_with?('Exception') }
254
+ payload = event[key]
255
+ message = payload['message'] || key
256
+ status = case key
257
+ when 'throttlingException' then 429
258
+ when 'validationException' then 400
259
+ when 'accessDeniedException', 'unrecognizedClientException' then 401
260
+ when 'serviceUnavailableException' then 503
261
+ else 500
262
+ end
263
+
264
+ response = ErrorResponse.new({ 'message' => message }, status)
265
+ Transport::ErrorMiddleware.parse_error(provider: self, response: response)
266
+ end
267
+
268
+ def extract_content_delta(event)
269
+ normalized_delta(event)['text']
270
+ end
271
+
272
+ # The cited span itself streams as ordinary text deltas; the citation
273
+ # delta carries the source. The final message resolves the span text
274
+ # from the accumulated content when indices are known.
275
+ def extract_citations_delta(event)
276
+ citation = normalized_delta(event)['citation']
277
+ return nil unless citation.is_a?(Hash)
278
+
279
+ [Chat.parse_citation(citation)]
280
+ end
281
+
282
+ def extract_thinking_delta(event)
283
+ reasoning_content = normalized_delta(event)['reasoningContent'] || {}
284
+
285
+ reasoning_text = reasoning_content['reasoningText'] || {}
286
+ return reasoning_text['text'] if reasoning_text.key?('text')
287
+ return reasoning_content['text'] if reasoning_content.key?('text')
288
+ return '' if [reasoning_text, reasoning_content].any? { |block| block.key?('signature') }
289
+
290
+ extract_thinking_from_start(event)
291
+ end
292
+
293
+ def extract_thinking_from_start(event)
294
+ start = event.dig('contentBlockStart', 'start', 'reasoningContent') || {}
295
+ return unless start.key?('reasoningText')
296
+
297
+ start.dig('reasoningText', 'text').to_s
298
+ end
299
+
300
+ def extract_thinking_signature(event)
301
+ signature = extract_signature_from_delta(event)
302
+ return signature if signature
303
+
304
+ signature = extract_signature_from_start(event)
305
+ return signature if signature
306
+
307
+ nil
308
+ end
309
+
310
+ def extract_signature_from_delta(event)
311
+ reasoning_content = normalized_delta(event)['reasoningContent'] || {}
312
+ reasoning_text = reasoning_content['reasoningText'] || {}
313
+ reasoning_text['signature'] || reasoning_content['signature'] || reasoning_content['redactedContent']
314
+ end
315
+
316
+ def extract_signature_from_start(event)
317
+ start = event.dig('contentBlockStart', 'start', 'reasoningContent')
318
+ return nil unless start
319
+
320
+ reasoning_text = start['reasoningText'] || {}
321
+ return reasoning_text['signature'] if reasoning_text['signature']
322
+ return start['redactedContent'] if start['redactedContent']
323
+
324
+ nil
325
+ end
326
+
327
+ def extract_tool_calls(event)
328
+ return extract_tool_call_start(event) if tool_call_start_event?(event)
329
+ return extract_tool_call_delta(event) if tool_call_delta_event?(event)
330
+
331
+ nil
332
+ end
333
+
334
+ def tool_call_start_event?(event)
335
+ event['contentBlockStart'] || event['start']
336
+ end
337
+
338
+ def tool_call_delta_event?(event)
339
+ event['contentBlockDelta'] || event.dig('delta', 'toolUse')
340
+ end
341
+
342
+ def extract_tool_call_start(event)
343
+ tool_use = event.dig('contentBlockStart', 'start', 'toolUse') || event.dig('start', 'toolUse')
344
+ return nil unless tool_use
345
+
346
+ if Chat.server_tool_use?(tool_use)
347
+ remember_server_tool_block(event)
348
+ return nil
349
+ end
350
+
351
+ tool_use_id = tool_use['toolUseId']
352
+ {
353
+ tool_use_id => ToolCall.new(
354
+ id: tool_use_id,
355
+ name: tool_use['name'],
356
+ arguments: tool_use['input'] || {}
357
+ )
358
+ }
359
+ end
360
+
361
+ def extract_tool_call_delta(event)
362
+ return nil if server_tool_block_event?(event)
363
+
364
+ input = normalized_delta(event).dig('toolUse', 'input')
365
+ return nil unless input
366
+
367
+ { nil => ToolCall.new(id: nil, name: nil, arguments: input) }
368
+ end
369
+
370
+ # Server-executed tool steps stream as typed toolUse and toolResult
371
+ # block starts. Their input deltas are not function-call arguments,
372
+ # so the block index is remembered and its deltas skipped.
373
+ def extract_server_tool_call_events(event)
374
+ start = event.dig('contentBlockStart', 'start') || event['start']
375
+ return [] unless start.is_a?(Hash)
376
+
377
+ tool_use = start['toolUse']
378
+ tool_result = start['toolResult']
379
+ if tool_use && Chat.server_tool_use?(tool_use)
380
+ [ServerToolCall.new(type: tool_use['type'], name: tool_use['name'], id: tool_use['toolUseId'],
381
+ input: tool_use['input'], raw: start)]
382
+ elsif tool_result && Chat.server_tool_result?(tool_result)
383
+ [ServerToolCall.new(type: tool_result['type'], id: tool_result['toolUseId'],
384
+ result: tool_result['content'], raw: start)]
385
+ else
386
+ []
387
+ end
388
+ end
389
+
390
+ def remember_server_tool_block(event)
391
+ index = event_block_index(event)
392
+ return if index.nil?
393
+
394
+ @server_tool_block_indices ||= {}
395
+ @server_tool_block_indices[index] = true
396
+ end
397
+
398
+ def server_tool_block_event?(event)
399
+ index = event_block_index(event)
400
+ !index.nil? && @server_tool_block_indices&.key?(index)
401
+ end
402
+
403
+ def event_block_index(event)
404
+ event['contentBlockIndex'] ||
405
+ event.dig('contentBlockStart', 'contentBlockIndex') ||
406
+ event.dig('contentBlockDelta', 'contentBlockIndex')
407
+ end
408
+
409
+ def normalized_delta(event)
410
+ delta = event.dig('contentBlockDelta', 'delta') || event['delta'] || {}
411
+ return delta if delta.is_a?(Hash)
412
+
413
+ if delta.is_a?(String) && !delta.empty?
414
+ JSON.parse(delta)
415
+ else
416
+ {}
417
+ end
418
+ rescue JSON::ParserError
419
+ {}
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The AWS Bedrock Converse API. Requests are SigV4-signed by the provider.
6
+ class Converse < Protocol
7
+ REGION_PREFIXES = %w[global us-gov us eu apac ap sa ca me af il au jp].freeze
8
+
9
+ include Converse::Chat
10
+ include Converse::Media
11
+ include Converse::Streaming
12
+
13
+ # Nova 2 models execute built-in tools server-side when the request
14
+ # names them as system tools.
15
+ SERVER_TOOL_ALIASES = {
16
+ web_search: lambda { |options|
17
+ { tool: { systemTool: { name: 'nova_grounding' }.merge(Support::Utils.deep_symbolize_keys(options)) } }
18
+ }
19
+ }.freeze
20
+
21
+ def server_tool_aliases
22
+ SERVER_TOOL_ALIASES
23
+ end
24
+
25
+ private
26
+
27
+ # Converse carries tools under toolConfig.tools rather than a
28
+ # top-level tools array.
29
+ def merge_server_tool_entries(payload, entries)
30
+ tool_config = payload[:toolConfig] ||= {}
31
+ tool_config[:tools] = Array(tool_config[:tools]) + entries
32
+ payload
33
+ end
34
+
35
+ def sync_response(payload, additional_headers = {})
36
+ response = signed_post(completion_url, payload, additional_headers)
37
+ parse_completion_response(response)
38
+ end
39
+
40
+ def post_count_tokens(payload)
41
+ signed_post(count_tokens_url, payload)
42
+ end
43
+
44
+ def signed_post(url, payload, additional_headers = {})
45
+ body = JSON.generate(payload)
46
+
47
+ @connection.post(url, payload, usage: @usage_tracker) do |req|
48
+ req.headers.merge!(@provider.sign_headers('POST', url, body))
49
+ req.headers.merge!(additional_headers) unless additional_headers.empty?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end