ruby_llm 1.16.0 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) 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 -6
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +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 +71172 -33253
  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 +540 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{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 +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +560 -128
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  301. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +16 -4
  330. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +15 -5
  381. data/lib/ruby_llm/railtie.rb +7 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +138 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +380 -6
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +338 -97
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -41
  432. data/lib/ruby_llm/connection.rb +0 -159
  433. data/lib/ruby_llm/content.rb +0 -91
  434. data/lib/ruby_llm/deprecator.rb +0 -24
  435. data/lib/ruby_llm/error_middleware.rb +0 -81
  436. data/lib/ruby_llm/instrumentation.rb +0 -36
  437. data/lib/ruby_llm/mime_type.rb +0 -96
  438. data/lib/ruby_llm/model/info.rb +0 -164
  439. data/lib/ruby_llm/model_registry.rb +0 -39
  440. data/lib/ruby_llm/models_schema.json +0 -171
  441. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  442. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  443. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  444. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  445. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  446. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  447. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  448. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  449. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  450. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  451. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  452. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  453. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  454. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  455. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  456. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  457. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  459. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  460. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  461. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  462. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  463. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  464. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  465. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  466. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  467. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  468. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  469. data/lib/ruby_llm/streaming.rb +0 -179
  470. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  471. data/lib/ruby_llm/utils.rb +0 -130
  472. data/lib/tasks/models.rake +0 -593
  473. data/lib/tasks/release.rake +0 -94
  474. data/lib/tasks/vcr.rake +0 -124
@@ -4,223 +4,603 @@ require 'json'
4
4
  require 'ruby_llm/error'
5
5
 
6
6
  module RubyLLM
7
- # Base class for LLM providers.
7
+ # A Provider connects RubyLLM to one AI service. It knows where to talk
8
+ # (host, authentication headers, configuration) and which protocol to
9
+ # speak for a given model and request. The wire formats themselves live
10
+ # under RubyLLM::Protocols.
11
+ #
12
+ # Subclass Provider to support a new service, then make it available
13
+ # with ::register:
14
+ #
15
+ # class Acme < RubyLLM::Provider
16
+ # protocol :chat_completions, RubyLLM::Protocols::ChatCompletions
17
+ #
18
+ # def self.configuration_options
19
+ # %i[acme_api_key]
20
+ # end
21
+ #
22
+ # def api_base
23
+ # 'https://api.acme.ai/v1'
24
+ # end
25
+ #
26
+ # def headers
27
+ # { 'Authorization' => "Bearer #{@config.acme_api_key}" }
28
+ # end
29
+ # end
30
+ #
31
+ # RubyLLM::Provider.register :acme, Acme
32
+ #
33
+ # See the custom providers guide for the full walkthrough.
8
34
  class Provider
9
- include Streaming
35
+ include Support::Inspectable
10
36
 
11
- attr_reader :config, :connection
37
+ BATCH_RATE_BY_COMPONENT = {
38
+ input: :input_per_million,
39
+ output: :output_per_million,
40
+ cache_read: :cache_read_input_per_million,
41
+ cache_write: :cache_write_input_per_million,
42
+ thinking: :reasoning_output_per_million
43
+ }.freeze
44
+ private_constant :BATCH_RATE_BY_COMPONENT
12
45
 
13
- def initialize(config)
46
+ # The Configuration the provider was built with.
47
+ attr_reader :config
48
+
49
+ attr_reader :connection # :nodoc:
50
+
51
+ def initialize(config) # :nodoc:
14
52
  @config = config
15
53
  ensure_configured!
16
- @connection = Connection.new(self, @config)
54
+ @connection = Transport::Connection.new(self, @config)
17
55
  end
18
56
 
57
+ # Returns the base URL that relative endpoint paths resolve against.
58
+ # The base implementation raises NotImplementedError, so every
59
+ # subclass must define it.
60
+ #
61
+ # def api_base
62
+ # @config.acme_api_base || 'https://api.acme.ai/v1'
63
+ # end
64
+ #
19
65
  def api_base
20
66
  raise NotImplementedError
21
67
  end
22
68
 
69
+ # Returns the headers merged into every request. The default is an
70
+ # empty hash. Override to supply authentication.
71
+ #
72
+ # def headers
73
+ # { 'Authorization' => "Bearer #{@config.acme_api_key}" }
74
+ # end
75
+ #
23
76
  def headers
24
77
  {}
25
78
  end
26
79
 
80
+ # Returns how many seconds the service asked us to wait before
81
+ # retrying a rate-limited request, or +nil+ when the response carries
82
+ # no timing information. The retry middleware already honors the
83
+ # standard <tt>Retry-After</tt> header; override this to read
84
+ # provider-specific rate-limit headers.
85
+ #
86
+ # def retry_delay(response)
87
+ # response.response_headers['x-acme-ratelimit-reset']&.to_f
88
+ # end
89
+ #
90
+ def retry_delay(_response)
91
+ nil
92
+ end
93
+
94
+ # Returns the provider slug, delegating to ::slug.
27
95
  def slug
28
96
  self.class.slug
29
97
  end
30
98
 
31
- def name
32
- self.class.name
99
+ # Returns the human-readable provider name, delegating to
100
+ # ::display_name.
101
+ def name # :nodoc:
102
+ self.class.display_name
33
103
  end
34
104
 
35
- def capabilities
105
+ def capabilities # :nodoc:
36
106
  self.class.capabilities
37
107
  end
38
108
 
39
- def configuration_requirements
109
+ def configuration_requirements # :nodoc:
40
110
  self.class.configuration_requirements
41
111
  end
42
112
 
43
- # rubocop:disable Metrics/ParameterLists
44
- def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
45
- tool_prefs: nil, &)
46
- normalized_temperature = maybe_normalize_temperature(temperature, model)
47
-
48
- payload = Utils.deep_merge(
49
- render_payload(
50
- messages,
51
- tools: tools,
52
- tool_prefs: tool_prefs,
53
- temperature: normalized_temperature,
54
- model: model,
55
- stream: block_given?,
56
- schema: schema,
57
- thinking: thinking
58
- ),
59
- params
113
+ def protocols # :nodoc:
114
+ self.class.protocols
115
+ end
116
+
117
+ # Returns the protocol class to use for +model+. Override to route
118
+ # between registered protocols per model or request operation. An
119
+ # explicit <tt>protocol:</tt> override on the chat or the provider's
120
+ # <tt><slug>_protocol</tt> configuration option takes precedence
121
+ # over this hook.
122
+ #
123
+ # def protocol_for(model, **)
124
+ # model.id.match?(/audio|realtime/) ? protocols[:chat_completions] : super
125
+ # end
126
+ #
127
+ def protocol_for(_model, **)
128
+ default_protocol
129
+ end
130
+
131
+ def complete(messages, tools:, temperature:, model:, provider_options: {}, headers: {}, schema: nil, # :nodoc:
132
+ max_output_tokens: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil,
133
+ protocol: nil, before_request: [], usage_recorder: nil, server_tools: [],
134
+ compaction: nil, end_user: nil, &)
135
+ protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
136
+ protocol_class.new(self, model).complete(
137
+ messages,
138
+ tools: tools,
139
+ server_tools: server_tools,
140
+ tool_prefs: tool_prefs,
141
+ temperature: temperature,
142
+ max_output_tokens: max_output_tokens,
143
+ provider_options: provider_options,
144
+ headers: headers,
145
+ schema: schema,
146
+ thinking: thinking,
147
+ citations: citations,
148
+ caching: caching,
149
+ compaction: compaction,
150
+ end_user: end_user,
151
+ before_request: before_request,
152
+ usage_recorder: usage_recorder,
153
+ &
60
154
  )
155
+ end
61
156
 
62
- if block_given?
63
- stream_response @connection, payload, headers, &
64
- else
65
- sync_response @connection, payload, headers
157
+ def tool_approval_response(tool_call, approved:, model:, protocol: nil) # :nodoc:
158
+ resolve_protocol(protocol, model).new(self, model).tool_approval_response(tool_call, approved:)
159
+ end
160
+
161
+ def compact(messages, model:, protocol: nil, headers: {}, before_request: [], usage_recorder: nil) # :nodoc:
162
+ resolve_protocol(protocol, model).new(self, model).compact(
163
+ messages, headers:, before_request:, usage_recorder:
164
+ )
165
+ end
166
+
167
+ def render(messages, tools:, temperature:, model:, provider_options: {}, schema: nil, thinking: nil, # :nodoc:
168
+ max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, protocol: nil,
169
+ before_request: [], server_tools: [], compaction: nil, end_user: nil)
170
+ protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
171
+ protocol_class.new(self, model).render(
172
+ messages,
173
+ tools: tools,
174
+ server_tools: server_tools,
175
+ tool_prefs: tool_prefs,
176
+ temperature: temperature,
177
+ max_output_tokens: max_output_tokens,
178
+ provider_options: provider_options,
179
+ schema: schema,
180
+ thinking: thinking,
181
+ citations: citations,
182
+ caching: caching,
183
+ compaction: compaction,
184
+ end_user: end_user,
185
+ before_request: before_request
186
+ )
187
+ end
188
+
189
+ def count_tokens(messages, model:, tools: {}, tool_prefs: nil, thinking: nil, schema: nil, # :nodoc:
190
+ citations: false, caching: nil, protocol: nil)
191
+ protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
192
+ protocol_class.new(self, model).count_tokens(
193
+ messages,
194
+ tools: tools,
195
+ tool_prefs: tool_prefs,
196
+ thinking: thinking,
197
+ schema: schema,
198
+ citations: citations,
199
+ caching: caching
200
+ )
201
+ end
202
+
203
+ def preprocess_message(message, model:, protocol: nil) # :nodoc:
204
+ protocol_class = resolve_protocol(
205
+ protocol,
206
+ model,
207
+ tools: {},
208
+ schema: nil,
209
+ thinking: nil,
210
+ tool_prefs: nil,
211
+ citations: false
212
+ )
213
+ protocol_class.new(self, model).preprocess_message(message)
214
+ end
215
+
216
+ def batches? # :nodoc:
217
+ batch_protocol.public_method_defined?(:create_batch)
218
+ end
219
+
220
+ def create_batch(requests) # :nodoc:
221
+ protocol = batch_protocol_for(requests)
222
+ ensure_batches_supported!(protocol)
223
+ protocol.new(self).create_batch(requests).merge(batch_protocol: protocol)
224
+ end
225
+
226
+ def find_batch(id) # :nodoc:
227
+ ensure_batches_supported!
228
+ batch_protocol.new(self).find_batch(id)
229
+ end
230
+
231
+ def cancel_batch(id) # :nodoc:
232
+ ensure_batches_supported!
233
+ batch_protocol.new(self).cancel_batch(id)
234
+ end
235
+
236
+ def batch_results(id, batch_protocol: nil) # :nodoc:
237
+ protocol = resolve_batch_protocol(batch_protocol) || self.batch_protocol
238
+ ensure_batches_supported!(protocol)
239
+ protocol.new(self).batch_results(id)
240
+ end
241
+
242
+ def batch_status(raw_status, completed:, batch_protocol: nil) # :nodoc:
243
+ protocol = resolve_batch_protocol(batch_protocol) || self.batch_protocol
244
+ ensure_batches_supported!(protocol)
245
+ parser = protocol.new(self)
246
+ return parser.send(:parse_batch_status, raw_status, completed:) if parser.respond_to?(:parse_batch_status, true)
247
+
248
+ completed ? :succeeded : :pending
249
+ end
250
+
251
+ def batch_cost(tokens, model:, category: :text_tokens) # :nodoc:
252
+ standard = Cost.new(tokens:, model:, category:)
253
+ return standard if tokens.reported_cost
254
+
255
+ pricing = model.pricing.public_send(category)
256
+ batch_tier = pricing.batch unless long_context_pricing?(pricing, tokens)
257
+ batch = Cost.new(tokens:, model:, category:, tier: :batch) if batch_tier
258
+ amounts = batch_cost_amounts(standard:, batch:, batch_tier:, model:)
259
+ Cost.from_h(amounts, tokens:)
260
+ end
261
+
262
+ def batch_cost_amounts(standard:, batch:, batch_tier:, model:) # :nodoc:
263
+ BATCH_RATE_BY_COMPONENT.to_h do |component, rate|
264
+ amount = if batch_tier&.public_send(rate)
265
+ batch.public_send(component)
266
+ else
267
+ value = standard.public_send(component)
268
+ multiplier = batch_cost_multiplier(model:, component:)
269
+ value * multiplier if value && multiplier
270
+ end
271
+ [component, amount]
66
272
  end
67
273
  end
68
- # rubocop:enable Metrics/ParameterLists
69
274
 
70
- def list_models
71
- response = @connection.get models_url
72
- parse_list_models_response response, slug, capabilities
275
+ def batch_cost_multiplier(**) = nil # :nodoc:
276
+
277
+ def long_context_pricing?(pricing, tokens) # :nodoc:
278
+ pricing.long_context &&
279
+ pricing.long_context_threshold &&
280
+ tokens.input.to_i + tokens.cache_read.to_i + tokens.cache_write.to_i > pricing.long_context_threshold
281
+ end
282
+
283
+ def batch_protocol_name(protocol) # :nodoc:
284
+ protocols.key(protocol)&.to_s
73
285
  end
74
286
 
75
- def embed(text, model:, dimensions:)
76
- payload = render_embedding_payload(text, model:, dimensions:)
77
- response = @connection.post(embedding_url(model:), payload)
78
- parse_embedding_response(response, model:, text:)
287
+ def files? # :nodoc:
288
+ protocols.key?(:files)
79
289
  end
80
290
 
81
- def moderate(input, model:)
82
- payload = render_moderation_payload(input, model:)
83
- response = @connection.post moderation_url, payload
84
- parse_moderation_response(response, model:)
291
+ def list_models # :nodoc:
292
+ listing_protocol.new(self).list_models
85
293
  end
86
294
 
87
- def paint(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Metrics/ParameterLists
88
- validate_paint_inputs!(with:, mask:)
89
- payload = render_image_payload(prompt, model:, size:, with:, mask:, params:)
90
- response = @connection.post images_url(with:, mask:), payload
91
- parse_image_response(response, model:)
295
+ def tokenize(text, model:) # :nodoc:
296
+ protocol = resolve_protocol(nil, model, operation: :tokenize)
297
+ protocol.new(self, model).tokenize(text, model: model_id_for(model))
92
298
  end
93
299
 
94
- def transcribe(audio_file, model:, language:, **options)
95
- file_part = build_audio_file_part(audio_file)
96
- payload = render_transcription_payload(file_part, model:, language:, **options)
97
- response = @connection.post transcription_url, payload
98
- parse_transcription_response(response, model:)
300
+ def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {}) # :nodoc:
301
+ protocol = resolve_protocol(nil, model, operation: :embed)
302
+ protocol.new(self, model).embed(
303
+ text, model: model_id_for(model), dimensions:, task_type:, title:, with:, provider_options:
304
+ )
99
305
  end
100
306
 
101
- def configured?
102
- configuration_requirements.all? { |req| @config.send(req) }
307
+ def render_embedding(text, model:, dimensions: nil) # :nodoc:
308
+ protocol = resolve_protocol(nil, model, operation: :embed)
309
+ protocol.new(self, model).render_embedding(text, model: model_id_for(model), dimensions:)
103
310
  end
104
311
 
105
- def local?
106
- self.class.local?
312
+ def moderate(input, model:, with: [], provider_options: {}) # :nodoc:
313
+ protocol = resolve_protocol(nil, model, operation: :moderate)
314
+ protocol.new(self, model).moderate(
315
+ input, model: model_id_for(model), with:, provider_options:
316
+ )
317
+ end
318
+
319
+ def research_later(prompt, **options) # :nodoc:
320
+ fetch_protocol(:research).new(self).create_research_job(prompt, **options)
321
+ end
322
+
323
+ def find_research_job(id) # :nodoc:
324
+ fetch_protocol(:research).new(self).find_research_job(id)
325
+ end
326
+
327
+ def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {}) # :nodoc:
328
+ protocol = resolve_protocol(nil, model, operation: :paint)
329
+ protocol.new(self, model).paint(
330
+ prompt, model: model_id_for(model), size:, count:, with:, mask:, provider_options:
331
+ )
332
+ end
333
+
334
+ def animate_later(prompt, model:, with: nil, extend: nil, provider_options: {}) # :nodoc:
335
+ protocol = resolve_protocol(nil, model, operation: :animate)
336
+ protocol.new(self, model).animate_later(
337
+ prompt, model: model_id_for(model), with:, extend:, provider_options:
338
+ )
339
+ end
340
+
341
+ def speak(input, model:, voice:, format:, provider_options: {}, &) # :nodoc:
342
+ protocol = resolve_protocol(nil, model, operation: :speak)
343
+ protocol.new(self, model).speak(
344
+ input, model: model_id_for(model), voice:, format:, provider_options:, &
345
+ )
346
+ end
347
+
348
+ def transcribe(audio_file, model:, language:, format: nil, timestamps: nil, speaker_names: nil, # :nodoc:
349
+ speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &)
350
+ protocol = resolve_protocol(nil, model, operation: :transcribe).new(self, model)
351
+ options = protocol.render_transcription_options(timestamps:, format:, streaming: block_given?)
352
+ provider_options = Support::Utils.deep_merge(options, provider_options)
353
+ protocol.transcribe(
354
+ audio_file,
355
+ model: model_id_for(model),
356
+ language:,
357
+ format:,
358
+ speaker_names:,
359
+ speaker_references:,
360
+ provider_options:,
361
+ prompt:,
362
+ temperature:,
363
+ &
364
+ )
365
+ end
366
+
367
+ def ocr(file, model:, pages: nil, provider_options: {}) # :nodoc:
368
+ protocol = resolve_protocol(nil, model, operation: :ocr)
369
+ protocol.new(self, model).ocr(file, model: model_id_for(model), pages:, provider_options:)
370
+ end
371
+
372
+ def rerank(query, documents, model:, top_n: nil, provider_options: {}) # :nodoc:
373
+ protocol = resolve_protocol(nil, model, operation: :rerank)
374
+ protocol.new(self, model).rerank(query, documents, model: model_id_for(model), top_n:, provider_options:)
375
+ end
376
+
377
+ def upload_file(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil, # :nodoc:
378
+ provider_options: {})
379
+ ensure_files_supported!
380
+ options = { filename:, purpose:, expires_in:, uri:, content_type:, provider_options: }.compact
381
+
382
+ protocols.fetch(:files).new(self).upload(file, **options)
383
+ end
384
+
385
+ def cache_content(content, model:, ttl: nil, instructions: nil, with: nil) # :nodoc:
386
+ protocol = resolve_protocol(nil, model, operation: :cache)
387
+ protocol.new(self, model).cache_content(
388
+ content, model: model_id_for(model), ttl:, instructions:, with:
389
+ )
390
+ end
391
+
392
+ def find_cache(name) # :nodoc:
393
+ default_protocol.new(self).find_cache(name)
394
+ end
395
+
396
+ def delete_cache(name) # :nodoc:
397
+ default_protocol.new(self).delete_cache(name)
398
+ end
399
+
400
+ def extend_cache(name, ttl:) # :nodoc:
401
+ default_protocol.new(self).extend_cache(name, ttl:)
107
402
  end
