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,43 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # A collection that manages and provides access to different categories of pricing information
4
+ class Model
5
+ # A Pricing groups a model's prices by usage category: text tokens,
6
+ # images, audio tokens, and embeddings. Each category is a
7
+ # PricingCategory. Prices are in USD per million tokens. Instances come
8
+ # from Model#pricing.
9
+ #
10
+ # model = RubyLLM.models.find "claude-sonnet-5"
11
+ # model.pricing.text_tokens.input # => 3
12
+ # model.pricing.text_tokens.output # => 15
13
+ #
6
14
  class Pricing
7
- def initialize(data)
15
+ # The pricing categories a model may define.
16
+ CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
17
+
18
+ def initialize(data) # :nodoc:
8
19
  @data = {}
9
20
 
10
- %i[text_tokens images audio_tokens embeddings].each do |category|
21
+ CATEGORIES.each do |category|
11
22
  @data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
12
23
  end
13
24
  end
14
25
 
15
- def method_missing(method, *args)
16
- if respond_to_missing?(method)
17
- @data[method.to_sym] || PricingCategory.new
18
- else
19
- super
20
- end
26
+ # Returns the PricingCategory for text token prices, or an empty
27
+ # category if the model has none.
28
+ def text_tokens
29
+ category(:text_tokens)
30
+ end
31
+
32
+ # Returns the PricingCategory for image generation prices, or an empty
33
+ # category if the model has none.
34
+ def images
35
+ category(:images)
21
36
  end
22
37
 
23
- def respond_to_missing?(method, include_private = false)
24
- %i[text_tokens images audio_tokens embeddings].include?(method.to_sym) || super
38
+ # Returns the PricingCategory for audio token prices, or an empty
39
+ # category if the model has none.
40
+ def audio_tokens
41
+ category(:audio_tokens)
25
42
  end
26
43
 
44
+ # Returns the PricingCategory for embedding prices, or an empty
45
+ # category if the model has none.
46
+ def embeddings
47
+ category(:embeddings)
48
+ end
49
+
50
+ # Returns the pricing data as a nested Hash keyed by category.
51
+ # Categories without prices are omitted.
27
52
  def to_h
28
53
  @data.transform_values(&:to_h)
29
54
  end
30
55
 
31
56
  private
32
57
 
58
+ def category(name)
59
+ @data[name] || PricingCategory.new
60
+ end
61
+
33
62
  def empty_pricing?(data)
34
63
  return true unless data
35
64
 
36
- %i[standard batch].each do |tier|
65
+ PricingCategory::TIERS.each do |tier|
37
66
  next unless data[tier]
38
67
 
39
68
  data[tier].each_value do |value|
40
- return false if value && value != 0.0
69
+ return false unless value.nil?
41
70
  end
42
71
  end
43
72
 
@@ -1,56 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # Represents pricing tiers for different usage categories (standard and batch)
4
+ class Model
5
+ # A PricingCategory holds the standard, batch, and long-context pricing
6
+ # tiers for one kind of model usage, such as text tokens or images.
7
+ # Model#pricing returns a Pricing collection whose categories are
8
+ # PricingCategory instances. Prices are in USD per million tokens.
9
+ #
10
+ # model = RubyLLM.models.find "claude-sonnet-5"
11
+ # category = model.pricing.text_tokens
12
+ # category.input # => 3
13
+ # category.output # => 15
14
+ #
6
15
  class PricingCategory
7
- attr_reader :standard, :batch
16
+ # The billing tiers a category may define.
17
+ TIERS = %i[standard batch long_context].freeze
8
18
 
