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
@@ -1,43 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # Stores non-zero pricing values for a single pricing tier.
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
18
+ # The names of the price readers a tier responds to.
7
19
  ATTRIBUTES = %i[
8
20
  input_per_million
9
21
  output_per_million
10
22
  cache_read_input_per_million
11
23
  cache_write_input_per_million
12
- cached_input_per_million
13
- cache_creation_input_per_million
14
24
  reasoning_output_per_million
15
25
  ].freeze
16
26
 
17
- def initialize(data = {})
27
+ def initialize(data = {}) # :nodoc:
18
28
  @values = {}
19
29
 
20
30
  data.each do |key, value|
21
- @values[key.to_sym] = value if value && value != 0.0
31
+ next if value.nil?
32
+
33
+ @values[key.to_sym] = value
22
34
  end
23
35
  end
24
36
 
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
+
25
67
  ATTRIBUTES.each do |attribute|
26
68
  define_method(attribute) do
27
69
  @values[attribute]
28
70
  end
29
-
30
- define_method("#{attribute}=") do |value|
31
- @values[attribute] = value if value && value != 0.0
32
- end
33
- end
34
-
35
- def [](key)
36
- @values[key.to_sym]
37
71
  end
38
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
+ #
39
79
  def to_h
40
- @values
80
+ @values.dup
41
81
  end
42
82
  end