108
403
 
109
- def remote?
110
- self.class.remote?
404
+ def find_file(file_id) # :nodoc:
405
+ ensure_files_supported!
406
+ protocols.fetch(:files).new(self).find(file_id)
111
407
  end
112
408
 
113
- def assume_models_exist?
409
+ def download_file(file_id) # :nodoc:
410
+ ensure_files_supported!
411
+ protocols.fetch(:files).new(self).download(file_id)
412
+ end
413
+
414
+ def list_file_uris(uri) # :nodoc:
415
+ ensure_files_supported!
416
+ protocols.fetch(:files).new(self).list_uris(uri)
417
+ end
418
+
419
+ def configured? # :nodoc:
420
+ self.class.configured?(@config)
421
+ end
422
+
423
+ def local? # :nodoc:
424
+ self.class.local?
425
+ end
426
+
427
+ def assume_models_exist? # :nodoc:
114
428
  self.class.assume_models_exist?
115
429
  end
116
430
 
117
- def parse_error(response)
118
- return if response.body.empty?
431
+ def parse_error(response) # :nodoc:
432
+ body = parse_error_body(response)
433
+ return unless body
119
434
 
120
- body = try_parse_json(response.body)
121
435
  case body
122
436
  when Hash
123
- error = body['error']
124
- return error if error.is_a?(String)
125
-
126
- body.dig('error', 'message')
437
+ error_part_message(body)
127
438
  when Array
