ruby_llm 1.15.0 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -6
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1208 -150
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +274 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +155 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +540 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/protocols/chat_completions/media.rb +121 -0
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +17 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -1,223 +1,606 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+ require 'ruby_llm/error'
5
+
3
6
  module RubyLLM
4
- # 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.
5
34
  class Provider
6
- include Streaming
35
+ include Support::Inspectable
36
+
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
45
+
46
+ # The Configuration the provider was built with.
47
+ attr_reader :config
7
48
 
8
- attr_reader :config, :connection
49
+ attr_reader :connection # :nodoc:
9
50
 
10
- def initialize(config)
51
+ def initialize(config) # :nodoc:
11
52
  @config = config
12
53
  ensure_configured!
13
- @connection = Connection.new(self, @config)
54
+ @connection = Transport::Connection.new(self, @config)
14
55
  end
15
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
+ #
16
65
  def api_base
17
66
  raise NotImplementedError
18
67
  end
19
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
+ #
20
76
  def headers
21
77
  {}
22
78
  end
23
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.
24
95
  def slug
25
96
  self.class.slug
26
97
  end
27
98
 
28
- def name
29
- self.class.name
99
+ # Returns the human-readable provider name, delegating to
100
+ # ::display_name.
101
+ def name # :nodoc:
102
+ self.class.display_name
30
103
  end
31
104
 
32
- def capabilities
105
+ def capabilities # :nodoc:
33
106
  self.class.capabilities
34
107
  end
35
108
 
36
- def configuration_requirements
109
+ def configuration_requirements # :nodoc:
37
110
  self.class.configuration_requirements
38
111
  end
39
112
 
40
- # rubocop:disable Metrics/ParameterLists
41
- def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
42
- tool_prefs: nil, &)
43
- normalized_temperature = maybe_normalize_temperature(temperature, model)
44
-
45
- payload = Utils.deep_merge(
46
- render_payload(
47
- messages,
48
- tools: tools,
49
- tool_prefs: tool_prefs,
50
- temperature: normalized_temperature,
51
- model: model,
52
- stream: block_given?,
53
- schema: schema,
54
- thinking: thinking
55
- ),
56
- 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
+ &
57
154
  )
155
+ end
58
156
 
59
- if block_given?
60
- stream_response @connection, payload, headers, &
61
- else
62
- 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]
63
272
  end
64
273
  end
65
- # rubocop:enable Metrics/ParameterLists
66
274
 
67
- def list_models
68
- response = @connection.get models_url
69
- 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
70
285
  end
71
286
 
72
- def embed(text, model:, dimensions:)
73
- payload = render_embedding_payload(text, model:, dimensions:)
74
- response = @connection.post(embedding_url(model:), payload)
75
- parse_embedding_response(response, model:, text:)
287
+ def files? # :nodoc:
288
+ protocols.key?(:files)
76
289
  end
77
290
 
78
- def moderate(input, model:)
79
- payload = render_moderation_payload(input, model:)
80
- response = @connection.post moderation_url, payload
81
- parse_moderation_response(response, model:)
291
+ def list_models # :nodoc:
292
+ listing_protocol.new(self).list_models
82
293
  end
83
294
 
84
- def paint(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Metrics/ParameterLists
85
- validate_paint_inputs!(with:, mask:)
86
- payload = render_image_payload(prompt, model:, size:, with:, mask:, params:)
87
- response = @connection.post images_url(with:, mask:), payload
88
- 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))
89
298
  end
90
299
 
91
- def transcribe(audio_file, model:, language:, **options)
92
- file_part = build_audio_file_part(audio_file)
93
- payload = render_transcription_payload(file_part, model:, language:, **options)
94
- response = @connection.post transcription_url, payload
95
- 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
+ )
96
305
  end
97
306
 
98
- def configured?
99
- 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:)
100
310
  end
101
311
 
102
- def local?
103
- 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
+ )
104
317
  end
105
318
 
106
- def remote?
107
- self.class.remote?
319
+ def research_later(prompt, **options) # :nodoc:
320
+ fetch_protocol(:research).new(self).create_research_job(prompt, **options)
108
321
  end
109
322
 
110
- def assume_models_exist?
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:)
402
+ end
403
+
404
+ def find_file(file_id) # :nodoc:
405
+ ensure_files_supported!
406
+ protocols.fetch(:files).new(self).find(file_id)
407
+ end
408
+
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:
111
428
  self.class.assume_models_exist?
112
429
  end
113
430
 
114
- def parse_error(response)
115
- return if response.body.empty?
431
+ def parse_error(response) # :nodoc:
432
+ body = parse_error_body(response)
433
+ return unless body
116
434
 
117
- body = try_parse_json(response.body)
118
435
  case body
119
436
  when Hash
120
- error = body['error']
121
- return error if error.is_a?(String)
122
-
123
- body.dig('error', 'message')
437
+ error_part_message(body)
124
438
  when Array
125
- body.map do |part|
126
- error = part['error']
127
- error.is_a?(String) ? error : part.dig('error', 'message')
128
- end.join('. ')
439
+ messages = body.filter_map { |part| error_part_message(part) }.reject(&:empty?)
440
+ messages.join('. ') unless messages.empty?
129
441
  else
130
442
  body
131
443
  end
132
444
  end
133
445
 