9
- def initialize(data = {})
10
- @standard = PricingTier.new(data[:standard] || {}) unless empty_tier?(data[:standard])
11
- @batch = PricingTier.new(data[:batch] || {}) unless empty_tier?(data[:batch])
19
+ # The standard-tier PricingTier, or +nil+ when the model has no
20
+ # standard pricing for this category.
21
+ attr_reader :standard
22
+
23
+ # The batch-tier PricingTier, or +nil+ when the model has no batch
24
+ # pricing for this category.
25
+ attr_reader :batch
26
+
27
+ # The long-context PricingTier, or +nil+ when the model has no
28
+ # separate rates for prompts above #long_context_threshold.
29
+ attr_reader :long_context
30
+
31
+ # Prompt-size threshold (in tokens) above which long-context rates
32
+ # apply, or +nil+ when the model has no long-context tier.
33
+ attr_reader :long_context_threshold
34
+
35
+ def initialize(data = {}) # :nodoc:
36
+ data = data.transform_keys(&:to_sym) if data.respond_to?(:transform_keys)
37
+
38
+ @standard = tier_from(data[:standard])
39
+ @batch = tier_from(data[:batch])
40
+ @long_context = tier_from(data[:long_context])
41
+ @long_context_threshold = Integer(data[:long_context_threshold], exception: false)
12
42
  end
13
43
 
44
+ # Returns the standard-tier input price in USD per million tokens,
45
+ # or +nil+ if the price is missing.
14
46
  def input
15
47
  standard&.input_per_million
16
48
  end
17
49
 
50
+ # Returns the standard-tier output price in USD per million tokens,
51
+ # or +nil+ if the price is missing.
18
52
  def output
19
53
  standard&.output_per_million
20
54
  end
21
55
 
56
+ # Returns the standard-tier cache read price in USD per million
57
+ # tokens, or +nil+ if the price is missing.
22
58
  def cache_read_input
23
- standard&.cache_read_input_per_million || standard&.cached_input_per_million
59
+ standard&.cache_read_input_per_million
24
60
  end
25
61
 
62
+ # Returns the standard-tier cache write price in USD per million
63
+ # tokens, or +nil+ if the price is missing.
26
64
  def cache_write_input
27
- standard&.cache_write_input_per_million || standard&.cache_creation_input_per_million
65
+ standard&.cache_write_input_per_million
28
66
  end
29
67
 
68
+ # Returns the standard-tier reasoning output price in USD per million
69
+ # tokens, or +nil+ if the price is missing.
30
70
  def reasoning_output
31
71
  standard&.reasoning_output_per_million
32
72
  end
33
73
 
34
- alias cached_input cache_read_input
35
- alias cache_creation_input cache_write_input
36
-
37
- def [](key)
38
- key == :batch ? batch : standard
74
+ # Returns the PricingTier that applies for a prompt of the given size.
75
+ # Uses #long_context when that tier exists and +prompt_tokens+ is
76
+ # greater than #long_context_threshold; otherwise returns #standard.
77
+ def tier_for(prompt_tokens)
78
+ if long_context && long_context_threshold &&
79
+ prompt_tokens.to_i > long_context_threshold
80
+ long_context
81
+ else
82
+ standard
83
+ end
39
84
  end
40
85
 
86
+ # Returns a Hash with present tier hashes and optional
87
+ # +:long_context_threshold+, omitting absent entries.
41
88
  def to_h
42
89
  result = {}
43
90
  result[:standard] = standard.to_h if standard
44
91
  result[:batch] = batch.to_h if batch
92
+ result[:long_context] = long_context.to_h if long_context
93
+ result[:long_context_threshold] = long_context_threshold if long_context_threshold
45
94
  result
46
95
  end
47
96
 