128
- body.map do |part|
129
- error = part['error']
130
- error.is_a?(String) ? error : part.dig('error', 'message')
131
- end.join('. ')
439
+ messages = body.filter_map { |part| error_part_message(part) }.reject(&:empty?)
440
+ messages.join('. ') unless messages.empty?
132
441
  else
133
442
  body
134
443
  end
135
444
  end
136
445
 
137
- def format_messages(messages)
138
- messages.map do |msg|
139
- {
140
- role: msg.role.to_s,
141
- content: msg.content
142
- }
143
- end
144
- end
145
-
146
- def format_tool_calls(_tool_calls)
147
- nil
148
- end
446
+ class << self
447
+ attr_reader :default_protocol # :nodoc:
448
+ attr_writer :slug # :nodoc:
149
449
 
150
- def parse_tool_calls(_tool_calls)
151
- nil
152
- end
450
+ # Returns the provider slug, a short lowercase string that
451
+ # identifies the provider and prefixes its configuration keys.
452
+ # Set by ::register, or derived from the class name.
453
+ def slug
454
+ @slug ||= to_s.split('::').last.downcase
455
+ end
153
456
 
154
- class << self
155
- def name
457
+ # Returns the human-readable provider name, derived from the
458
+ # class name. Override for custom branding.
459
+ def display_name
156
460
  to_s.split('::').last
