ruby_llm 1.16.0 → 2.0.0

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 (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  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 +109 -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 +3 -1
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -9
  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/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +264 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +831 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +255 -53
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +685 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/provider_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,685 @@
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, provider_tools: [], compaction: nil, end_user: nil, &)
100
+ resolution = resolve_provider_tools_for_request(provider_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:, provider_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, provider_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_provider_tools(payload, provider_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 provider-tool support override this;
192
+ # +nil+ means the protocol has no provider-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.without_thinking if foreign_thinking?(message)
475
+ return message unless auto_upload_large_files?
476
+ return message unless message.role == :user
477
+ return message if message.attachments.empty?
478
+
479
+ uploaded = message.attachments.map { |attachment| preprocess_attachment(attachment) }
480
+ return message if uploaded == message.attachments
481
+
482
+ message.with_attachments(uploaded)
483
+ end
484
+
485
+ private
486
+
487
+ # A thinking signature is opaque to every provider but the one that
488
+ # issued it, so a message another provider produced replays without
489
+ # its thinking. A message with no known producer replays as it is.
490
+ def foreign_thinking?(message)
491
+ return false unless message.role == :assistant && carries_thinking?(message)
492
+
493
+ producer = producer_slug(message)
494
+ !producer.nil? && producer != @provider.slug
495
+ end
496
+
497
+ def carries_thinking?(message)
498
+ return true if message.thinking || message.raw_reasoning
499
+
500
+ message.tool_call? && message.tool_calls.each_value.any?(&:thought_signature)
501
+ end
502
+
503
+ # Only a usage entry names the producer: a model id alone can belong
504
+ # to several providers.
505
+ def producer_slug(message)
506
+ message.ruby_llm_usage_entries.reverse.find(&:succeeded?)&.provider
507
+ end
508
+
509
+ def resolve_provider_tools_for_request(entries)
510
+ return nil if entries.nil? || entries.empty?
511
+
512
+ aliases = server_tool_aliases
513
+ unless aliases
514
+ raise UnsupportedServerToolError,
515
+ "#{@provider.name} has no provider-tool support through RubyLLM yet. " \
516
+ 'Request options in the provider vocabulary can be set with with_provider_options.'
517
+ end
518
+
519
+ RubyLLM::Tools::ProviderTools.resolve(entries, aliases: aliases, owner: @provider.name)
520
+ end
521
+
522
+ def apply_provider_tools(payload, entries)
523
+ resolution = resolve_provider_tools_for_request(entries)
524
+ return payload unless resolution
525
+
526
+ payload = Support::Utils.deep_merge(payload, resolution.payload) unless resolution.payload.empty?
527
+ merge_server_tool_entries(payload, resolution.tools) if resolution.tools.any?
528
+ payload
529
+ end
530
+
531
+ # Provider tools join function tools in the payload's tools array. The
532
+ # entry shape comes from the alias table or the caller's raw Hash.
533
+ def merge_server_tool_entries(payload, entries)
534
+ payload[:tools] = Array(payload[:tools]) + entries
535
+ end
536
+
537
+ def track_usage(operation, on_finish: nil)
538
+ @usage_tracker = Accounting::Usage::Tracker.new(
539
+ operation:,
540
+ provider: @provider,
541
+ model: @model,
542
+ config: @config,
543
+ on_finish:
544
+ )
545
+ result = yield
546
+ @usage_tracker.succeed(result)
547
+ result
548
+ rescue StandardError => e
549
+ @usage_tracker.fail_pending(e)
550
+ raise
551
+ ensure
552
+ @usage_tracker = nil
553
+ end
554
+
555
+ def apply_before_request_hooks(payload, hooks)
556
+ Array(hooks).each { |hook| hook.call(payload) }
557
+ payload
558
+ end
559
+
560
+ def auto_upload_large_files?
561
+ @config.auto_upload_large_files && @provider.files? && supports_provider_file_references?
562
+ end
563
+
564
+ def supports_provider_file_references?
565
+ false
566
+ end
567
+
568
+ def preprocess_attachment(attachment)
569
+ return attachment if attachment.provider_file?
570
+ return attachment unless upload_large_attachment?(attachment)
571
+
572
+ ensure_provider_file_size!(attachment)
573
+ Attachment.new(provider_upload(attachment), config: @config)
574
+ end
575
+
576
+ # Uploads are memoized per provider on the attachment itself, so a chat
577
+ # that switches providers uploads once to each rather than replaying
578
+ # another provider's file reference. An upload past its provider
579
+ # retention window is replaced rather than reused.
580
+ def provider_upload(attachment)
581
+ scope = provider_upload_scope
582
+ upload = attachment.provider_uploads[scope]
583
+ return upload if upload && !upload.expired?
584
+
585
+ attachment.provider_uploads[scope] =
586
+ @provider.upload_file(attachment, **provider_file_upload_options(attachment))
587
+ end
588
+
589
+ # A file id belongs to the account that uploaded it, so the memo is keyed
590
+ # by the credentials in play as well as the provider: a chat moved to
591
+ # another Context uploads again instead of replaying a foreign id. The
592
+ # credentials themselves are hashed so the attachment never carries them.
593
+ def provider_upload_scope
594
+ credentials = @provider.class.configuration_options.map { |option| @config.public_send(option) }
595
+ "#{@provider.slug}:#{Digest::SHA256.hexdigest(credentials.join("\0"))}"
596
+ end
597
+
598
+ def upload_large_attachment?(attachment)
599
+ size = attachment.byte_size
600
+ size && size > default_large_file_upload_threshold && provider_file_attachable?(attachment)
601
+ end
602
+
603
+ def default_large_file_upload_threshold
604
+ Float::INFINITY
605
+ end
606
+
607
+ def provider_file_upload_limit
608
+ nil
609
+ end
610
+
611
+ def provider_file_attachable?(_attachment)
612
+ false
613
+ end
614
+
615
+ def provider_file_upload_options(_attachment)
616
+ {}
617
+ end
618
+
619
+ def ensure_provider_file_size!(attachment)
620
+ limit = provider_file_upload_limit
621
+ return unless limit && attachment.byte_size.to_i > limit
622
+
623
+ raise Error, "#{@provider.name} file uploads support files up to #{format_bytes(limit)}; " \
624
+ "#{attachment.filename} is #{format_bytes(attachment.byte_size)}"
625
+ end
626
+
627
+ def format_bytes(bytes)
628
+ return 'unknown size' unless bytes
629
+
630
+ "#{(bytes.to_f / (1024 * 1024)).round(1)} MB"
631
+ end
632
+
633
+ def validate_paint_inputs!(with:, mask:)
634
+ return if with.nil? && mask.nil?
635
+
636
+ raise UnsupportedAttachmentError, 'image reference'
637
+ end
638
+
639
+ def validate_animate_inputs!(with:)
640
+ return if with.empty?
641
+
642
+ raise UnsupportedAttachmentError, 'video reference image'
643
+ end
644
+
645
+ def build_audio_file_part(audio_file)
646
+ require 'faraday/multipart'
647
+
648
+ attachment = audio_file.is_a?(Attachment) ? audio_file : Attachment.new(audio_file, config: @config)
649
+ body = attachment.path? ? File.expand_path(attachment.source) : StringIO.new(attachment.content)
650
+
651
+ Faraday::Multipart::FilePart.new(body, attachment.mime_type, audio_file_name(attachment))
652
+ end
653
+
654
+ # Providers reject audio whose filename carries no extension, which a URL
655
+ # or an IO often has none of, so the detected format supplies one.
656
+ def audio_file_name(attachment)
657
+ name = attachment.filename.to_s
658
+ name = 'audio' if name.empty?
659
+ attachment.extension ? name : "#{name}.#{attachment.format}"
660
+ end
661
+
662
+ def post_count_tokens(payload)
663
+ @connection.post count_tokens_url, payload
664
+ end
665
+
666
+ def sync_response(payload, additional_headers = {})
667
+ response = @connection.post completion_url, payload, usage: @usage_tracker do |req|
668
+ req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
669
+ end
670
+ parse_completion_response response
671
+ end
672
+
673
+ def parse_completion_response(response)
674
+ body = response.body
675
+ if body.nil? || (body.respond_to?(:empty?) && body.empty?)
676
+ raise Error.new('Provider returned an empty response body', response:)
677
+ end
678
+
679
+ message = parse_completion_body(body, raw: response)
680
+ raise Error.new('Provider returned no completion message', response:) unless message
681
+
682
+ message
683
+ end
684
+ end
685
+ end