ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -3,10 +3,15 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # Ollama API integration.
6
- class Ollama < OpenAI
7
- include Ollama::Chat
8
- include Ollama::Media
9
- include Ollama::Models
6
+ class Ollama < Provider
7
+ # Ollama's dialect of the Chat Completions API.
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include Ollama::Chat
10
+ include Ollama::Media
11
+ include Ollama::Models
12
+ end
13
+
14
+ protocol :chat_completions, ChatCompletions
10
15
 
11
16
  def api_base
12
17
  @config.ollama_api_base
@@ -30,10 +35,6 @@ module RubyLLM
30
35
  def local?
31
36
  true
32
37
  end
33
-
34
- def capabilities
35
- Ollama::Capabilities
36
- end
37
38
  end
38
39
  end
39
40
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OllamaCloud
6
+ # Models methods for the Ollama Cloud API integration
7
+ module Models
8
+ def model_capabilities
9
+ super - %w[structured_output]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Ollama Cloud API integration.
6
+ class OllamaCloud < Provider
7
+ # Ollama Cloud's dialect of the Chat Completions API.
8
+ class ChatCompletions < Ollama::ChatCompletions
9
+ include OllamaCloud::Models
10
+ end
11
+
12
+ protocol :chat_completions, ChatCompletions
13
+
14
+ def api_base
15
+ @config.ollama_cloud_api_base || 'https://ollama.com/v1'
16
+ end
17
+
18
+ def headers
19
+ { 'Authorization' => "Bearer #{@config.ollama_cloud_api_key}" }
20
+ end
21
+
22
+ class << self
23
+ def configuration_options
24
+ %i[ollama_cloud_api_key ollama_cloud_api_base]
25
+ end
26
+
27
+ def configuration_requirements
28
+ %i[ollama_cloud_api_key]
29
+ end
30
+
31
+ # Returns +true+: Ollama retires cloud models faster than the
32
+ # registry tracks them, so model ids are accepted as given. Call
33
+ # RubyLLM.models.refresh to pull the live catalog.
34
+ def assume_models_exist?
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,270 +3,65 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAI
6
- # Provider-level capability checks and narrow registry fallbacks.
6
+ # Feature capability gaps not represented in upstream model catalogs.
7
7
  module Capabilities