157
461
  end
158
462
 
159
- def slug
160
- name.downcase
463
+ # Returns the provider's narrow model capability augmenter, or +nil+
464
+ # when models.dev and the provider listing are sufficient.
465
+ def capabilities
466
+ nil
161
467
  end
162
468
 
163
- def capabilities
469
+ def models_dev_alias(_model_id, _models_dev_by_key, _provider_model = nil) # :nodoc:
164
470
  nil
165
471
  end
166
472
 
473
+ # The id RubyLLM registers for a models.dev entry. Providers whose
474
+ # catalog spells ids differently from models.dev override this.
475
+ def models_dev_model_id(id) # :nodoc:
476
+ id
477
+ end
478
+
479
+ # Returns the configuration keys that must be set before the
480
+ # provider is usable. The base implementation returns an empty
481
+ # array.
482
+ #
483
+ # def self.configuration_requirements
484
+ # %i[acme_api_key]
485
+ # end
486
+ #
167
487
  def configuration_requirements
168
488
  []
169
489
  end
170
490
 
491
+ # Returns every configuration key the provider contributes.
492
+ # ::register defines a Configuration accessor for each one.
493
+ # The base implementation returns an empty array.
494
+ #
495
+ # def self.configuration_options
496
+ # %i[acme_api_key acme_api_base]
497
+ # end
498
+ #
171
499
  def configuration_options