134
- def format_messages(messages)
135
- messages.map do |msg|
136
- {
137
- role: msg.role.to_s,
138
- content: msg.content
139
- }
140
- end
141
- end
142
-
143
- def format_tool_calls(_tool_calls)
144
- nil
145
- end
446
+ class << self
447
+ attr_reader :default_protocol # :nodoc:
448
+ attr_writer :slug # :nodoc:
146
449
 
147
- def parse_tool_calls(_tool_calls)
148
- nil
149
- 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
150
456
 
151
- class << self
152
- def name
457
+ # Returns the human-readable provider name, derived from the
458
+ # class name. Override for custom branding.
459
+ def display_name
153
460
  to_s.split('::').last
154
461
  end
155
462
 
156
- def slug
157
- 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
158
467
  end
159
468
 
160
- def capabilities
469
+ def models_dev_alias(_model_id, _models_dev_by_key, _provider_model = nil) # :nodoc:
161
470
  nil
162
471
  end
163
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
+ #
164
487
  def configuration_requirements
165
488
  []
166
489
  end
167
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
+ #
168
499
  def configuration_options
169
500
  []
170
501
  end
171
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+.
172
506
  def local?
173
507
  false
174
508
  end
175
509
 
176
- def remote?
510
+ def remote? # :nodoc:
177
511
  !local?
178
512
  end
179
513
 
514
+ # Returns whether the provider accepts model ids missing from the
515
+ # model registry. The base implementation returns +false+.
180
516
  def assume_models_exist?
181
517
  false
182
518
  end
183
519
 
184
- 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:
185
527
  configuration_requirements.all? { |req| config.send(req) }
186
528
  end
187
529
 
188
- 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
189
558
  providers[name.to_sym] = provider_class
190
- 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"])
191
561
  end
192
562
 
193
- def resolve(name)
563
+ def resolve(name) # :nodoc:
194
564
  providers[name.to_sym]
195
565
  end
196
566
 
197
- def for(model)
198
- model_info = Models.find(model)
199
- 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
200
577
  end
201
578
 
579
+ # Returns the global registry of providers, a hash mapping slug
580
+ # symbols to provider classes.
202
581
  def providers
203
582
  @providers ||= {}
204
583
  end
205
584
 
206
- def local_providers
585
+ def model_registry_files # :nodoc:
586
+ @model_registry_files ||= {}
587
+ end
588
+
589
+ def local_providers # :nodoc:
207
590
  providers.select { |_slug, provider_class| provider_class.local? }
208
591
  end
209
592
 
210
- def remote_providers
593
+ def remote_providers # :nodoc:
211
594
  providers.select { |_slug, provider_class| provider_class.remote? }
212
595
  end
213
596
 
214
- def configured_providers(config)
597
+ def configured_providers(config) # :nodoc:
215
598
  providers.select do |_slug, provider_class|
216
599
  provider_class.configured?(config)
217
600
  end.values
218
601
  end
219
602
 
220
- def configured_remote_providers(config)
603
+ def configured_remote_providers(config) # :nodoc:
221
604
  providers.select do |_slug, provider_class|
222
605
  provider_class.remote? && provider_class.configured?(config)
223
606
  end.values
@@ -226,21 +609,62 @@ module RubyLLM
226
609
 
227
610
  private
228
611
 
229
- def validate_paint_inputs!(with:, mask:)
230
- 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?
231
618
 
232
- raise UnsupportedAttachmentError, "#{name} does not support image references in paint"
619
+ raise Error, "#{slug} doesn't support file uploads"
233
620
  end
234
621
 
235
- def build_audio_file_part(file_path)
236
- expanded_path = File.expand_path(file_path)
237
- mime_type = Marcel::MimeType.for(Pathname.new(expanded_path))
622
+ def resolve_protocol(name, model, **request)
623
+ explicit = name || configured_protocol
624
+ explicit ? fetch_protocol(explicit) : protocol_for(model, **request)
625
+ end
238
626
 
239
- Faraday::Multipart::FilePart.new(
240
- expanded_path,
241
- mime_type,
242
- File.basename(expanded_path)
243
- )
627
+ def default_protocol
628
+ fetch_protocol(configured_protocol || self.class.default_protocol)
629
+ end
630
+
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
244
668
  end
245
669
 
246
670
  def try_parse_json(maybe_json)
@@ -251,22 +675,39 @@ module RubyLLM
251
675
  maybe_json
252
676
  end
253
677
 
254
- def ensure_configured!
255
- missing = configuration_requirements.reject { |req| @config.send(req) }
256
- return if missing.empty?
678
+ def parse_error_body(response)
679
+ body = response.body
680
+ return if body.nil? || (body.respond_to?(:empty?) && body.empty?)
257
681
 
258
- raise ConfigurationError, "Missing configuration for #{name}: #{missing.join(', ')}"
682
+ try_parse_json(body)
259
683
  end
260
684
 
261
- def maybe_normalize_temperature(temperature, _model)
262
- temperature
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) }
263
693
  end
264
694
 
265
- def sync_response(connection, payload, additional_headers = {})
266
- response = connection.post completion_url, payload do |req|
267
- req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
268
- end
269
- parse_completion_response response
695
+ def ensure_configured!
696
+ return if configured?
697
+
698
+ missing = configuration_requirements.reject { |req| @config.send(req) }
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
704
+
705
+ raise ConfigurationError,
706
+ "#{name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
707
+ end
708
+
709
+ def inspect_attributes # :nodoc:
710
+ { slug: slug }
270
711
  end
271
712
  end
272
713
  end