97
+ # Builds long-context rates and threshold from a models.dev-style cost
98
+ # Hash (as stored on Model#metadata under +:cost+). Returns
99
+ # <tt>[rates_hash, threshold]</tt>, or <tt>[nil, nil]</tt> when the
100
+ # cost has no context tier.
101
+ def self.long_context_from_cost(cost) # :nodoc:
102
+ cost = RubyLLM::Support::Utils.deep_symbolize_keys(cost || {})
103
+ return [nil, nil] if cost.empty?
104
+
105
+ entry, threshold = context_cost(cost)
106
+ return [nil, nil] unless entry
107
+
108
+ rates = {
109
+ input_per_million: entry[:input],
110
+ output_per_million: entry[:output],
111
+ cache_read_input_per_million: entry[:cache_read],
112
+ cache_write_input_per_million: entry[:cache_write],
113
+ reasoning_output_per_million: entry[:reasoning]
114
+ }.compact
115
+ rates.empty? ? [nil, nil] : [rates, threshold]
116
+ end
117
+
118
+ def self.context_cost(cost) # :nodoc:
119
+ context_tier = Array(cost[:tiers]).find do |entry|
120
+ entry.is_a?(Hash) && entry.dig(:tier, :type).to_s == 'context'
121
+ end
122
+ if context_tier
123
+ [context_tier, Integer(context_tier.dig(:tier, :size), exception: false)]
124
+ elsif cost[:context_over_200k].is_a?(Hash)
125
+ [cost[:context_over_200k], 200_000]
126
+ end
127
+ end
128
+
129
+ private_class_method :context_cost
130
+
48
131
  private
49
132
 
133
+ def tier_from(tier_data)
134
+ return nil if empty_tier?(tier_data)
135
+
136
+ PricingTier.new(tier_data || {})
137
+ end
138
+
50
139
  def empty_tier?(tier_data)
51
140
  return true unless tier_data
52
141
 
53
- tier_data.values.all? { |v| v.nil? || v == 0.0 }
142
+ tier_data.values.all?(&:nil?)
54
143
  end
55
144
  end
56
145
  end
@@ -1,32 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # A dynamic class for storing non-zero pricing values with flexible attribute access
4
+ class Model
5
+ # A PricingTier holds the prices a model charges within one billing tier,
6
+ # such as standard, batch, or long_context. Each price is in USD per one
7
+ # million tokens. Prices missing from the registry read as +nil+. A zero
8
+ # price means the usage is free.
9
+ #
10
+ # Instances come from PricingCategory#standard, #batch, and #long_context:
11
+ #
12
+ # model = RubyLLM.models.find "claude-sonnet-5"
13
+ # tier = model.pricing.text_tokens.standard
14
+ # tier.input_per_million # => 3
15
+ # tier.output_per_million # => 15
16
+ #
6
17
  class PricingTier
7
- def initialize(data = {})
18
+ # The names of the price readers a tier responds to.
19
+ ATTRIBUTES = %i[
20
+ input_per_million
21
+ output_per_million
22
+ cache_read_input_per_million
23
+ cache_write_input_per_million
24
+ reasoning_output_per_million
25
+ ].freeze
26
+
27
+ def initialize(data = {}) # :nodoc:
8
28
  @values = {}
9
29
 
10
30
  data.each do |key, value|
11
- @values[key.to_sym] = value if value && value != 0.0
12
- end
13
- end
31
+ next if value.nil?
14
32
 
15
- def method_missing(method, *args)
16
- if method.to_s.end_with?('=')
17
- key = method.to_s.chomp('=').to_sym
18
- @values[key] = args.first if args.first && args.first != 0.0
19
- elsif @values.key?(method)
20
- @values[method]
33
+ @values[key.to_sym] = value
21
34
  end
22
35
  end
23
36
 
24
- def respond_to_missing?(method, include_private = false)
25
- method.to_s.end_with?('=') || @values.key?(method.to_sym) || super
37
+ ##
38
+ # :method: input_per_million
39
+ #
40
+ # Returns the USD price per million input tokens, or +nil+ if the
41
+ # price is missing.
42
+
43
+ ##
44
+ # :method: output_per_million
45
+ #
46
+ # Returns the USD price per million output tokens, or +nil+ if the
47
+ # price is missing.
48
+
49
+ ##
50
+ # :method: cache_read_input_per_million
51
+ #
52
+ # Returns the USD price per million input tokens read from the
53
+ # provider's prompt cache, or +nil+ if the price is missing.
54
+
55
+ ##
56
+ # :method: cache_write_input_per_million
57
+ #
58
+ # Returns the USD price per million input tokens written to the
59
+ # provider's prompt cache, or +nil+ if the price is missing.
60
+
61
+ ##
62
+ # :method: reasoning_output_per_million
63
+ #
64
+ # Returns the USD price per million reasoning output tokens, or +nil+
65
+ # if the price is missing.
66
+
67
+ ATTRIBUTES.each do |attribute|
68
+ define_method(attribute) do
69
+ @values[attribute]
70
+ end
26
71
  end