172
500
  []
173
501
  end
174
502
 
503
+ # Returns whether the provider talks to a locally hosted service.
504
+ # The base implementation returns +false+. Local providers such as
505
+ # Ollama return +true+.
175
506
  def local?
176
507
  false
177
508
  end
178
509
 
179
- def remote?
510
+ def remote? # :nodoc:
180
511
  !local?
181
512
  end
182
513
 
514
+ # Returns whether the provider accepts model ids missing from the
515
+ # model registry. The base implementation returns +false+.
183
516
  def assume_models_exist?
184
517
  false
185
518
  end
186
519
 
187
- def configured?(config)
520
+ # Returns whether +operation+ requires an inference model. Override
521
+ # for endpoints that operate on an explicitly configured resource.
522
+ def model_required?(**)
523
+ true
524
+ end
525
+
526
+ def configured?(config) # :nodoc:
188
527
  configuration_requirements.all? { |req| config.send(req) }
189
528
  end
190
529
 
191
- def register(name, provider_class)
530
+ # Registers +protocol_class+ under +name+. The first registered
531
+ # protocol becomes the provider's default. Pass +batches:+ to compose
532
+ # batch operations into the registered protocol.
533
+ #
534
+ # protocol :chat_completions, ChatCompletions
535
+ # protocol :responses, Protocols::Responses, batches: Protocols::Responses::Batches
536
+ #
537
+ def protocol(name, protocol_class, batches: nil)
538
+ @default_protocol = name.to_sym if protocols.empty?
539
+ protocols[name.to_sym] = batches ? Class.new(protocol_class) { include batches } : protocol_class
540
+ end
541
+
542
+ def protocols # :nodoc:
543
+ @protocols ||= {}
544
+ end
545
+
546
+ # Registers +provider_class+ under the slug +name+, making it
547
+ # available to RubyLLM.chat and the other top-level helpers.
548
+ # Stamps the class's slug, adds it to ::providers, and defines a
549
+ # Configuration accessor for each of its configuration options. A
550
+ # provider gem may pass the path to its bundled model catalog.
551
+ #
552
+ # RubyLLM::Provider.register :acme, RubyLLM::Providers::Acme
553
+ # RubyLLM::Provider.register :acme, RubyLLM::Providers::Acme,
554
+ # models: File.expand_path('../../../models.json', __dir__)
555
+ #
556
+ def register(name, provider_class, models: nil)
557
+ provider_class.slug = name.to_s
192
558
  providers[name.to_sym] = provider_class