8
- module_function
9
-
10
- MODEL_PATTERNS = {
11
- gpt_image15: /^gpt-image-1\.5/,
12
- gpt_image_mini: /^gpt-image-1-mini/,
13
- gpt_image: /^gpt-image-1(?:$|-)/,
14
- gpt41: /^gpt-4\.1(?!-(?:mini|nano))/,
15
- gpt41_mini: /^gpt-4\.1-mini/,
16
- gpt41_nano: /^gpt-4\.1-nano/,
17
- gpt4: /^gpt-4(?:-\d{6})?$/,
18
- gpt4_turbo: /^gpt-4(?:\.5)?-(?:\d{6}-)?(preview|turbo)/,
19
- gpt35_turbo: /^gpt-3\.5-turbo/,
20
- gpt4o: /^gpt-4o(?!-(?:mini|audio|realtime|transcribe|tts|search))/,
21
- gpt4o_audio: /^gpt-4o-(?:audio)/,
22
- gpt4o_mini: /^gpt-4o-mini(?!-(?:audio|realtime|transcribe|tts|search))/,
23
- gpt4o_mini_audio: /^gpt-4o-mini-audio/,
24
- gpt4o_mini_realtime: /^gpt-4o-mini-realtime/,
25
- gpt4o_mini_transcribe: /^gpt-4o-mini-transcribe/,
26
- gpt4o_mini_tts: /^gpt-4o-mini-tts/,
27
- gpt4o_realtime: /^gpt-4o-realtime/,
28
- gpt4o_search: /^gpt-4o-search/,
29
- gpt4o_transcribe: /^gpt-4o-transcribe/,
30
- gpt5: /^gpt-5(?!.*(?:mini|nano))/,
31
- gpt5_mini: /^gpt-5.*mini/,
32
- gpt5_nano: /^gpt-5.*nano/,
33
- o1: /^o1(?!-(?:mini|pro))/,
34
- o1_mini: /^o1-mini/,
35
- o1_pro: /^o1-pro/,
36
- o3_mini: /^o3-mini/,
37
- babbage: /^babbage/,
38
- davinci: /^davinci/,
39
- embedding3_large: /^text-embedding-3-large/,
40
- embedding3_small: /^text-embedding-3-small/,
41
- embedding_ada: /^text-embedding-ada/,
42
- tts1: /^tts-1(?!-hd)/,
43
- tts1_hd: /^tts-1-hd/,
44
- whisper: /^whisper/,
45
- moderation: /^(?:omni|text)-moderation/
46
- }.freeze
47
-
48
- PRICES = {
49
- gpt_image: {
50
- text: { input: 5.0, cached_input: 1.25 },
51
- images: { input: 10.0, cached_input: 2.5, output: 40.0 }
52
- },
53
- gpt_image_mini: {
54
- text: { input: 2.0, cached_input: 0.2 },
55
- images: { input: 2.5, cached_input: 0.25, output: 8.0 }
56
- },
57
- gpt_image15: {
58
- text: { input: 5.0, cached_input: 1.25, output: 10.0 },
59
- images: { input: 8.0, cached_input: 2.0, output: 32.0 }
60
- },
61
- gpt5: { input: 1.25, output: 10.0, cached_input: 0.125 },
62
- gpt5_mini: { input: 0.25, output: 2.0, cached_input: 0.025 },
63
- gpt5_nano: { input: 0.05, output: 0.4, cached_input: 0.005 },
64
- gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
65
- gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
66
- gpt41_nano: { input: 0.1, output: 0.4 },
67
- gpt4: { input: 10.0, output: 30.0 },
68
- gpt4_turbo: { input: 10.0, output: 30.0 },
69
- gpt35_turbo: { input: 0.5, output: 1.5 },
70
- gpt4o: { input: 2.5, output: 10.0 },
71
- gpt4o_audio: { input: 2.5, output: 10.0 },
72
- gpt4o_mini: { input: 0.15, output: 0.6 },
73
- gpt4o_mini_audio: { input: 0.15, output: 0.6 },
74
- gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
75
- gpt4o_mini_transcribe: { input: 1.25, output: 5.0 },
76
- gpt4o_mini_tts: { input: 0.6, output: 12.0 },
77
- gpt4o_realtime: { input: 5.0, output: 20.0 },
78
- gpt4o_search: { input: 2.5, output: 10.0 },
79
- gpt4o_transcribe: { input: 2.5, output: 10.0 },
80
- o1: { input: 15.0, output: 60.0 },
81
- o1_mini: { input: 1.1, output: 4.4 },
82
- o1_pro: { input: 150.0, output: 600.0 },
83
- o3_mini: { input: 1.1, output: 4.4 },
84
- babbage: { input: 0.4, output: 0.4 },
85
- davinci: { input: 2.0, output: 2.0 },
86
- embedding3_large: { price: 0.13 },
87
- embedding3_small: { price: 0.02 },
88
- embedding_ada: { price: 0.10 },
89
- tts1: { price: 15.0 },
90
- tts1_hd: { price: 30.0 },
91
- whisper: { price: 0.006 },
92
- moderation: { price: 0.0 }
93
- }.freeze
94
-
95
- NIL_LIMIT_FAMILIES = %w[
96
- gpt_image
97
- gpt_image_mini
98
- gpt_image15
99
- gpt4o_mini_tts
100
- tts1
101
- tts1_hd
102
- whisper
103
- moderation
104
- embedding3_large
105
- embedding3_small
106
- embedding_ada
8
+ TOOL_CAPABILITIES = %w[tool_choice parallel_tool_calls].freeze
9
+ CHAT_MODELS = %w[
10
+ gpt-5-chat-latest
11
+ gpt-5.1-chat-latest
107
12
  ].freeze
13
+ CODEX_MODELS = %w[
14
+ gpt-5-codex
15
+ gpt-5.1-codex
16
+ gpt-5.1-codex-max
17
+ gpt-5.1-codex-mini
18
+ gpt-5.2-codex
19
+ ].freeze
20
+ SEARCH_MODELS = %w[
21
+ gpt-4o-mini-search-preview
22
+ gpt-4o-mini-search-preview-2025-03-11
23
+ gpt-4o-search-preview
24
+ gpt-4o-search-preview-2025-03-11
25
+ gpt-5-search-api
26
+ gpt-5-search-api-2025-10-14
27
+ ].freeze
28
+ DEEP_RESEARCH_MODELS = %w[
29
+ o3-deep-research
30
+ o3-deep-research-2025-06-26
31
+ o4-mini-deep-research
32
+ o4-mini-deep-research-2025-06-26
33
+ ].freeze
34
+ MODERATION_MODELS = %w[
35
+ omni-moderation-2024-09-26
36
+ omni-moderation-latest
37
+ ].freeze
38
+ TRANSCRIPTION_MODELS = %w[
39
+ gpt-live-transcribe
40
+ gpt-realtime-whisper
41
+ gpt-transcribe
42
+ gpt-4o-mini-transcribe
43
+ gpt-4o-mini-transcribe-2025-03-20
44
+ gpt-4o-mini-transcribe-2025-12-15
45
+ gpt-4o-transcribe
46
+ gpt-4o-transcribe-diarize
47
+ whisper-1
48
+ ].freeze
49
+ EXPLICIT_CAPABILITIES = {
50
+ 'function_calling' => (CHAT_MODELS + CODEX_MODELS).freeze,
51
+ 'structured_output' => (CHAT_MODELS + CODEX_MODELS + SEARCH_MODELS).freeze,
52
+ 'vision' => (CHAT_MODELS + CODEX_MODELS + DEEP_RESEARCH_MODELS + MODERATION_MODELS).freeze,
53
+ 'reasoning' => (CODEX_MODELS + DEEP_RESEARCH_MODELS).freeze,
54
+ 'transcription' => TRANSCRIPTION_MODELS,
55
+ 'citations' => SEARCH_MODELS
56
+ }.freeze
108
57
 
109
- def supports_tool_choice?(_model_id)
110
- true
111
- end
112
-
113
- def supports_tool_parallel_control?(_model_id)
114
- true
115
- end
116
-
117
- def context_window_for(model_id)
118
- family = model_family(model_id)
119
- return nil if NIL_LIMIT_FAMILIES.include?(family)
120
-
121
- case family
122
- when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
123
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
124
- 'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime', 'gpt4o_search',
125
- 'gpt4o_transcribe', 'o1_mini' then 128_000
126
- when 'gpt4' then 8_192
127
- when 'gpt4o_mini_transcribe' then 16_000
128
- when 'o1', 'o1_pro', 'o3_mini' then 200_000
129
- when 'gpt35_turbo' then 16_385
130
- else 4_096
131
- end
132
- end
133
-
134
- def max_tokens_for(model_id)
135
- family = model_family(model_id)
136
- return nil if NIL_LIMIT_FAMILIES.include?(family)
137
-
138
- case family
139
- when 'gpt5', 'gpt5_mini', 'gpt5_nano' then 400_000
140
- when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
141
- when 'gpt4' then 8_192
142
- when 'gpt35_turbo' then 4_096
143
- when 'gpt4o_mini_transcribe' then 2_000
144
- when 'o1', 'o1_pro', 'o3_mini' then 100_000
145
- when 'o1_mini' then 65_536
146
- else 16_384
147
- end
148
- end
149
-
150
- def critical_capabilities_for(model_id)
151
- capabilities = []
152
- capabilities << 'function_calling' if supports_functions?(model_id)
153
- capabilities << 'structured_output' if supports_structured_output?(model_id)
154
- capabilities << 'vision' if supports_vision?(model_id)
155
- capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
156
- capabilities
157
- end
158
-
159
- def pricing_for(model_id)
160
- return image_pricing_for(model_id) if image_model?(model_id)
161
-
162
- standard_pricing = {
163
- input_per_million: input_price_for(model_id),
164
- output_per_million: output_price_for(model_id)
165
- }
166
-
167
- cached_price = cached_input_price_for(model_id)
168
- standard_pricing[:cache_read_input_per_million] = cached_price if cached_price
169
-
170
- { text_tokens: { standard: standard_pricing } }
171
- end
172
-
173
- def model_family(model_id)
174
- MODEL_PATTERNS.each do |family, pattern|
175
- return family.to_s if model_id.match?(pattern)
176
- end
177
-
178
- 'other'
179
- end
180
-
181
- def supports_vision?(model_id)
182
- case model_family(model_id)
183
- when 'gpt_image', 'gpt_image_mini', 'gpt_image15', 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini',
184
- 'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'moderation',
185
- 'gpt4o_search'
186
- true
187
- else
188
- false
189
- end
190
- end
191
-
192
- def supports_functions?(model_id)
193
- case model_family(model_id)
194
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4',
195
- 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
196
- true
197
- else
198
- false
199
- end
200
- end
201
-
202
- def supports_structured_output?(model_id)
203
- case model_family(model_id)
204
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4o',
205
- 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
206
- true
207
- else
208
- false
58
+ def self.augment(capabilities, model_id:, **)
59
+ additions = EXPLICIT_CAPABILITIES.filter_map do |capability, models|
60
+ capability if models.include?(model_id)
209
61
  end
62
+ supported = capabilities | additions
63
+ supported | (supported.include?('function_calling') ? TOOL_CAPABILITIES : [])
210
64
  end
211
-
212
- def input_price_for(model_id)
213
- return family_prices(model_id).dig(:text, :input) if image_model?(model_id)
214
-
215
- price_for(model_id, :input, 0.50)
216
- end
217
-
218
- def output_price_for(model_id)
219
- return family_prices(model_id).dig(:text, :output) if image_model?(model_id)
220
-
221
- price_for(model_id, :output, 1.50)
222
- end
223
-
224
- def cached_input_price_for(model_id)
225
- return family_prices(model_id).dig(:text, :cached_input) if image_model?(model_id)
226
-
227
- family = model_family(model_id).to_sym
228
- PRICES.fetch(family, {})[:cached_input]
229
- end
230
-
231
- def image_model?(model_id)
232
- %w[gpt_image gpt_image_mini gpt_image15].include?(model_family(model_id))
233
- end
234
-
235
- def image_pricing_for(model_id)
236
- text_pricing = {
237
- input_per_million: input_price_for(model_id)
238
- }
239
- cached_text_price = cached_input_price_for(model_id)
240
- text_pricing[:cache_read_input_per_million] = cached_text_price if cached_text_price
241
-
242
- image_pricing = {
243
- input_per_million: family_prices(model_id).dig(:images, :input),
244
- output_per_million: family_prices(model_id).dig(:images, :output)
245
- }
246
- cached_image_price = family_prices(model_id).dig(:images, :cached_input)
247
- image_pricing[:cache_read_input_per_million] = cached_image_price if cached_image_price
248
-
249
- {
250
- text_tokens: { standard: text_pricing },
251
- images: { standard: image_pricing }
252
- }
253
- end
254
-
255
- def price_for(model_id, key, fallback)
256
- prices = family_prices(model_id)
257
- prices = { key => fallback } if prices.empty?
258
- prices[key] || prices[:price] || fallback
259
- end
260
-
261
- def family_prices(model_id)
262
- family = model_family(model_id).to_sym
263
- PRICES.fetch(family, {})
264
- end
265
-
266
- module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
267
- :model_family, :supports_vision?, :supports_functions?, :supports_structured_output?,
268
- :input_price_for, :output_price_for, :cached_input_price_for, :image_model?,
269
- :image_pricing_for, :price_for, :family_prices
270
65
  end
271
66
  end
272
67
  end
@@ -3,33 +3,33 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAI
6
- # Models methods of the OpenAI API integration
6
+ # Provider-owned model catalog reconciliation.
7
7
  module Models
8
- module_function
8
+ SNAPSHOT_BASES = {
9
+ 'gpt-3.5-turbo-0125' => 'gpt-3.5-turbo',
10
+ 'gpt-3.5-turbo-1106' => 'gpt-3.5-turbo',
11
+ 'gpt-4-0314' => 'gpt-4',
12
+ 'gpt-4-0613' => 'gpt-4',
13
+ 'gpt-4-turbo-2024-04-09' => 'gpt-4-turbo',
14
+ 'o1-2024-12-17' => 'o1',
15
+ 'o3-mini-2025-01-31' => 'o3-mini'
16
+ }.freeze
9
17
 
10
- def models_url
11
- 'models'
12
- end
18
+ def self.models_dev_alias(model_id, models_dev_by_key, _provider_model = nil)
19
+ base_id = SNAPSHOT_BASES[model_id]
20
+ if base_id
21
+ source = models_dev_by_key["openai:#{base_id}"]
22
+ return Model.new(source.to_h.merge(id: model_id)) if source
23
+ end
13
24
 
14
- def parse_list_models_response(response, slug, capabilities)
15
- Array(response.body['data']).map do |model_data|
16
- model_id = model_data['id']
25
+ match = model_id.match(/\A(.+)-(\d{4}-\d{2}-\d{2})\z/)
26
+ return unless match
17
27
 
18
- Model::Info.new(
19
- id: model_id,
20
- name: model_id,
21
- provider: slug,
22
- created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
23
- context_window: capabilities.context_window_for(model_id),
24
- max_output_tokens: capabilities.max_tokens_for(model_id),
25
- capabilities: capabilities.critical_capabilities_for(model_id),
26
- pricing: capabilities.pricing_for(model_id),
27
- metadata: {
28
- object: model_data['object'],
29
- owned_by: model_data['owned_by']
30
- }
31
- )
32
- end
28
+ source = models_dev_by_key["openai:#{match[1]}"]
29
+ return unless source&.created_at
30
+ return unless source.created_at.to_date.to_s == match[2]
31
+
32
+ Model.new(source.to_h.merge(id: model_id))
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # OpenAI's Responses API, including input-token counting.
7
+ class Responses < Protocols::Responses
8
+ include Protocols::Responses::TokenCounting
9
+ include Protocols::Responses::Compaction
10
+ end
11
+ end
12
+ end
13
+ end
@@ -4,20 +4,26 @@ module RubyLLM
4
4
  module Providers
5
5
  # OpenAI API integration.
6
6
  class OpenAI < Provider
7
- include OpenAI::Chat
8
- include OpenAI::Embeddings
9
- include OpenAI::Models
10
- include OpenAI::Moderation
11
- include OpenAI::Streaming
12
- include OpenAI::Tools
13
- include OpenAI::Images
14
- include OpenAI::Media
15
- include OpenAI::Transcription
7
+ protocol :responses, OpenAI::Responses, batches: Protocols::Responses::Batches
8
+ protocol :chat_completions, Protocols::ChatCompletions, batches: Protocols::ChatCompletions::Batches
9
+ protocol :embeddings, Protocols::ChatCompletions,
10
+ batches: Protocols::ChatCompletions::EmbeddingBatches
11
+ protocol :files, Protocols::OpenAI::Files
12
+
13
+ RATE_LIMIT_RESET_HEADERS = %w[x-ratelimit-reset-requests x-ratelimit-reset-tokens].freeze
14
+ RESET_DURATION_UNITS = { 'h' => 3600, 'm' => 60, 's' => 1, 'ms' => 0.001 }.freeze
16
15
 
17
16
  def api_base
18
17
  @config.openai_api_base || 'https://api.openai.com/v1'
19
18
  end
20
19
 
20
+ # Audio, realtime, and dedicated search models only exist on Chat Completions.
21
+ def protocol_for(model, **)
22
+ return protocols[:chat_completions] if Capabilities::SEARCH_MODELS.include?(model.id)
23
+
24
+ model.id.match?(/audio|realtime/) ? protocols[:chat_completions] : super
25
+ end
26
+
21
27
  def headers
22
28
  {
23
29
  'Authorization' => "Bearer #{@config.openai_api_key}",
@@ -26,15 +32,37 @@ module RubyLLM
26
32
  }.compact
27
33
  end
28
34
 
29
- def maybe_normalize_temperature(temperature, model)
30
- OpenAI::Temperature.normalize(temperature, model.id)
35
+ # OpenAI reports when each rate limit resets in its own headers,
36
+ # as durations like "6m0s", "7.66s" or "76ms".
37
+ def retry_delay(response)
38
+ headers = response.response_headers
39
+ return unless headers
40
+
41
+ RATE_LIMIT_RESET_HEADERS.filter_map { |header| parse_reset_duration(headers[header]) }.max
42
+ end
43
+
44
+ def find_batch(id)
45
+ batch = super
46
+ protocol = batch_protocol_for_endpoint(batch[:endpoint])
47
+
48
+ protocol ? batch.merge(batch_protocol: protocol) : batch
49
+ end
50
+
51
+ def batch_results(id, batch_protocol: nil)
52
+ super(id, batch_protocol: resolve_batch_protocol(batch_protocol) || batch_protocol_for_stored_batch(id))
31
53
  end
32
54
 
55
+ def batch_cost_multiplier(**) = 0.5
56
+
33
57
  class << self
34
58
  def capabilities
35
59
  OpenAI::Capabilities
36
60
  end
37
61
 
62
+ def models_dev_alias(...)
63
+ OpenAI::Models.models_dev_alias(...)
64
+ end
65
+
38
66
  def configuration_options
39
67
  %i[
40
68
  openai_api_key
@@ -49,6 +77,59 @@ module RubyLLM
49
77
  %i[openai_api_key]
50
78
  end
51
79
  end
80
+
81
+ private
82
+
83
+ def parse_reset_duration(value)
84
+ duration = value.to_s
85
+ parts = duration.scan(/(\d+(?:\.\d+)?)(ms|h|m|s)/)
86
+ return if parts.empty?
87
+ return unless parts.map { |amount, unit| "#{amount}#{unit}" }.join == duration
88
+
89
+ parts.sum { |amount, unit| amount.to_f * RESET_DURATION_UNITS[unit] }
90
+ end
91
+
92
+ def batch_protocol
93
+ batch_protocol_for_name(:responses)
94
+ end
95
+
96
+ def batch_protocol_for(requests)
97
+ names = requests.map { |request| batch_protocol_name_for(request.fetch(:payload)) }.uniq
98
+ return batch_protocol_for_name(names.first) if names.one?
99
+
100
+ raise Error, 'openai batch requests must target one endpoint per submission'
101
+ end
102
+
103
+ def batch_protocol_name_for(payload)
104
+ input = payload[:input] || payload['input']
105
+ return :embeddings if embeddings_input?(input)
106
+ return :responses if input
107
+ return :chat_completions if payload.key?(:messages) || payload.key?('messages')
108
+
109
+ raise Error, 'openai batch requests only support chat, responses, or embedding payloads'
110
+ end
111
+
112
+ # Responses payloads carry the conversation as an array of message hashes
113
+ # under :input; embedding payloads put the text itself there.
114
+ def embeddings_input?(input)
115
+ input.is_a?(String) || (input.is_a?(Array) && !input.empty? && input.all?(String))
116
+ end
117
+
118
+ def batch_protocol_for_stored_batch(id)
119
+ data = @connection.get("batches/#{id}").body
120
+ batch_protocol_for_endpoint(data['endpoint']) || batch_protocol
121
+ end
122
+
123
+ def batch_protocol_for_endpoint(endpoint)
124
+ case endpoint.to_s.delete_prefix('/')
125
+ when 'v1/responses', 'responses'
126
+ batch_protocol_for_name(:responses)
127
+ when 'v1/chat/completions', 'chat/completions'
128
+ batch_protocol_for_name(:chat_completions)
129
+ when 'v1/embeddings', 'embeddings'
130
+ batch_protocol_for_name(:embeddings)
131
+ end
132
+ end
52
133
  end
53
134
  end
54
135
  end