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,662 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module RubyLLM
6
+ # A Protocol knows how to talk to a family of provider APIs: rendering
7
+ # request payloads, parsing responses, streaming chunks, and naming the
8
+ # endpoints involved. Its counterpart, Provider, knows where to talk and
9
+ # who it is. The protocols that ship with the gem live under
10
+ # RubyLLM::Protocols.
11
+ #
12
+ # Subclass Protocol, or a shipped subclass such as
13
+ # RubyLLM::Protocols::ChatCompletions, to support a new wire format. Each
14
+ # operation (chat, embeddings, moderation, image generation, video
15
+ # generation, speech, transcription, OCR, reranking, token counting, and
16
+ # model listing) is served by three kinds of seam method you override:
17
+ #
18
+ # - <tt>render_*</tt> serializes a RubyLLM request into the wire payload,
19
+ # such as +render_payload+ for chat or +render_embedding_payload+.
20
+ # - <tt>*_url</tt> names the endpoint, such as +completion_url+ or
21
+ # +embedding_url+.
22
+ # - <tt>parse_*</tt> turns the wire response back into RubyLLM objects, such
23
+ # as +parse_completion_body+ or +parse_embedding_response+.
24
+ #
25
+ # Override the seams for the operations you support; the rest raise
26
+ # NotImplementedError. For example:
27
+ #
28
+ # class ChatCompletions < RubyLLM::Protocols::ChatCompletions
29
+ # def completion_url
30
+ # 'v2/chat'
31
+ # end
32
+ # end
33
+ #
34
+ # A protocol instance is constructed by its Provider and borrows the
35
+ # provider's Connection, so subclasses never build HTTP clients
36
+ # themselves.
37
+ class Protocol
38
+ include Streaming
39
+ include BinaryStreaming
40
+
41
+ # The Provider this protocol talks through.
42
+ attr_reader :provider
43
+
44
+ # The provider's Configuration.
45
+ attr_reader :config
46
+
47
+ # The provider's HTTP connection. Subclasses use it to reach their
48
+ # endpoints.
49
+ attr_reader :connection
50
+
51
+ # The Model this instance targets, or +nil+ for model-less operations
52
+ # such as listing models.
53
+ attr_reader :model
54
+
55
+ # :stopdoc:
56
+
57
+ # Declares seam methods that raise NotImplementedError until a subclass
58
+ # overrides them: render_* serializes a request to wire form, *_url names an
59
+ # endpoint, and parse_* reads a wire response back into RubyLLM objects.
60
+ def self.abstract(*names)
61
+ names.each do |name|
62
+ define_method(name) do |*_args, **_opts|
63
+ raise NotImplementedError, "#{self.class} must implement ##{name}"
64
+ end
65
+ end
66
+ end
67
+
68
+ abstract :render_payload, :completion_url, :parse_completion_body
69
+ abstract :render_tool_approval_response
70
+ abstract :models_url, :parse_list_models_response
71
+ abstract :render_embedding_payload, :embedding_url, :parse_embedding_response
72
+ abstract :render_moderation_payload, :moderation_url, :parse_moderation_response
73
+ abstract :render_image_payload, :images_url, :parse_image_response
74
+ abstract :render_video_payload, :video_url, :parse_video_job
75
+ abstract :video_job_url, :parse_video_job_status, :download_video
76
+ abstract :render_speech_payload, :speech_url, :parse_speech_response
77
+ abstract :render_transcription_payload, :transcription_url, :parse_transcription_response
78
+ abstract :render_ocr_payload, :ocr_url, :parse_ocr_response
79
+ abstract :render_rerank_payload, :rerank_url, :parse_rerank_response
80
+ abstract :render_count_tokens_payload, :count_tokens_url, :parse_count_tokens_response
81
+ abstract :render_tokenization_payload, :tokenization_url, :parse_tokenization_response
82
+ abstract :render_compaction_payload, :compaction_url, :parse_compaction_response
83
+ abstract :render_cache_payload, :render_cache_update_payload, :caches_url, :cache_url, :parse_cache_response
84
+
85
+ def initialize(provider, model = nil)
86
+ @provider = provider
87
+ @config = provider.config
88
+ @connection = provider.connection
89
+ @model = model
90
+ end
91
+
92
+ def tool_approval_response(tool_call, approved:)
93
+ Message.new(role: :tool, content: approved ? 'Approved' : 'Denied', tool_call_id: tool_call.id,
94
+ raw_content: render_tool_approval_response(tool_call, approved:))
95
+ end
96
+
97
+ def complete(messages, tools:, temperature:, provider_options: {}, headers: {}, schema: nil, thinking: nil,
98
+ max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
99
+ usage_recorder: nil, server_tools: [], compaction: nil, end_user: nil, &)
100
+ resolution = resolve_server_tools_for_request(server_tools)
101
+ headers = resolution.headers.merge(headers) if resolution
102
+ headers = apply_compaction_headers(headers, compaction) if compaction
103
+ payload = render(
104
+ messages, tools:, tool_prefs:, temperature:, max_output_tokens:, provider_options:, schema:, thinking:,
105
+ citations:, caching:, compaction:, end_user:, before_request:, server_tools:,
106
+ stream: block_given?
107
+ )
108
+
109
+ track_usage(:chat, on_finish: usage_recorder) do
110
+ if block_given?
111
+ stream_response(payload, headers) do |chunk|
112
+ @usage_tracker.observe(chunk)
113
+ yield chunk
114
+ end
115
+ else
116
+ sync_response payload, headers
117
+ end
118
+ end
119
+ rescue NotImplementedError
120
+ raise Error, "#{@provider.name} doesn't support chat"
121
+ end
122
+
123
+ def render(messages, tools:, temperature:, provider_options: {}, schema: nil, thinking: nil,
124
+ max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
125
+ stream: false, server_tools: [], compaction: nil, end_user: nil)
126
+ payload = render_payload(
127
+ messages,
128
+ tools: tools,
129
+ tool_prefs: tool_prefs,
130
+ temperature: temperature,
131
+ max_output_tokens: max_output_tokens,
132
+ model: model,
133
+ stream: stream,
134
+ schema: schema,
135
+ thinking: thinking,
136
+ citations: citations,
137
+ caching: caching
138
+ )
139
+ payload = apply_end_user(payload, end_user) if end_user
140
+ payload = apply_compaction(payload, compaction) if compaction
141
+ payload = Support::Utils.deep_merge(payload, provider_options)
142
+ payload = apply_server_tools(payload, server_tools)
143
+ apply_before_request_hooks(payload, before_request)
144
+ rescue NotImplementedError
145
+ raise Error, "#{@provider.name} doesn't support chat"
146
+ end
147
+
148
+ # Writes the chat's safety identifier into the request payload and
149
+ # returns it. The default drops the value with a debug log, so
150
+ # providers with no equivalent field simply omit it. Protocols whose
151
+ # API accepts one override this.
152
+ #
153
+ # def apply_end_user(payload, identifier)
154
+ # payload.merge(safety_identifier: identifier)
155
+ # end
156
+ #
157
+ def apply_end_user(payload, identifier)
158
+ RubyLLM.logger.debug do
159
+ "#{@provider.name} has no safety identifier parameter, dropping #{identifier}"
160
+ end
161
+ payload
162
+ end
163
+
164
+ # Writes the chat's context compaction options into the request payload
165
+ # and returns it. +compaction+ is the provider-neutral Hash from
166
+ # Chat#with_compaction, whose keys are Chat::COMPACTION_OPTIONS. The
167
+ # default drops it with a debug log, so providers that manage context
168
+ # themselves simply ignore the request. Protocols whose API compacts
169
+ # server-side override this, mapping the options they support and
170
+ # logging the ones they do not.
171
+ #
172
+ # def apply_compaction(payload, compaction)
173
+ # payload.merge(context_management: [{ type: 'compaction' }])
174
+ # end
175
+ #
176
+ def apply_compaction(payload, compaction)
177
+ RubyLLM.logger.debug do
178
+ "#{@provider.name} has no context compaction parameter, dropping #{compaction.inspect}"
179
+ end
180
+ payload
181
+ end
182
+
183
+ # Returns the completion request's HTTP headers with anything context
184
+ # compaction requires added. The default changes nothing; protocols that
185
+ # gate compaction behind a beta header override this.
186
+ def apply_compaction_headers(headers, _compaction)
187
+ headers
188
+ end
189
+
190
+ # The alias table mapping portable server tool names to this protocol's
191
+ # wire format. Protocols with server-tool support override this;
192
+ # +nil+ means the protocol has no server-tool support at all.
193
+ def server_tool_aliases
194
+ nil
195
+ end
196
+
197
+ def count_tokens(messages, tools:, tool_prefs: nil, thinking: nil, schema: nil, citations: false, caching: nil)
198
+ payload = render_count_tokens_payload(
199
+ messages,
200
+ tools: tools,
201
+ tool_prefs: tool_prefs,
202
+ model: model,
203
+ schema: schema,
204
+ thinking: thinking,
205
+ citations: citations,
206
+ caching: caching
207
+ )
208
+ parse_count_tokens_response post_count_tokens(payload)
209
+ rescue NotImplementedError
210
+ raise Error, "#{@provider.name} doesn't support token counting"
211
+ end
212
+
213
+ def list_models
214
+ response = @connection.get models_url
215
+ parse_list_models_response response, @provider.slug
216
+ end
217
+
218
+ def tokenize(text, model:)
219
+ payload = render_tokenization_payload(text, model:)
220
+ response = @connection.post tokenization_url, payload
221
+ parse_tokenization_response(response, model:)
222
+ rescue NotImplementedError
223
+ raise Error, "#{@provider.name} doesn't support text tokenization"
224
+ end
225
+
226
+ def compact(messages, headers: {}, before_request: [], usage_recorder: nil)
227
+ payload = apply_before_request_hooks(render_compaction_payload(messages), before_request)
228
+ track_usage(:chat, on_finish: usage_recorder) do
229
+ response = @connection.post compaction_url, payload, usage: @usage_tracker do |request|
230
+ request.headers = headers.merge(request.headers) unless headers.empty?
231
+ end
232
+ parse_compaction_response(response)
233
+ end
234
+ rescue NotImplementedError
235
+ raise Error, "#{@provider.name} doesn't support manual compaction"
236
+ end
237
+
238
+ def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {})
239
+ attachments = Attachment.wrap(with, config: @config)
240
+ raise UnsupportedAttachmentError, attachments.first.mime_type if attachments.any? && !supports_embedding_media?
241
+
242
+ track_usage(:embedding) do
243
+ payload = if attachments.any?
244
+ render_embedding_payload(text, model:, dimensions:, task_type:, title:, with: attachments,
245
+ provider_options:)
246
+ else
247
+ render_embedding_payload(text, model:, dimensions:, task_type:, title:, provider_options:)
248
+ end
249
+ response = @connection.post(embedding_url(model:), payload, usage: @usage_tracker)
250
+ parse_embedding_response(response, model:, text:)
251
+ end
252
+ rescue NotImplementedError
253
+ raise Error, "#{@provider.name} doesn't support embeddings"
254
+ end
255
+
256
+ def render_embedding(text, model:, dimensions: nil, task_type: nil, title: nil, provider_options: {}) # :nodoc:
257
+ render_embedding_payload(text, model:, dimensions:, task_type:, title:, provider_options:)
258
+ end
259
+
260
+ def moderate(input, model:, with: [], provider_options: {})
261
+ track_usage(:moderation) do
262
+ payload = render_moderation_payload(input, model:, with: Attachment.wrap(with, config: @config),
263
+ provider_options:)
264
+ response = @connection.post moderation_url, payload, usage: @usage_tracker
265
+ parse_moderation_response(response, model:)
266
+ end
267
+ rescue NotImplementedError
268
+ raise Error, "#{@provider.name} doesn't support moderation"
269
+ end
270
+
271
+ def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
272
+ track_usage(:image) do
273
+ validate_paint_inputs!(with:, mask:)
274
+ payload = render_image_payload(prompt, model:, size:, count:, with:, mask:, provider_options:)
275
+ response = post_image(payload, with:, mask:)
276
+ images = parse_image_responses(response, model:)
277
+ images.each { |image| image.config = @config }
278
+ images.size <= 1 ? images.first : images
279
+ end
280
+ rescue NotImplementedError
281
+ raise Error, "#{@provider.name} doesn't support image generation"
282
+ end
283
+
284
+ def post_image(payload, with:, mask:)
285
+ @connection.post images_url(with:, mask:), payload, usage: @usage_tracker
286
+ end
287
+
288
+ # Returns every Image in an image generation response, as an Array. The
289
+ # default asks for the one image parse_image_response reads; protocols
290
+ # whose API can return several images per request override it. Only the
291
+ # first image carries the call's usage, so summing across the array
292
+ # gives the cost of the call.
293
+ def parse_image_responses(response, model:)
294
+ Array(parse_image_response(response, model:))
295
+ end
296
+
297
+ # Video generation is asynchronous on every provider: this submits the
298
+ # job and returns a VideoJob, whose #refresh and #video poll and
299
+ # download through this protocol instance.
300
+ def animate_later(prompt, model:, with: nil, extend: nil, provider_options: {})
301
+ raise ArgumentError, 'with: and extend: cannot be combined' if with && extend
302
+
303
+ if extend
304
+ payload = render_video_extension_payload(prompt, model:, extend:, provider_options:)
305
+ url = video_extension_url
306
+ else
307
+ attachments = Attachment.wrap(with, config: @config)
308
+ validate_animate_inputs!(with: attachments)
309
+ payload = render_video_payload(prompt, model:, with: attachments, provider_options:)
310
+ url = video_request_url(payload)
311
+ end
312
+ response = post_video(url, payload)
313
+ parse_video_job(response, model:)
314
+ rescue NotImplementedError
315
+ raise Error, "#{@provider.name} doesn't support video generation"
316
+ end
317
+
318
+ def post_video(url, payload)
319
+ @connection.post url, payload, idempotent: false
320
+ end
321
+
322
+ def video_request_url(_payload)
323
+ video_url
324
+ end
325
+
326
+ def video_extension_url
327
+ video_url
328
+ end
329
+
330
+ def render_video_extension_payload(*)
331
+ raise Error, "#{@provider.name} doesn't support video extension"
332
+ end
333
+
334
+ def video_extension_attachment(source)
335
+ source = source.url || StringIO.new(source.to_blob) if source.is_a?(Video)
336
+ attachments = if source.respond_to?(:read)
337
+ [Attachment.new(source, filename: 'video.mp4', config: @config)]
338
+ else
339
+ Attachment.wrap(source, config: @config)
340
+ end
341
+ raise ArgumentError, 'extend: takes exactly one video' unless attachments.one?
342
+
343
+ attachment = attachments.first
344
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.video?
345
+
346
+ attachment
347
+ end
348
+
349
+ def refresh_video_job(job)
350
+ parse_video_job_status @connection.get(video_job_url(job)), job: job
351
+ end
352
+
353
+ def speak(input, model:, voice:, format:, provider_options: {}, &block)
354
+ track_usage(:speech) do
355
+ payload = render_speech_payload(input, model:, voice:, format:, provider_options:)
356
+ next stream_speech(payload, model:, voice:, format:, &block) if block
357
+
358
+ response = @connection.post speech_url(model:), payload, usage: @usage_tracker
359
+ parse_speech_response(response, model:, voice:, format:)
360
+ end
361
+ rescue NotImplementedError
362
+ raise Error, "#{@provider.name} doesn't support speech generation"
363
+ end
364
+
365
+ def stream_speech(*, **, &)
366
+ raise Error, "#{@provider.name} doesn't support streaming speech with this protocol"
367
+ end
368
+
369
+ def stream_speech_response(url, payload, model:, voice:, format:)
370
+ empty_response = Faraday::Response.new(body: '')
371
+ audio = parse_speech_response(empty_response, model:, voice:, format:)
372
+ response = stream_binary(url, payload) do |data|
373
+ yield SpeechChunk.new(data:, format: audio.format, mime_type: audio.mime_type)
374
+ end
375
+ parse_speech_response(response, model:, voice:, format:)
376
+ end
377
+
378
+ def render_transcription_options(timestamps:, **)
379
+ return {} if timestamps.nil?
380
+
381
+ raise ArgumentError, 'This transcription protocol does not support timestamps'
382
+ end
383
+
384
+ def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
385
+ speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &block)
386
+ streaming = block_given?
387
+ track_usage(:transcription) do
388
+ file_part = build_audio_file_part(audio_file)
389
+ payload = render_transcription_payload(file_part, model:, language:, format:, speaker_names:,
390
+ speaker_references:, provider_options:, prompt:,
391
+ temperature:)
392
+ next stream_transcription(payload, model:, &block) if streaming
393
+
394
+ response = @connection.post transcription_url, payload, usage: @usage_tracker
395
+ parse_transcription_response(response, model:)
396
+ end
397
+ rescue NotImplementedError
398
+ raise Error, "#{@provider.name} doesn't support transcription"
399
+ end
400
+
401
+ # Streams a transcription, yielding TranscriptionChunk objects and
402
+ # returning the final Transcription. Protocols whose provider streams
403
+ # transcriptions override this.
404
+ def stream_transcription(*, **, &)
405
+ raise_transcription_streaming_unsupported
406
+ end
407
+
408
+ def raise_transcription_streaming_unsupported # :nodoc:
409
+ raise Error, "#{@provider.name} doesn't support streaming transcription"
410
+ end
411
+
412
+ # Whether the protocol can embed media attachments alongside text.
413
+ # Protocols that support multimodal embeddings override this and accept
414
+ # a +with:+ array of Attachments in render_embedding_payload.
415
+ def supports_embedding_media?
416
+ false
417
+ end
418
+
419
+ def ocr(file, model:, pages: nil, provider_options: {})
420
+ track_usage(:ocr) do
421
+ payload = render_ocr_payload(file, model:, pages:, provider_options:)
422
+ response = @connection.post ocr_url, payload, usage: @usage_tracker
423
+ parse_ocr_response(response, model:)
424
+ end
425
+ rescue NotImplementedError
426
+ raise Error, "#{@provider.name} doesn't support OCR"
427
+ end
428
+
429
+ def rerank(query, documents, model:, top_n: nil, provider_options: {})
430
+ track_usage(:rerank) do
431
+ payload = render_rerank_payload(query, documents, model:, top_n:, provider_options:)
432
+ response = @connection.post rerank_url, payload, usage: @usage_tracker
433
+ parse_rerank_response(response, model:, documents:)
434
+ end
435
+ rescue NotImplementedError
436
+ raise Error, "#{@provider.name} doesn't support reranking"
437
+ end
438
+
439
+ def cache_content(content, model:, ttl: nil, instructions: nil, with: nil)
440
+ payload = render_cache_payload(content, model:, ttl:, instructions:,
441
+ attachments: Attachment.wrap(with, config: @config))
442
+ response = @connection.post caches_url, payload, idempotent: false
443
+ parse_cache_response(response.body)
444
+ rescue NotImplementedError
445
+ raise Error, "#{@provider.name} doesn't support explicit content caching"
446
+ end
447
+
448
+ def find_cache(name)
449
+ response = @connection.get cache_url(name)
450
+ parse_cache_response(response.body)
451
+ rescue NotImplementedError
452
+ raise Error, "#{@provider.name} doesn't support explicit content caching"
453
+ end
454
+
455
+ def delete_cache(name)
456
+ @connection.delete cache_url(name)
457
+ true
458
+ rescue NotImplementedError
459
+ raise Error, "#{@provider.name} doesn't support explicit content caching"
460
+ end
461
+
462
+ def extend_cache(name, ttl:)
463
+ response = @connection.patch cache_url(name), render_cache_update_payload(ttl:)
464
+ parse_cache_response(response.body)
465
+ rescue NotImplementedError
466
+ raise Error, "#{@provider.name} doesn't support explicit content caching"
467
+ end
468
+
469
+ def parse_error(response)
470
+ @provider.parse_error(response)
471
+ end
472
+
473
+ def preprocess_message(message)
474
+ return message unless auto_upload_large_files?
475
+ return message unless message.role == :user
476
+ return message if message.attachments.empty?
477
+
478
+ uploaded = message.attachments.map { |attachment| preprocess_attachment(attachment) }
479
+ return message if uploaded == message.attachments
480
+
481
+ message.with_attachments(uploaded)
482
+ end
483
+
484
+ private
485
+
486
+ def resolve_server_tools_for_request(entries)
487
+ return nil if entries.nil? || entries.empty?
488
+
489
+ aliases = server_tool_aliases
490
+ unless aliases
491
+ raise UnsupportedServerToolError,
492
+ "#{@provider.name} has no server-tool support through RubyLLM yet. " \
493
+ 'Request options in the provider vocabulary can be set with with_provider_options.'
494
+ end
495
+
496
+ RubyLLM::Tools::ServerTools.resolve(entries, aliases: aliases, owner: @provider.name)
497
+ end
498
+
499
+ def apply_server_tools(payload, entries)
500
+ resolution = resolve_server_tools_for_request(entries)
501
+ return payload unless resolution
502
+
503
+ payload = Support::Utils.deep_merge(payload, resolution.payload) unless resolution.payload.empty?
504
+ merge_server_tool_entries(payload, resolution.tools) if resolution.tools.any?
505
+ payload
506
+ end
507
+
508
+ # Server tools join function tools in the payload's tools array. The
509
+ # entry shape comes from the alias table or the caller's raw Hash.
510
+ def merge_server_tool_entries(payload, entries)
511
+ payload[:tools] = Array(payload[:tools]) + entries
512
+ end
513
+
514
+ def track_usage(operation, on_finish: nil)
515
+ @usage_tracker = Accounting::Usage::Tracker.new(
516
+ operation:,
517
+ provider: @provider,
518
+ model: @model,
519
+ config: @config,
520
+ on_finish:
521
+ )
522
+ result = yield
523
+ @usage_tracker.succeed(result)
524
+ result
525
+ rescue StandardError => e
526
+ @usage_tracker.fail_pending(e)
527
+ raise
528
+ ensure
529
+ @usage_tracker = nil
530
+ end
531
+
532
+ def apply_before_request_hooks(payload, hooks)
533
+ Array(hooks).each { |hook| hook.call(payload) }
534
+ payload
535
+ end
536
+
537
+ def auto_upload_large_files?
538
+ @config.auto_upload_large_files && @provider.files? && supports_provider_file_references?
539
+ end
540
+
541
+ def supports_provider_file_references?
542
+ false
543
+ end
544
+
545
+ def preprocess_attachment(attachment)
546
+ return attachment if attachment.provider_file?
547
+ return attachment unless upload_large_attachment?(attachment)
548
+
549
+ ensure_provider_file_size!(attachment)
550
+ Attachment.new(provider_upload(attachment), config: @config)
551
+ end
552
+
553
+ # Uploads are memoized per provider on the attachment itself, so a chat
554
+ # that switches providers uploads once to each rather than replaying
555
+ # another provider's file reference. An upload past its provider
556
+ # retention window is replaced rather than reused.
557
+ def provider_upload(attachment)
558
+ scope = provider_upload_scope
559
+ upload = attachment.provider_uploads[scope]
560
+ return upload if upload && !upload.expired?
561
+
562
+ attachment.provider_uploads[scope] =
563
+ @provider.upload_file(attachment, **provider_file_upload_options(attachment))
564
+ end
565
+
566
+ # A file id belongs to the account that uploaded it, so the memo is keyed
567
+ # by the credentials in play as well as the provider: a chat moved to
568
+ # another Context uploads again instead of replaying a foreign id. The
569
+ # credentials themselves are hashed so the attachment never carries them.
570
+ def provider_upload_scope
571
+ credentials = @provider.class.configuration_options.map { |option| @config.public_send(option) }
572
+ "#{@provider.slug}:#{Digest::SHA256.hexdigest(credentials.join("\0"))}"
573
+ end
574
+
575
+ def upload_large_attachment?(attachment)
576
+ size = attachment.byte_size
577
+ size && size > default_large_file_upload_threshold && provider_file_attachable?(attachment)
578
+ end
579
+
580
+ def default_large_file_upload_threshold
581
+ Float::INFINITY
582
+ end
583
+
584
+ def provider_file_upload_limit
585
+ nil
586
+ end
587
+
588
+ def provider_file_attachable?(_attachment)
589
+ false
590
+ end
591
+
592
+ def provider_file_upload_options(_attachment)
593
+ {}
594
+ end
595
+
596
+ def ensure_provider_file_size!(attachment)
597
+ limit = provider_file_upload_limit
598
+ return unless limit && attachment.byte_size.to_i > limit
599
+
600
+ raise Error, "#{@provider.name} file uploads support files up to #{format_bytes(limit)}; " \
601
+ "#{attachment.filename} is #{format_bytes(attachment.byte_size)}"
602
+ end
603
+
604
+ def format_bytes(bytes)
605
+ return 'unknown size' unless bytes
606
+
607
+ "#{(bytes.to_f / (1024 * 1024)).round(1)} MB"
608
+ end
609
+
610
+ def validate_paint_inputs!(with:, mask:)
611
+ return if with.nil? && mask.nil?
612
+
613
+ raise UnsupportedAttachmentError, 'image reference'
614
+ end
615
+
616
+ def validate_animate_inputs!(with:)
617
+ return if with.empty?
618
+
619
+ raise UnsupportedAttachmentError, 'video reference image'
620
+ end
621
+
622
+ def build_audio_file_part(audio_file)
623
+ require 'faraday/multipart'
624
+
625
+ attachment = audio_file.is_a?(Attachment) ? audio_file : Attachment.new(audio_file, config: @config)
626
+ body = attachment.path? ? File.expand_path(attachment.source) : StringIO.new(attachment.content)
627
+
628
+ Faraday::Multipart::FilePart.new(body, attachment.mime_type, audio_file_name(attachment))
629
+ end
630
+
631
+ # Providers reject audio whose filename carries no extension, which a URL
632
+ # or an IO often has none of, so the detected format supplies one.
633
+ def audio_file_name(attachment)
634
+ name = attachment.filename.to_s
635
+ name = 'audio' if name.empty?
636
+ attachment.extension ? name : "#{name}.#{attachment.format}"
637
+ end
638
+
639
+ def post_count_tokens(payload)
640
+ @connection.post count_tokens_url, payload
641
+ end
642
+
643
+ def sync_response(payload, additional_headers = {})
644
+ response = @connection.post completion_url, payload, usage: @usage_tracker do |req|
645
+ req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
646
+ end
647
+ parse_completion_response response
648
+ end
649
+
650
+ def parse_completion_response(response)
651
+ body = response.body
652
+ if body.nil? || (body.respond_to?(:empty?) && body.empty?)
653
+ raise Error.new('Provider returned an empty response body', response:)
654
+ end
655
+
656
+ message = parse_completion_body(body, raw: response)
657
+ raise Error.new('Provider returned no completion message', response:) unless message
658
+
659
+ message
660
+ end
661
+ end
662
+ end