193
- RubyLLM::Configuration.register_provider_options(provider_class.configuration_options)
559
+ models ? model_registry_files[name.to_sym] = models : model_registry_files.delete(name.to_sym)
560
+ RubyLLM::Configuration.register_provider_options(provider_class.configuration_options + [:"#{name}_protocol"])
194
561
  end
195
562
 
196
- def resolve(name)
563
+ def resolve(name) # :nodoc:
197
564
  providers[name.to_sym]
198
565
  end
199
566
 
200
- def for(model)
201
- model_info = Models.find(model)
202
- resolve model_info.provider
567
+ def resolve!(name) # :nodoc:
568
+ providers[name.to_sym] ||
569
+ raise(Error, "Unknown provider: #{name.inspect}. Available providers: #{providers.keys.join(', ')}")
570
+ end
571
+
572
+ # Resolves +model_id+ to the id the registry stores it under for this
573
+ # provider. Defaults to the id unchanged; providers whose catalog ids
574
+ # differ from their request ids (Bedrock's region prefixes) override it.
575
+ def resolve_registry_id(model_id, _models, _config = nil)
576
+ model_id
203
577
  end
204
578
 
579
+ # Returns the global registry of providers, a hash mapping slug
580
+ # symbols to provider classes.
205
581
  def providers
