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,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
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Deepgram
6
+ # Model catalog for Deepgram. GET v1/models splits the catalog into
7
+ # stt and tts groups, and names each entry twice: +name+ is the short
8
+ # name, such as the voice 'zeus', while +canonical_name+ is the id the
9
+ # listen and speak endpoints accept.
10
+ module Models
11
+ module_function
12
+
13
+ def models_url
14
+ 'v1/models'
15
+ end
16
+
17
+ def parse_list_models_response(response, slug)
18
+ body = response.body || {}
19
+
20
+ %w[stt tts].flat_map do |group|
21
+ Array(body[group]).group_by { |data| data['canonical_name'] || data['name'] }
22
+ .map { |_, rows| build_model(merge_language_rows(rows), slug, group) }
23
+ end
24
+ end
25
+
26
+ # Deepgram lists a model once per language it was trained on, so
27
+ # nova-3-general arrives 146 times with a different language and
28
+ # version each time. One model answers to that id, so the rows
29
+ # collapse into it, carrying every language between them. Fields
30
+ # the rows disagree on describe a single language rather than the
31
+ # model, and are left out.
32
+ def merge_language_rows(rows)
33
+ merged = rows.first.merge('languages' => rows.flat_map { |row| Array(row['languages']) }.uniq.sort)
34
+ %w[version architecture].each do |key|
35
+ merged.delete(key) unless rows.map { |row| row[key] }.uniq.one?
36
+ end
37
+ merged
38
+ end
39
+
40
+ def build_model(data, slug, group)
41
+ model_id = data['canonical_name'] || data['name']
42
+ transcription = group == 'stt' && !model_id.start_with?('flux-')
43
+ modalities = if group == 'stt'
44
+ { input: ['audio'], output: ['text'] }
45
+ else
46
+ { input: ['text'], output: ['audio'] }
47
+ end
48
+
49
+ Model.new(
50
+ id: model_id,
51
+ name: model_id,
52
+ provider: slug,
53
+ family: data['architecture'],
54
+ modalities: modalities,
55
+ capabilities: transcription ? ['transcription'] : [],
56
+ pricing: {},
57
+ metadata: build_metadata(data)
58
+ )
59
+ end
60
+
61
+ def build_metadata(data)
62
+ metadata = {
63
+ architecture: data['architecture'],
64
+ languages: data['languages'],
65
+ version: data['version']
66
+ }
67
+ metadata[:voice] = data['name'] if data['name'] && data['name'] != data['canonical_name']
68
+ metadata.merge!(data['metadata'].transform_keys(&:to_sym)) if data['metadata'].is_a?(Hash)
69
+ metadata.compact
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Deepgram
8
+ # Speech dialect for the Deepgram text-to-speech API. Deepgram names
9
+ # the voice inside the model id, so aura-2-thalia-en is the Thalia
10
+ # voice of Aura 2: asking for a +voice:+ swaps that segment of the
11
+ # model id. The container is an encoding query value, the body carries
12
+ # only the text, and the response is raw audio bytes.
13
+ module Speech
14
+ DEFAULT_FORMAT = 'mp3'
15
+
16
+ # RubyLLM format names in Deepgram's encoding vocabulary. Deepgram
17
+ # wraps linear16 in a WAV header unless the container is turned off,
18
+ # and the compressed encodings carry their own.
19
+ OUTPUT_FORMATS = {
20
+ 'aac' => { encoding: 'aac' },
21
+ 'alaw' => { encoding: 'alaw', container: 'wav' },
22
+ 'flac' => { encoding: 'flac' },
23
+ 'mp3' => { encoding: 'mp3' },
24
+ 'mulaw' => { encoding: 'mulaw', container: 'wav' },
25
+ 'opus' => { encoding: 'opus' },
26
+ 'pcm' => { encoding: 'linear16', container: 'none' },
27
+ 'wav' => { encoding: 'linear16', container: 'wav' }
28
+ }.freeze
29
+
30
+ def speak(input, model:, voice:, format:, provider_options: {}, &block)
31
+ track_usage(:speech) do
32
+ spoken_model = speech_model_for(model, voice)
33
+ payload = render_speech_payload(input, model: spoken_model, voice:, format:)
34
+ url = speech_url(model: spoken_model, format:, provider_options:)
35
+ if block
36
+ next stream_speech_response(url, payload, model: spoken_model, voice: voice_for(spoken_model),
37
+ format:, &block)
38
+ end
39
+
40
+ response = @connection.post url, payload, usage: @usage_tracker
41
+ parse_speech_response(response, model: spoken_model, voice: voice_for(spoken_model), format:)
42
+ end
43
+ end
44
+
45
+ def speech_url(model:, format: nil, provider_options: {})
46
+ "v1/speak?#{URI.encode_www_form(speech_params(model:, format:, provider_options:))}"
47
+ end
48
+
49
+ def speech_params(model:, format: nil, provider_options: {})
50
+ { model: model }.merge(OUTPUT_FORMATS.fetch(format_for(format), encoding: format_for(format)))
51
+ .merge(provider_options).compact
52
+ end
53
+
54
+ # rubocop:disable-next Lint/UnusedMethodArgument
55
+ def render_speech_payload(input, model:, voice: nil, format: nil, provider_options: {})
56
+ { text: input }
57
+ end
58
+
59
+ def parse_speech_response(response, model:, voice:, format:)
60
+ RubyLLM::Speech.new(
61
+ data: response.body,
62
+ model: model,
63
+ voice: voice,
64
+ format: format_for(format)
65
+ )
66
+ end
67
+
68
+ # Puts +voice+ into the voice segment of +model+, so Aura 2 with
69
+ # voice 'zeus' is aura-2-zeus-en. A voice that already names a whole
70
+ # model is used as it stands.
71
+ def speech_model_for(model, voice)
72
+ return model unless voice
73
+
74
+ segments = model.to_s.split('-')
75
+ return voice if voice.include?('-') || segments.size < 3
76
+
77
+ (segments[0..-3] + [voice, segments[-1]]).join('-')
78
+ end
79
+
80
+ # The voice segment of a Deepgram model id, so aura-2-thalia-en
81
+ # speaks as 'thalia'.
82
+ def voice_for(model)
83
+ segments = model.to_s.split('-')
84
+ segments[-2] if segments.size >= 3
85
+ end
86
+
87
+ def format_for(format)
88
+ (format || DEFAULT_FORMAT).to_s
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Deepgram
6
+ module StreamingTranscription # :nodoc: all
7
+ def stream_live_transcription(audio_file, model:, language:, speaker_names:, provider_options:, prompt:, &block)
8
+ attachment = Attachment.wrap(audio_file, config: @config)
9
+ raise ArgumentError, 'Transcription requires exactly one audio file' unless attachment.one?
10
+
11
+ track_usage(:transcription) do
12
+ url = streaming_transcription_url(model:, language:, speaker_names:, provider_options:, prompt:)
13
+ segments = []
14
+ metadata = nil
15
+ @usage_tracker.start
16
+ Transport::WebsocketConnection.open(url, headers: @provider.headers, config: @config) do |socket|
17
+ write = ->(connection) { send_transcription_audio(connection, attachment.first.content) }
18
+ socket.each_message(write:) do |message|
19
+ event = JSON.parse(message)
20
+ case event['type']
21
+ when 'Results'
22
+ process_transcription_result(event, segments, &block)
23
+ when 'Metadata'
24
+ metadata = event
25
+ when 'Error'
26
+ raise Error, event['description'] || event['message'] || 'Deepgram transcription failed'
27
+ end
28
+ end
29
+ end
30
+ raise Error, 'Deepgram transcription ended before its completion metadata' unless metadata
31
+
32
+ result = build_live_transcription(segments, metadata, model:, language:)
33
+ block.call(TranscriptionChunk.new(type: TranscriptionChunk::DONE, text: result.text, raw: metadata))
34
+ result
35
+ end
36
+ end
37
+
38
+ def streaming_transcription_url(model:, language:, speaker_names:, provider_options:, prompt:)
39
+ params = { model:, language:, smart_format: true, interim_results: true }
40
+ params[:diarize_model] = DIARIZE_MODEL if speaker_names
41
+ params[:keyterm] = Array(prompt) if prompt
42
+ params.merge!(provider_options)
43
+ params = params.compact.flat_map { |key, value| Array(value).map { |item| [key, item] } }
44
+ uri = URI.join("#{@provider.api_base.sub(%r{/+\z}, '')}/", "v1/listen?#{URI.encode_www_form(params)}")
45
+ uri.scheme = uri.scheme == 'https' ? 'wss' : 'ws'
46
+ uri.to_s
47
+ end
48
+
49
+ def send_transcription_audio(socket, audio)
50
+ offset = 0
51
+ while offset < audio.bytesize
52
+ socket.send_binary(audio.byteslice(offset, 16_384))
53
+ offset += 16_384
54
+ end
55
+ socket.send_text(JSON.generate(type: 'CloseStream'))
56
+ end
57
+
58
+ def process_transcription_result(event, segments)
59
+ alternative = event.dig('channel', 'alternatives', 0) || {}
60
+ text = alternative['transcript'].to_s
61
+ return if text.empty?
62
+
63
+ unless event['is_final']
64
+ yield TranscriptionChunk.new(type: TranscriptionChunk::PARTIAL, text:, raw: event)
65
+ return
66
+ end
67
+
68
+ delta = segments.empty? ? text : " #{text}"
69
+ segment = {
70
+ 'text' => text,
71
+ 'start' => event['start'],
72
+ 'end' => event['start'].to_f + event['duration'].to_f,
73
+ 'channel' => event.dig('channel_index', 0),
74
+ 'words' => alternative['words']
75
+ }.compact
76
+ segments << segment
77
+ yield TranscriptionChunk.new(type: TranscriptionChunk::SEGMENT, delta:, segment:, raw: event)
78
+ end
79
+
80
+ def build_live_transcription(segments, metadata, model:, language:)
81
+ RubyLLM::Transcription.new(
82
+ text: segments.map { |segment| segment.fetch('text') }.join(' '), model:, language:,
83
+ duration: metadata['duration'], segments:, words: segments.flat_map { |segment| segment['words'] || [] }
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Deepgram
8
+ # Transcription dialect for the Deepgram speech-to-text API. Every
9
+ # option is a query parameter on v1/listen, and the body is either the
10
+ # audio bytes or a JSON pointer to a remote url. Giving speaker names
11
+ # turns on diarization, which labels each word and utterance with a
12
+ # numeric speaker index.
13
+ module Transcription
14
+ def render_transcription_options(timestamps:, **)
15
+ return {} if timestamps.nil? || timestamps == :word
16
+
17
+ raise ArgumentError, 'Deepgram transcription timestamps must be word'
18
+ end
19
+
20
+ # Turned on by default so the transcript reads like the transcripts
21
+ # every other provider returns. Smart formatting punctuates and
22
+ # formats numbers, dates, and currency; utterances split the
23
+ # transcript into timed segments.
24
+ DEFAULT_PARAMS = { smart_format: true, utterances: true }.freeze
25
+
26
+ # The batch diarizer to run. 'latest' tracks Deepgram's current
27
+ # generally available diarizer, which the boolean diarize parameter
28
+ # it deprecates does not.
29
+ DIARIZE_MODEL = 'latest'
30
+
31
+ def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
32
+ speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &block)
33
+ if block
34
+ if format || speaker_references || temperature
35
+ raise ArgumentError,
36
+ 'Deepgram streaming transcription does not accept format, speaker_references or temperature'
37
+ end
38
+
39
+ return stream_live_transcription(audio_file, model:, language:, speaker_names:, provider_options:, prompt:,
40
+ &block)
41
+ end
42
+
43
+ track_usage(:transcription) do
44
+ attachment = Attachment.new(audio_file, config: @config)
45
+ url = transcription_url(model:, language:, speaker_names:, provider_options:)
46
+ payload = render_transcription_payload(attachment)
47
+ response = post_transcription(url, payload, attachment)
48
+ parse_transcription_response(response, model:)
49
+ end
50
+ end
51
+
52
+ def transcription_url(model:, language: nil, speaker_names: nil, provider_options: {})
53
+ "v1/listen?#{URI.encode_www_form(transcription_params(model:, language:, speaker_names:,
54
+ provider_options:))}"
55
+ end
56
+
57
+ def transcription_params(model:, language: nil, speaker_names: nil, provider_options: {})
58
+ params = { model: model, language: language }.merge(DEFAULT_PARAMS)
59
+ params[:diarize_model] = DIARIZE_MODEL if speaker_names
60
+ params.merge(provider_options).compact
61
+ end
62
+
63
+ # Deepgram fetches remote audio itself, so a url attachment is sent
64
+ # as a JSON pointer and everything else is uploaded as raw bytes.
65
+ def render_transcription_payload(attachment)
66
+ return { url: attachment.source.to_s } if attachment.url?
67
+
68
+ attachment.content
69
+ end
70
+
71
+ def parse_transcription_response(response, model:)
72
+ data = response.body || {}
73
+ channel = data.dig('results', 'channels', 0) || {}
74
+ alternative = channel.dig('alternatives', 0) || {}
75
+
76
+ RubyLLM::Transcription.new(
77
+ text: alternative['transcript'],
78
+ model: model,
79
+ language: channel['detected_language'],
80
+ duration: data.dig('metadata', 'duration'),
81
+ segments: data.dig('results', 'utterances'),
82
+ words: alternative['words']
83
+ )
84
+ end
85
+
86
+ private
87
+
88
+ def post_transcription(url, payload, attachment)
89
+ @connection.post(url, payload, usage: @usage_tracker) do |request|
90
+ request.headers['Content-Type'] = attachment.mime_type || 'application/octet-stream' unless attachment.url?
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The Deepgram API: speech to text on v1/listen, text to speech on
6
+ # v1/speak, and the model catalog behind them. Deepgram has no chat,
7
+ # embedding, or image endpoints, so those seams are left unimplemented.
8
+ #
9
+ # Deepgram carries its request options in the query string rather than
10
+ # the body, so +provider_options:+ joins the query on both audio
11
+ # operations.
12
+ class Deepgram < Protocol
13
+ include Deepgram::Models
14
+ include Deepgram::Speech
15
+ include Deepgram::Transcription
16
+ include Deepgram::StreamingTranscription
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module DeepSeek
6
+ # DeepSeek stores image files for reuse by vision models.
7
+ class Files < Protocols::OpenAI::Files
8
+ IMAGE_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
9
+ MAX_FILE_SIZE = 64 * 1024 * 1024
10
+
11
+ def download(_file_id)
12
+ raise Error, 'DeepSeek does not support downloading uploaded files'
13
+ end
14
+
15
+ private
16
+
17
+ def render_upload_payload(attachment, purpose: nil, **options)
18
+ raise UnsupportedAttachmentError, attachment.mime_type unless IMAGE_TYPES.include?(attachment.mime_type)
19
+ raise ArgumentError, 'DeepSeek image uploads cannot exceed 64 MiB' if file_size(attachment) > MAX_FILE_SIZE
20
+ raise ArgumentError, 'DeepSeek file uploads require purpose: user_data' if purpose && purpose != 'user_data'
21
+
22
+ super(attachment, purpose: 'user_data', **options)
23
+ end
24
+
25
+ def uploaded_file(data, **attributes)
26
+ super(data, **attributes, downloadable: false)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ElevenLabs
6
+ # Workspace media assets used by the Image & Video API.
7
+ class Assets < Protocols::Files
8
+ def download(file_id)
9
+ file = find(file_id)
10
+ url = file.metadata['content_url']
11
+ raise Error, 'ElevenLabs asset is still processing; retrieve it again before downloading' unless url
12
+
13
+ Transport::Connection.basic(@config).get(url).body
14
+ end
15
+
16
+ private
17
+
18
+ def files_url
19
+ 'v1/assets'
20
+ end
21
+
22
+ def render_upload_payload(attachment, purpose: nil, expires_in: nil, name: nil)
23
+ raise ArgumentError, 'ElevenLabs assets do not accept purpose or expires_in' if purpose || expires_in
24
+
25
+ { asset: file_part(attachment), name: name || attachment.filename }
26
+ end
27
+
28
+ def parse_file_response(data)
29
+ uploaded_file(data, id: data.fetch('asset_id'), filename: data['name'], mime_type: data['mime_type'],
30
+ created_at: timestamp(data['created_at_unix']), downloadable: true,
31
+ status: data['content_url'] ? 'ready' : 'processing')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ElevenLabs
6
+ class Flows
7
+ module Images # :nodoc: all
8
+ MASK_MODELS = %w[gpt-image-1 gpt-image-1.5 gpt-image-2].freeze
9
+
10
+ def images_url(**)
11
+ 'v1/flows/image'
12
+ end
13
+
14
+ def post_image(payload, **)
15
+ @connection.post images_url, payload, usage: @usage_tracker, idempotent: false
16
+ end
17
+
18
+ def render_image_payload(prompt, model:, size:, with: nil, mask: nil, provider_options: {}, **)
19
+ images = Attachment.wrap(with, config: @config)
20
+ payload = { model_id: model, prompt: }
21
+ payload[:images] = images.map { |image| render_media_reference(image) } if images.any?
22
+ payload[:aspect_ratio] = image_aspect_ratio(size) if size && size != 'auto'
23
+ if mask
24
+ raise ArgumentError, 'ElevenLabs image masks require a GPT Image model' unless MASK_MODELS.include?(model)
25
+
26
+ payload[:mask] = render_media_reference(Attachment.wrap(mask, config: @config).first)
27
+ end
28
+ Support::Utils.deep_merge(payload, provider_options)
29
+ end
30
+
31
+ def parse_image_response(response, model:)
32
+ id = response.body.fetch('id')
33
+ body = wait_for_image(id)
34
+ Image.new(url: body.fetch('content_url'), mime_type: body.fetch('content_mime_type'), model:)
35
+ end
36
+
37
+ def wait_for_image(id)
38
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config.request_timeout
39
+ loop do
40
+ body = @connection.get("#{images_url}/#{id}").body
41
+ state = parse_generation_status(body)
42
+ return body if state[:status] == :completed
43
+ raise Error, "ElevenLabs image generation failed: #{state[:error]}" if state[:status] == :failed
44
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
45
+ raise Error, "ElevenLabs image generation timed out: #{id}"
46
+ end
47
+
48
+ sleep 1
49
+ end
50
+ end
51
+
52
+ def validate_paint_inputs!(with:, mask:)
53
+ images = Attachment.wrap(with, config: @config)
54
+ raise ArgumentError, 'An image mask requires a source image' if mask && images.empty?
55
+
56
+ images += Attachment.wrap(mask, config: @config) if mask
57
+ images.each do |image|
58
+ raise UnsupportedAttachmentError, image.mime_type unless image.image?
59
+ end
60
+ end
61
+
62
+ def image_aspect_ratio(size)
63
+ match = size.to_s.match(/\A([1-9]\d*)x([1-9]\d*)\z/)
64
+ raise ArgumentError, 'size must be widthxheight or auto' unless match
65
+
66
+ width, height = match.captures.map(&:to_i)
67
+ divisor = width.gcd(height)
68
+ "#{width / divisor}:#{height / divisor}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ElevenLabs
6
+ class Flows
7
+ module Media # :nodoc: all
8
+ def render_media_reference(attachment)
9
+ unless attachment.image? || attachment.audio? || attachment.video?
10
+ raise UnsupportedAttachmentError, attachment.mime_type
11
+ end
12
+
13
+ if attachment.provider_file?
14
+ unless attachment.source.provider.to_s == @provider.slug
15
+ raise ArgumentError, 'ElevenLabs media references require an asset from the same provider'
16
+ end
17
+
18
+ { type: 'asset', asset_id: attachment.provider_file_id }
19
+ else
20
+ { type: 'inline_base64', content_base64: attachment.encoded, mime_type: attachment.mime_type }
21
+ end
22
+ end
23
+
24
+ def parse_generation_status(body)
25
+ case body['status']
26
+ when 'pending', 'generating'
27
+ { status: :pending, raw: body }
28
+ when 'completed'
29
+ raise Error, 'ElevenLabs completed a generation without an output URL' unless body['content_url']
30
+
31
+ { status: :completed, raw: body }
32
+ when 'failed'
33
+ { status: :failed, raw: body, error: body['error_message'] || body['failure_reason'] }
34
+ else
35
+ raise Error, "ElevenLabs returned an unknown generation status: #{body['status'].inspect}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end