27
72
 
73
+ # Returns a new Hash of the tier's prices with Symbol keys. Missing
74
+ # prices are omitted.
75
+ #
76
+ # tier.to_h
77
+ # # => {input_per_million: 3, output_per_million: 15, ...}
78
+ #
28
79
  def to_h
29
- @values
80
+ @values.dup
30
81
  end
31
82
  end
32
83
  end
@@ -1,7 +1,249 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Model-related classes for working with LLM models
5
- module Model
4
+ # A Model describes one entry in the model registry: the model's identity,
5
+ # capabilities, modalities, pricing, and provider metadata. Instances come
6
+ # from the registry through RubyLLM.models and from Chat#model.
7
+ #
8
+ # model = RubyLLM.models.find('gpt-5.6')
9
+ # model.name # => "GPT-5.6"
10
+ # model.provider # => "openai"
11
+ # model.context_window # => 1050000
12
+ # model.supports?(:vision) # => true
13
+ #
14
+ class Model
15
+ include Support::Inspectable
16
+
17
+ # The provider's identifier for the model, e.g. <tt>"gpt-5.6"</tt>.
18
+ attr_reader :id
19
+
20
+ # The human-readable model name, e.g. <tt>"GPT-5.6"</tt>.
21
+ attr_reader :name
22
+
23
+ # The provider slug as a String, e.g. <tt>"openai"</tt>.
24
+ attr_reader :provider
25
+
26
+ # The model family as a String, e.g. <tt>"gpt"</tt>, or +nil+.
27
+ attr_reader :family
28
+
29
+ # The model's release timestamp as a UTC Time, or +nil+.
30
+ attr_reader :created_at
31
+
32
+ # The maximum number of input tokens the model accepts, or +nil+.
33
+ attr_reader :context_window
34
+
35
+ # The maximum number of tokens the model can generate, or +nil+.
36
+ attr_reader :max_output_tokens
37
+
38
+ # The model's training data cutoff as a Date, or +nil+.
39
+ attr_reader :knowledge_cutoff
40
+
41
+ # The supported input and output modalities as a Model::Modalities object.
42
+ #
43
+ # model.modalities.input # => ["text", "image", "pdf"]
44
+ # model.modalities.output # => ["text"]
45
+ #
46
+ attr_reader :modalities
47
+
48
+ # The model's capability names as an array of Strings,
49
+ # e.g. <tt>["function_calling", "streaming"]</tt>.
50
+ attr_reader :capabilities
51
+
52
+ # The model's pricing as a Model::Pricing object, in USD per million tokens.
53
+ attr_reader :pricing
54
+
55
+ # Provider-specific metadata as a Hash.
56
+ attr_reader :metadata
57
+
58
+ # The time the configured provider stopped listing the model, as a UTC
59
+ # Time, or +nil+ while the provider still lists it. Only a store that keeps
60
+ # unlisted entries, such as the Rails model table, sets it.
61
+ attr_reader :unlisted_at
62
+
63
+ attr_reader :reasoning_options # :nodoc:
64
+
65
+ def self.default(model_id, provider) # :nodoc:
66
+ new(
67
+ id: model_id,
68
+ name: model_id.tr('-', ' ').capitalize,
69
+ provider: provider,
70
+ capabilities: %w[function_calling streaming vision structured_output],
71
+ modalities: { input: %w[text image], output: %w[text] },
72
+ metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
73
+ )
74
+ end
75
+
76
+ def initialize(data) # :nodoc:
77
+ @id = data[:id]
78
+ @name = data[:name]
79
+ @provider = data[:provider]
80
+ @family = data[:family]
81
+ @created_at = Support::Utils.to_time(data[:created_at])&.utc
82
+ @context_window = data[:context_window]
83
+ @max_output_tokens = data[:max_output_tokens]
84
+ @knowledge_cutoff = Support::Utils.to_date(data[:knowledge_cutoff])
85
+ @modalities = Modalities.new(data[:modalities] || {})
86
+ @capabilities = data[:capabilities] || []
87
+ @metadata = data[:metadata]&.dup || {}
88
+ @unlisted_at = Support::Utils.to_time(data[:unlisted_at])&.utc
89
+ @pricing = Pricing.new(pricing_data_with_long_context(data[:pricing] || {}))
90
+ @reasoning_options = normalize_reasoning_options(reasoning_options_from(data))
91
+ store_reasoning_options_metadata
92
+ end
93
+
94
+ # Returns whether #capabilities includes +capability+, given as a String
95
+ # or Symbol.
96
+ #
97
+ # model.supports?(:function_calling) # => true
98
+ #
99
+ def supports?(capability)
100
+ capabilities.include?(capability.to_s)
101
+ end
102
+
103
+ # Returns whether the configured provider has stopped listing the model.
104
+ # An unlisted model is kept only because application records still
105
+ # reference it, and it may already be failing at the provider.
106
+ #
107
+ # model.unlisted? # => false
108
+ #
109
+ def unlisted?
110
+ !unlisted_at.nil?
111
+ end
112
+
113
+ # Returns the provider display name and model name combined,
114
+ # e.g. <tt>"OpenAI - GPT-5.6"</tt>.
115
+ def label
116
+ provider_name = provider_class&.display_name || provider
117
+ "#{provider_name} - #{name}"
118
+ end
119
+
120
+ def reasoning_option(type) # :nodoc:
121
+ reasoning_options.find { |option| option[:type] == type.to_s }
122
+ end
123
+
124
+ def reasoning_option_values(type) # :nodoc:
125
+ Array(reasoning_option(type)&.fetch(:values, nil))
126
+ end
127
+
128
+ # The standard text-token price columns, keyed by the symbol #price
129
+ # accepts.
130
+ PRICES = {
131
+ input: :input,
132
+ output: :output,
133
+ cache_read: :cache_read_input,
134
+ cache_write: :cache_write_input
135
+ }.freeze
136
+ private_constant :PRICES
137
+
138
+ # Returns the standard text-token price for +kind+ in USD per million
139
+ # tokens, or +nil+ if the registry has no such price. Valid kinds are
140
+ # +:input+, +:output+, +:cache_read+, and +:cache_write+.
141
+ #
142
+ # model.price(:input) # => 2.5
143
+ # model.price(:output) # => 10.0
144
+ #
145
+ def price(kind)
146
+ column = PRICES.fetch(kind) do
147
+ raise ArgumentError, "Unknown price kind: #{kind.inspect}. Valid kinds: #{PRICES.keys.join(', ')}"
148
+ end
149
+ pricing.text_tokens.public_send(column)
150
+ end
151
+
152
+ # Builds a Cost for +tokens+ (a Tokens object, or anything responding
153
+ # to +tokens+) using this model's pricing.
154
+ #
155
+ # cost = model.cost_for(response.tokens)
156
+ # puts cost.total
157
+ #
158
+ # Pass <tt>tier: :batch</tt> to use the model's explicit batch rates.
159
+ #
160
+ def cost_for(tokens, tier: :standard)
161
+ tokens = tokens.tokens if tokens.respond_to?(:tokens)
162
+
163
+ Cost.new(tokens:, model: self, tier:)
164
+ end
165
+
166
+ # Returns the Provider class registered for this model's provider slug,
167
+ # or +nil+ if no such provider is registered.
168
+ def provider_class
169
+ RubyLLM::Provider.resolve provider
170
+ end
171
+
172
+ # Returns the model's primary function, inferred from its output
173
+ # modalities: +:chat+, +:embedding+, +:moderation+, +:image+, +:audio+,
174
+ # +:video+, or +:rerank+.
175
+ def type
176
+ output = modalities.output
177
+ return :embedding if output.include?('embeddings')
178
+ return :moderation if output.include?('moderation')
179
+ return :image if output.include?('image')
180
+ return :audio if output.include?('audio')
181
+ return :video if output.include?('video')
182
+ return :rerank if output.include?('rerank')
183
+
184
+ :chat
185
+ end
186
+
187
+ def to_h # :nodoc:
188
+ data = {
189
+ id: id,
190
+ name: name,
191
+ provider: provider,
192
+ family: family,
193
+ created_at: created_at,
194
+ context_window: context_window,
195
+ max_output_tokens: max_output_tokens,
196
+ knowledge_cutoff: knowledge_cutoff,
197
+ modalities: modalities.to_h,
198
+ capabilities: capabilities,
199
+ pricing: pricing.to_h,
200
+ metadata: metadata
201
+ }
202
+ # A JSON registry drops unlisted models, so the key stays out of it.
203
+ data[:unlisted_at] = unlisted_at if unlisted_at
204
+ data
205
+ end
206
+
207
+ private
208
+
209
+ def pricing_data_with_long_context(pricing)
210
+ pricing = RubyLLM::Support::Utils.deep_symbolize_keys(RubyLLM::Support::Utils.deep_dup(pricing))
211
+ text = pricing[:text_tokens] || {}
212
+ return pricing if text[:long_context]
213
+
214
+ rates, threshold = PricingCategory.long_context_from_cost(metadata[:cost] || metadata['cost'])
215
+ return pricing unless rates
216
+
217
+ pricing[:text_tokens] = text.merge(long_context: rates)
218
+ pricing[:text_tokens][:long_context_threshold] = threshold if threshold
219
+ pricing
220
+ end
221
+
222
+ def reasoning_options_from(data)
223
+ data[:reasoning_options] || metadata[:reasoning_options] || metadata['reasoning_options']
224
+ end
225
+
226
+ def store_reasoning_options_metadata
227
+ return unless reasoning_options.any?
228
+
229
+ metadata.delete('reasoning_options')
230
+ metadata[:reasoning_options] = reasoning_options
231
+ end
232
+
233
+ def normalize_reasoning_options(options)
234
+ Array(options).filter_map do |option|
235
+ next unless option.is_a?(Hash)
236
+
237
+ normalized = option.to_h.transform_keys(&:to_sym)
238
+ normalized[:type] = normalized[:type].to_s if normalized[:type]
239
+ normalized[:values] = Array(normalized[:values]).map(&:to_s) if normalized.key?(:values)
240
+ normalized[:default] = normalized[:default].to_s if normalized[:default].is_a?(Symbol)
241
+ normalized
242
+ end
243
+ end
244
+
245
+ def inspect_attributes # :nodoc:
246
+ { id: id, provider: provider, name: name }
247
+ end
6
248
  end
7
249
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLLM
6
+ class Models
7
+ class Aliases # :nodoc:
8
+ class << self
9
+ def resolve(model_id, provider = nil)
10
+ return model_id unless aliases[model_id]
11
+
12
+ if provider
13
+ aliases[model_id][provider.to_s] || model_id
14
+ else
15
+ aliases[model_id].values.first || model_id
16
+ end
17
+ end
18
+
19
+ def aliases
20
+ @aliases ||= load_aliases
21
+ end
22
+
23
+ def aliases_file
24
+ File.expand_path('../aliases.json', __dir__)
25
+ end
26
+
27
+ def load_aliases
28
+ if File.exist?(aliases_file)
29
+ JSON.parse(File.read(aliases_file))
30
+ else
31
+ {}
32
+ end
33
+ end
34
+
35
+ def reload
36
+ @aliases = load_aliases
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end