206
582
  @providers ||= {}
207
583
  end
208
584
 
209
- def local_providers
585
+ def model_registry_files # :nodoc:
586
+ @model_registry_files ||= {}
587
+ end
588
+
589
+ def local_providers # :nodoc:
210
590
  providers.select { |_slug, provider_class| provider_class.local? }
211
591
  end
212
592
 
213
- def remote_providers
593
+ def remote_providers # :nodoc:
214
594
  providers.select { |_slug, provider_class| provider_class.remote? }
215
595
  end
216
596
 
217
- def configured_providers(config)
597
+ def configured_providers(config) # :nodoc:
218
598
  providers.select do |_slug, provider_class|
219
599
  provider_class.configured?(config)
220
600
  end.values
221
601
  end
222
602
 
223
- def configured_remote_providers(config)
603
+ def configured_remote_providers(config) # :nodoc:
224
604
  providers.select do |_slug, provider_class|
225
605
  provider_class.remote? && provider_class.configured?(config)
226
606
  end.values
@@ -229,25 +609,62 @@ module RubyLLM
229
609
 
230
610
  private
231
611
 
232
- def validate_paint_inputs!(with:, mask:)
233
- return if with.nil? && mask.nil?
612
+ def ensure_batches_supported!(protocol = batch_protocol)
613
+ raise Error, "#{slug} doesn't support batch requests" unless protocol.public_method_defined?(:create_batch)
614
+ end
615
+
616
+ def ensure_files_supported!
617
+ return if files?
234
618
 