43
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
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'rbconfig'
6
+ require 'tempfile'
7
+
8
+ module RubyLLM
9
+ class Models
10
+ module Registry # :nodoc: all
11
+ PUBLISHED_URL = 'https://rubyllm.com/models.json'
12
+
13
+ class FileStore
14
+ attr_reader :path
15
+
16
+ def initialize(path)
17
+ @path = path.respond_to?(:path) ? path.path : path.to_s
18
+ raise ModelRegistryError, 'A model registry file path is required' if @path.empty?
19
+ end
20
+
21
+ def read
22
+ Registry.read(path)
23
+ end
24
+
25
+ def etag
26
+ return unless File.file?(path)
27
+
28
+ value = File.read(etag_path).strip
29
+ value unless value.empty?
30
+ rescue Errno::ENOENT
31
+ nil
32
+ rescue SystemCallError => e
33
+ raise ModelRegistryError, "Could not read the model registry ETag from #{etag_path}: #{e.message}"
34
+ end
35
+
36
+ def write(models, etag: nil)
37
+ FileUtils.mkdir_p(File.dirname(path))
38
+ atomic_write(path, Registry.pretty_json(models))
39
+ write_etag(etag)
40
+ models
41
+ end
42
+
43
+ private
44
+
45
+ def etag_path
46
+ "#{path}.etag"
47
+ end
48
+
49
+ def write_etag(etag)
50
+ if etag
51
+ atomic_write(etag_path, "#{etag}\n")
52
+ else
53
+ FileUtils.rm_f(etag_path)
54
+ end
55
+ end
56
+
57
+ def atomic_write(destination, contents)
58
+ directory = File.dirname(destination)
59
+ basename = File.basename(destination)
60
+ Tempfile.create([basename, '.tmp'], directory) do |temporary|
61
+ temporary.binmode
62
+ temporary.write(contents)
63
+ temporary.flush
64
+ temporary.fsync
65
+ temporary.chmod(readable_mode(destination))
66
+ replace_file(temporary.path, destination)
67
+ end
68
+ end
69
+
70
+ # Tempfile is 0600 and the rename carries that mode onto the registry,
71
+ # which then reads as unreadable to any other user on the machine.
72
+ def readable_mode(destination)
73
+ return File.stat(destination).mode & 0o7777 if File.exist?(destination)
74
+
75
+ 0o666 & ~File.umask
76
+ end
77
+
78
+ def replace_file(source, destination)
79
+ File.rename(source, destination)
80
+ rescue Errno::EACCES, Errno::EEXIST
81
+ FileUtils.rm_f(destination)
82
+ File.rename(source, destination)
83
+ end
84
+ end
85
+
86
+ class PublishedSource
87
+ Result = Struct.new(:models, :etag, :not_modified)
88
+
89
+ attr_reader :url
90
+
91
+ def initialize(url = PUBLISHED_URL)
92
+ @url = url
93
+ end
94
+
95
+ def fetch(etag: nil)
96
+ connection = RubyLLM::Transport::Connection.basic do |faraday|
97
+ faraday.response :json, parser_options: { symbolize_names: true }
98
+ end
99
+ response = connection.get(url) do |request|
100
+ request.headers['If-None-Match'] = etag if etag
101
+ end
102
+
103
+ return Result.new(nil, response.headers['etag'] || etag, true) if response.status == 304
104
+
105
+ models = Registry.models_from_data(response.body, source: url)
106
+ raise ModelRegistryError, 'Published model registry is empty' if models.empty?
107
+
108
+ Result.new(models, response.headers['etag'], false)
109
+ rescue ModelRegistryError
110
+ raise
111
+ rescue StandardError => e
112
+ raise ModelRegistryError, "Could not refresh the model registry from #{url}: #{e.message}"
113
+ end
114
+ end
115
+
116
+ module_function
117
+
118
+ def read(file)
119
+ data = JSON.parse(File.read(file), symbolize_names: true)
120
+ models_from_data(data, source: file)
121
+ rescue Errno::ENOENT
122
+ nil
123
+ rescue JSON::ParserError => e
124
+ raise ModelRegistryError, "Invalid model registry JSON in #{file}: #{e.message}"
125
+ rescue SystemCallError => e
126
+ raise ModelRegistryError, "Could not read the model registry from #{file}: #{e.message}"
127
+ end
128
+
129
+ def models_from_data(data, source: nil)
130
+ unless data.is_a?(Array)
131
+ location = source ? " in #{source}" : ''
132
+ raise ModelRegistryError, "Model registry#{location} must be a JSON array"
133
+ end
134
+
135
+ data.map { |model| model.is_a?(RubyLLM::Model) ? model : RubyLLM::Model.new(model) }
136
+ end
137
+
138
+ def pretty_json(models)
139
+ "#{JSON.pretty_generate(Array(models).map(&:to_h))}\n"
140
+ end
141
+
142
+ def cache_path
143
+ home = Dir.home
144
+ host_os = RbConfig::CONFIG['host_os']
145
+
146
+ directory = if host_os.match?(/darwin/i)
147
+ File.join(home, 'Library', 'Caches', 'RubyLLM')
148
+ elsif host_os.match?(/mswin|mingw|cygwin/i)
149
+ local_app_data = ENV.fetch('LOCALAPPDATA', nil)
150
+ local_app_data = File.join(home, 'AppData', 'Local') if local_app_data.to_s.empty?
151
+ File.join(local_app_data, 'RubyLLM', 'Cache')
152
+ else
153
+ xdg_cache = ENV.fetch('XDG_CACHE_HOME', nil)
154
+ xdg_cache = File.join(home, '.cache') if xdg_cache.to_s.empty?
155
+ File.join(xdg_cache, 'ruby_llm')
156
+ end
157
+
158
+ File.join(directory, 'models.json')
159
+ rescue ArgumentError
160
+ # Dir.home raises when the home directory cannot be determined.
161
+ nil
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schematist'
4
+
5
+ module RubyLLM
6
+ class Models
7
+ # Validation schema for entries in the model registry. The registry
8
+ # builder validates refreshed registries against it before publishing,
9
+ # and the spec suite validates the bundled models.json.
10
+ class Schema < Schematist::Schema # :nodoc:
11
+ CAPABILITIES = %w[
12
+ streaming function_calling tool_choice parallel_tool_calls
13
+ structured_output predicted_outputs
14
+ distillation fine_tuning batch realtime image_generation
15
+ speech_generation transcription translation citations reasoning
16
+ caching moderation json_mode vision video ocr
17
+ ].freeze
18
+
19
+ description 'A model entry in the RubyLLM model registry'
20
+
21
+ string :id, description: 'Unique identifier for the model'
22
+ string :name, description: 'Display name of the model'
23
+ string :provider, description: 'Provider of the model (e.g., openai, anthropic, mistral)'
24
+
25
+ any_of :family, required: false, description: 'Model family (e.g., gpt-4, claude-3)' do
26
+ string
27
+ null
28
+ end
29
+
30
+ # Ruby Time#to_s strings today, not ISO8601; normalize at write time
31
+ # before tightening this to format: 'date-time'.
32
+ any_of :created_at, required: false, description: 'Creation date of the model' do
33
+ string
34
+ null
35
+ end
36
+
37
+ any_of :context_window, description: 'Maximum context window size' do
38
+ integer minimum: 0
39
+ null
40
+ end
41
+
42
+ any_of :max_output_tokens, description: 'Maximum output tokens' do
43
+ integer minimum: 0
44
+ null
45
+ end
46
+
47
+ any_of :knowledge_cutoff, required: false, description: 'Knowledge cutoff date' do
48
+ string format: 'date'
49
+ null
50
+ end
51
+
52
+ object :modalities, required: false do
53
+ array :input, description: 'Supported input modalities' do
54
+ string enum: RubyLLM::Models::MODELS_DEV_INPUT_MODALITIES
55
+ end
56
+ array :output, description: 'Supported output modalities' do
57
+ string enum: RubyLLM::Models::MODELS_DEV_OUTPUT_MODALITIES
58
+ end
59
+ end
60
+
61
+ array :capabilities, required: false, description: 'Model capabilities' do
62
+ string enum: CAPABILITIES
63
+ end
64
+
65
+ object :pricing, required: false, description: 'Pricing information for the model' do
66
+ RubyLLM::Model::Pricing::CATEGORIES.each do |category|
67
+ object category, required: false do
68
+ RubyLLM::Model::PricingCategory::TIERS.each do |tier|
69
+ object tier, required: false do
70
+ RubyLLM::Model::PricingTier::ATTRIBUTES.each do |attribute|
71
+ number attribute, minimum: 0, required: false
72
+ end
73
+ end
74
+ end
75
+ integer :long_context_threshold,
76
+ minimum: 0,
77
+ required: false,
78
+ description: 'Prompt size above which long_context rates apply'
79
+ end
80
+ end
81
+ end
82
+
83
+ object :metadata, required: false, description: 'Additional metadata about the model' do
84
+ additional_properties true
85
+ end
86
+
87
+ any_of :unlisted_at, required: false, description: 'When the provider stopped listing the model' do
88
+ string
89
+ null
90
+ end
91
+
92
+ # The Draft 2020-12 JSON Schema document for one model entry. Wrap it in an
93
+ # array schema to validate a whole registry.
94
+ def self.json_schema
95
+ new.to_json_schema
96
+ end
97
+ end
98
+ end
99
+ end