235
- raise UnsupportedAttachmentError, 'image reference'
619
+ raise Error, "#{slug} doesn't support file uploads"
236
620
  end
237
621
 
238
- def build_audio_file_part(file_path)
239
- require 'faraday/multipart'
240
- require 'marcel'
241
- require 'pathname'
622
+ def resolve_protocol(name, model, **request)
623
+ explicit = name || configured_protocol
624
+ explicit ? fetch_protocol(explicit) : protocol_for(model, **request)
625
+ end
242
626
 
243
- expanded_path = File.expand_path(file_path)
244
- mime_type = Marcel::MimeType.for(Pathname.new(expanded_path))
627
+ def default_protocol
628
+ fetch_protocol(configured_protocol || self.class.default_protocol)
629
+ end
245
630
 
246
- Faraday::Multipart::FilePart.new(
247
- expanded_path,
248
- mime_type,
249
- File.basename(expanded_path)
250
- )
631
+ # The model catalog lives at the provider's own listing endpoint, which
632
+ # the chat protocol override has no say over.
633
+ def listing_protocol
634
+ fetch_protocol(self.class.default_protocol)
635
+ end
636
+
637
+ def batch_protocol
638
+ fetch_protocol(self.class.default_protocol)
639
+ end
640
+
641
+ def batch_protocol_for(_requests)
642
+ batch_protocol
643
+ end
644
+
645
+ def batch_protocol_for_name(name)
646
+ protocol = protocols[name.to_sym]
647
+ protocol if protocol&.public_method_defined?(:create_batch)
648
+ end
649
+
650
+ def resolve_batch_protocol(protocol)
651
+ return protocol if protocol.is_a?(Module)
652
+
653
+ protocol && batch_protocol_for_name(protocol)
654
+ end
655
+
656
+ def configured_protocol
657
+ @config.send(:"#{slug}_protocol")
658
+ end
659
+
660
+ def fetch_protocol(name)
661
+ protocols.fetch(name.to_sym) do
662
+ raise Error, "#{name} is not a protocol of #{self.class.display_name}. Available: #{protocols.keys.join(', ')}"
663
+ end
664
+ end
665
+
666
+ def model_id_for(model)
667
+ model.respond_to?(:id) ? model.id : model
251
668
  end
252
669
 
253
670
  def try_parse_json(maybe_json)
@@ -258,24 +675,39 @@ module RubyLLM
258
675
  maybe_json
259
676
  end
260
677
 
678
+ def parse_error_body(response)
679
+ body = response.body
680
+ return if body.nil? || (body.respond_to?(:empty?) && body.empty?)
681
+
682
+ try_parse_json(body)
683
+ end
684
+
685
+ def error_part_message(part)
686
+ return part.to_s unless part.is_a?(Hash)
687
+
688
+ error = part['error']
689
+ return error if error.is_a?(String)
690
+
691
+ nested_message = error['message'] if error.is_a?(Hash)
692
+ [nested_message, part['message'], part['detail']].find { |message| message.is_a?(String) }
693
+ end
694
+
261
695
  def ensure_configured!
696
+ return if configured?
697
+
262
698
  missing = configuration_requirements.reject { |req| @config.send(req) }
263
- return if missing.empty?
699
+ config_block = <<~RUBY
700
+ RubyLLM.configure do |config|
701
+ #{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
702
+ end
703
+ RUBY
264
704
 
265
705
  raise ConfigurationError,
266
- "Missing configuration for #{name}: #{missing.join(', ')}. " \
267
- 'Set these keys on RubyLLM.config before using this provider.'
268
- end
269
-
270
- def maybe_normalize_temperature(temperature, _model)
271
- temperature
706
+ "#{name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
272
707
  end
273
708
 
274
- def sync_response(connection, payload, additional_headers = {})
275
- response = connection.post completion_url, payload do |req|
276
- req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
277
- end
278
- parse_completion_response response
709
+ def inspect_attributes # :nodoc:
710
+ { slug: slug }
279
711
  end
280
712
  end
281
713
  end