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
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Support # :nodoc:
5
+ module Utils # :nodoc:
6
+ UNDERSCORE_BOUNDARY = /(?<=[a-z\d])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/
7
+
8
+ # Keys a JSON Schema document carries for its own sake: $schema names the
9
+ # dialect and title names the document, neither of which describes the shape
10
+ # a provider is being asked to produce.
11
+ SCHEMA_METADATA_KEYS = %w[$schema title].freeze
12
+
13
+ module_function
14
+
15
+ def hash_get(hash, key)
16
+ symbol_key = key.to_sym
17
+ hash.key?(symbol_key) ? hash[symbol_key] : hash[key.to_s]
18
+ end
19
+
20
+ def strip_schema_metadata(schema)
21
+ return schema unless schema.is_a?(Hash)
22
+
23
+ schema.reject { |key, _| SCHEMA_METADATA_KEYS.include?(key.to_s) }
24
+ end
25
+
26
+ # Acronym-aware underscoring: 'HTTPProxyTool' -> 'http_proxy_tool'.
27
+ def underscore(name)
28
+ name.gsub(UNDERSCORE_BOUNDARY, '_').downcase
29
+ end
30
+
31
+ def to_safe_array(item)
32
+ case item
33
+ when Array then item
34
+ when Hash then [item]
35
+ else item.respond_to?(:read) ? [item] : Array(item)
36
+ end
37
+ end
38
+
39
+ def to_time(value)
40
+ return unless value
41
+
42
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
43
+ end
44
+
45
+ def to_date(value)
46
+ return unless value
47
+
48
+ value.is_a?(Date) ? value : Date.parse(value.to_s)
49
+ end
50
+
51
+ def safe_constantize(name)
52
+ parts = name.to_s.split('::').reject(&:empty?)
53
+ return if parts.empty?
54
+
55
+ namespace = Object
56
+ until parts.empty?
57
+ const_name = parts.shift
58
+ return unless namespace.const_defined?(const_name, false)
59
+
60
+ namespace = namespace.const_get(const_name, false)
61
+ end
62
+ namespace
63
+ rescue NameError
64
+ nil
65
+ end
66
+
67
+ def parse_iso_date_prefix(value)
68
+ return value if value.is_a?(Date)
69
+
70
+ date = value.to_s.strip
71
+ return if date.empty?
72
+
73
+ case date
74
+ when /\A\d{4}-\d{2}-\d{2}\z/
75
+ Date.iso8601(date)
76
+ when /\A\d{4}-\d{2}\z/
77
+ Date.iso8601("#{date}-01")
78
+ when /\A\d{4}\z/
79
+ Date.iso8601("#{date}-01-01")
80
+ end
81
+ rescue ArgumentError
82
+ nil
83
+ end
84
+
85
+ def iso_date_prefix_to_utc_midnight_string(value)
86
+ date = parse_iso_date_prefix(value)
87
+ "#{date.strftime('%Y-%m-%d')} 00:00:00 UTC" if date
88
+ end
89
+
90
+ def deep_merge(original, overrides)
91
+ original.merge(overrides) do |_key, original_value, overrides_value|
92
+ if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
93
+ deep_merge(original_value, overrides_value)
94
+ else
95
+ overrides_value
96
+ end
97
+ end
98
+ end
99
+
100
+ def deep_dup(value)
101
+ case value
102
+ when Hash
103
+ value.each_with_object({}) do |(key, val), duped|
104
+ duped[deep_dup(key)] = deep_dup(val)
105
+ end
106
+ when Array
107
+ value.map { |item| deep_dup(item) }
108
+ else
109
+ begin
110
+ value.dup
111
+ rescue TypeError
112
+ value
113
+ end
114
+ end
115
+ end
116
+
117
+ def deep_stringify_keys(value)
118
+ case value
119
+ when Hash
120
+ value.each_with_object({}) do |(key, val), result|
121
+ result[key.to_s] = deep_stringify_keys(val)
122
+ end
123
+ when Array
124
+ value.map { |item| deep_stringify_keys(item) }
125
+ when Symbol
126
+ value.to_s
127
+ else
128
+ value
129
+ end
130
+ end
131
+
132
+ def deep_symbolize_keys(value)
133
+ case value
134
+ when Hash
135
+ value.each_with_object({}) do |(key, val), result|
136
+ symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
137
+ result[symbolized_key] = deep_symbolize_keys(val)
138
+ end
139
+ when Array
140
+ value.map { |item| deep_symbolize_keys(item) }
141
+ else
142
+ value
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -1,48 +1,155 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Represents provider thinking output.
4
+ # A Thinking holds the reasoning output a provider returned alongside a
5
+ # response. Instances appear on Message#thinking, and on Chunk#thinking
6
+ # while streaming, when the model exposes its thinking.
7
+ #
8
+ # chat = RubyLLM.chat(model: 'claude-opus-5').with_thinking(display: :summarized)
9
+ # response = chat.ask "What is 15 * 23?"
10
+ # response.thinking&.text
11
+ # response.thinking&.signature
12
+ #
5
13
  class Thinking
6
- attr_reader :text, :signature
14
+ include Support::Inspectable
7
15
 
8
- def initialize(text: nil, signature: nil)
16
+ # The reasoning text the provider returned, or +nil+.
17
+ attr_reader :text
18
+
19
+ # The provider's opaque signature or encrypted reasoning payload for the
20
+ # thinking block, or +nil+. Shown as redacted in pretty-print output.
21
+ attr_reader :signature
22
+
23
+ def initialize(text: nil, signature: nil) # :nodoc:
9
24
  @text = text
10
25
  @signature = signature
11
26
  end
12
27
 
13
- def self.build(text: nil, signature: nil)
14
- text = nil if text.is_a?(String) && text.empty?
28
+ def self.build(text: nil, signature: nil) # :nodoc:
15
29
  signature = nil if signature.is_a?(String) && signature.empty?
30
+ text = nil if text.is_a?(String) && text.empty? && signature.nil?
16
31
 
17
32
  return nil if text.nil? && signature.nil?
18
33
 
19
34
  new(text: text, signature: signature)
20
35
  end
21
36
 
22
- def pretty_print(printer)
23
- printer.object_group(self) do
24
- printer.breakable
25
- printer.text 'text='
26
- printer.pp text
27
- printer.comma_breakable
28
- printer.text 'signature='
29
- printer.pp(signature ? '[REDACTED]' : nil)
30
- end
37
+ def inspect_attributes # :nodoc:
38
+ { text: text, signature: signature ? '[REDACTED]' : nil }
31
39
  end
32
40
  end
33
41
 
34
42
  class Thinking
35
- # Normalized config for thinking across providers.
36
- class Config
37
- attr_reader :effort, :budget
43
+ class Controls # :nodoc: all
44
+ PREFERRED_EFFORTS = %w[medium low minimal high xhigh max].freeze
45
+
46
+ def initialize(model)
47
+ @model = model
48
+ end
49
+
50
+ def enable
51
+ effort = default_effort
52
+ return { effort: effort } if effort
53
+
54
+ budget = explicit_budget
55
+ return { budget: budget } if budget
56
+ return { enabled: true } if model.reasoning_option(:toggle)
57
+
58
+ effort = preferred_effort
59
+ return { effort: effort } if effort
60
+
61
+ budget = minimum_budget
62
+ return { budget: budget } if budget
63
+
64
+ :already_on if reasoning_model? && model.reasoning_options.empty?
65
+ end
66
+
67
+ def disable
68
+ return { effort: :none } if model.reasoning_option_values(:effort).include?('none')
38
69
 
39
- def initialize(effort: nil, budget: nil)
40
- @effort = effort.is_a?(Symbol) ? effort.to_s : effort
70
+ budget = model.reasoning_option(:budget_tokens)
71
+ return { budget: 0 } if budget && budget[:min].is_a?(Numeric) && budget[:min] <= 0
72
+
73
+ { enabled: false } if model.reasoning_option(:toggle) || budget
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :model
79
+
80
+ def default_effort
81
+ option = model.reasoning_option(:effort)
82
+ return unless option&.key?(:default)
83
+
84
+ default = option[:default].to_s
85
+ default.to_sym unless default == 'none'
86
+ end
87
+
88
+ def preferred_effort
89
+ PREFERRED_EFFORTS.find { |effort| model.reasoning_option_values(:effort).include?(effort) }&.to_sym
90
+ end
91
+
92
+ def explicit_budget
93
+ default = model.reasoning_option(:budget_tokens)&.fetch(:default, nil)
94
+ default if default.is_a?(Numeric) && default.positive?
95
+ end
96
+
97
+ def minimum_budget
98
+ minimum = model.reasoning_option(:budget_tokens)&.fetch(:min, nil)
99
+ return unless minimum.is_a?(Numeric)
100
+
101
+ [minimum, 1].max
102
+ end
103
+
104
+ def reasoning_model?
105
+ model.supports?(:reasoning) || model.metadata[:reasoning] || model.metadata['reasoning']
106
+ end
107
+ end
108
+
109
+ class Config # :nodoc: all
110
+ attr_reader :effort, :budget, :display, :enabled
111
+
112
+ def self.default
113
+ new(intent: :enable)
114
+ end
115
+
116
+ def self.disabled
117
+ new(intent: :disable)
118
+ end
119
+
120
+ def initialize(effort: nil, budget: nil, display: nil, enabled: nil, intent: nil)
121
+ @effort = effort&.to_sym
41
122
  @budget = budget
123
+ @display = display&.to_sym
124
+ @enabled = enabled
125
+ @intent = intent
42
126
  end
43
127
 
44
128
  def enabled?
45
- !effort.nil? || !budget.nil?
129
+ !@intent.nil? || !enabled.nil? || !effort.nil? || !budget.nil? || !display.nil?
130
+ end
131
+
132
+ def disabled?
133
+ enabled == false || effort == :none
134
+ end
135
+
136
+ def resolve(model)
137
+ return self if @intent.nil?
138
+
139
+ options = Controls.new(model).public_send(@intent)
140
+ return nil if options == :already_on
141
+ raise ArgumentError, resolution_error(model) unless options
142
+
143
+ self.class.new(**options)
144
+ end
145
+
146
+ private
147
+
148
+ def resolution_error(model)
149
+ message = "RubyLLM does not know how to #{@intent} thinking for #{model.provider}/#{model.id}."
150
+ return "#{message} Pass effort:, budget:, or display:." if @intent == :enable
151
+
152
+ "#{message} The model registry has no off control."
46
153
  end
47
154
  end
48
155
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # The token IDs produced by a model's tokenizer for plain text.
5
+ # These exclude chat formatting, tools, and media, and do not measure
6
+ # billable generation usage.
7
+ class Tokenization
8
+ include Support::Inspectable
9
+
10
+ # The integer token IDs in text order.
11
+ attr_reader :ids
12
+
13
+ # The id of the model whose tokenizer was used.
14
+ attr_reader :model
15
+
16
+ # The provider's unmodified response, including token strings when available.
17
+ attr_reader :raw
18
+
19
+ # Tokenizes +text+ and returns a Tokenization. Most code calls
20
+ # RubyLLM.tokenize. +model:+ defaults to the configured chat model;
21
+ # +provider:+ selects its provider. +assume_model_exists:+ skips
22
+ # registry lookup, and +context:+ supplies an isolated configuration.
23
+ #
24
+ # result = RubyLLM.tokenize("Hello Ruby", model: "grok-4.3", provider: :xai)
25
+ # result.ids
26
+ # result.count
27
+ #
28
+ # Raises ArgumentError for non-string input and RubyLLM::Error when
29
+ # the provider does not expose a tokenizer.
30
+ def self.tokenize(text, model: nil, provider: nil, assume_model_exists: false, context: nil)
31
+ raise ArgumentError, 'text must be a String' unless text.is_a?(String)
32
+
33
+ config = context&.config || RubyLLM.config
34
+ model ||= config.default_model
35
+ model, provider_instance = Models.resolve(model, provider:, assume_model_exists:, config:)
36
+ payload = { model: model.id, provider: provider_instance.slug }
37
+ RubyLLM.instrument('tokenization.ruby_llm', payload, config:) do |event|
38
+ result = provider_instance.tokenize(text, model:)
39
+ event[:result] = result
40
+ result
41
+ end
42
+ end
43
+
44
+ def initialize(ids:, model:, raw: nil) # :nodoc:
45
+ @ids = ids.dup.freeze
46
+ @model = model
47
+ @raw = raw
48
+ end
49
+
50
+ # Returns the number of tokens in the text.
51
+ def count
52
+ ids.length
53
+ end
54
+
55
+ def inspect_attributes # :nodoc:
56
+ { model:, count: }
57
+ end
58
+ end
59
+ end
@@ -1,55 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Represents token usage for a response.
4
+ # A Tokens holds normalized token counts for a provider attempt, a
5
+ # response, or an aggregate. Read them from Chat#tokens, Message#tokens,
6
+ # Chunk#tokens, and individual operation results such as Embedding.
7
+ # Counts the provider did not report are +nil+.
8
+ #
9
+ # response = chat.ask "What is the capital of France?"
10
+ # response.tokens.input # standard input tokens
11
+ # response.tokens.output # billable output tokens
12
+ # response.tokens.cache_read # prompt cache reads
13
+ # response.tokens.cache_write # prompt cache writes
14
+ #
5
15
  class Tokens
6
- attr_reader :input, :output, :cached, :cache_creation, :thinking
16
+ include Support::Inspectable
7
17
 
8
- # rubocop:disable Metrics/ParameterLists
9
- def initialize(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
18
+ # The number of standard (non-cached) input tokens, or +nil+ if the
19
+ # provider did not report it.
20
+ attr_reader :input
21
+
22
+ # The number of billable output tokens, or +nil+ if the provider did
23
+ # not report it. Includes thinking tokens when the provider bills
24
+ # them as output.
25
+ attr_reader :output
26
+
27
+ # The number of tokens served from the provider's prompt cache, or
28
+ # +nil+ if the provider did not report it.
29
+ attr_reader :cache_read
30
+
31
+ # The number of tokens written to the provider's prompt cache, or
32
+ # +nil+ if the provider did not report it.
33
+ attr_reader :cache_write
34
+
35
+ # The number of thinking (reasoning) tokens, or +nil+ if the provider
36
+ # does not report them.
37
+ attr_reader :thinking
38
+
39
+ # The provider's server-tool usage counters as a Hash, such as
40
+ # <tt>{"web_search_requests" => 2}</tt>, or +nil+ if the provider did
41
+ # not report any. Counters are provider-shaped and billed per use, not
42
+ # in tokens.
43
+ attr_reader :server_tool_use
44
+
45
+ # The exact cost of the call in US dollars as reported by the
46
+ # provider, or +nil+ if the provider does not report one. When present,
47
+ # Cost#total returns it instead of a registry-price estimate.
48
+ attr_reader :reported_cost
49
+
50
+ # Creates token counts from the supplied values. Omitted counts remain
51
+ # +nil+; zero means a reported count of zero. Returns a Tokens object
52
+ # even when every count is +nil+.
53
+ #
54
+ # tokens = RubyLLM::Tokens.new(input: 100, output: 20)
55
+ # RubyLLM.models.find("gpt-5.6").cost_for(tokens).total
56
+ #
57
+ # +server_tool_use:+ holds provider tool-usage counters;
58
+ # +reported_cost:+ holds the provider's total price in US dollars.
59
+ def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil, thinking: nil,
60
+ server_tool_use: nil, reported_cost: nil)
10
61
  @input = input
11
62
  @output = output
12
- @cached = cached
13
- @cache_creation = cache_creation
14
- @thinking = thinking || reasoning
63
+ @cache_read = cache_read
64
+ @cache_write = cache_write
65
+ @thinking = thinking
66
+ @server_tool_use = server_tool_use
67
+ @reported_cost = reported_cost
15
68
  end
16
- # rubocop:enable Metrics/ParameterLists
17
69
 
18
- # rubocop:disable Metrics/ParameterLists
19
- def self.build(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
20
- return nil if [input, output, cached, cache_creation, thinking, reasoning].all?(&:nil?)
70
+ # Sums token counts across provider attempts. A bucket remains +nil+ when
71
+ # no attempt reported it.
72
+ def self.aggregate(tokens)
73
+ tokens = Array(tokens).compact
74
+ return new if tokens.empty?
75
+ return tokens.first if tokens.one?
21
76
 
22
- new(
23
- input: input,
24
- output: output,
25
- cached: cached,
26
- cache_creation: cache_creation,
27
- thinking: thinking,
28
- reasoning: reasoning
29
- )
77
+ values = %i[input output cache_read cache_write thinking reported_cost].to_h do |component|
78
+ reported = tokens.filter_map { |usage| usage.public_send(component) }
79
+ [component, reported.empty? ? nil : reported.sum]
80
+ end
81
+ new(**values, server_tool_use: aggregate_server_tool_use(tokens))
82
+ end
83
+
84
+ def self.aggregate_server_tool_use(tokens) # :nodoc:
85
+ reported = tokens.filter_map(&:server_tool_use)
86
+ return nil if reported.empty?
87
+
88
+ reported.each_with_object({}) do |counters, total|
89
+ counters.each { |tool, count| total[tool] = total.fetch(tool, 0) + count.to_i }
90
+ end
30
91
  end
31
- # rubocop:enable Metrics/ParameterLists
92
+ private_class_method :aggregate_server_tool_use
32
93
 
94
+ # Returns the counts as a hash with keys +:input_tokens+,
95
+ # +:output_tokens+, +:cache_read_tokens+, +:cache_write_tokens+, and
96
+ # +:thinking_tokens+, omitting +nil+ counts. Includes +:server_tool_use+
97
+ # when reported. The provider's reported cost is available separately
98
+ # through #reported_cost.
99
+ #
100
+ # response.tokens.to_h
101
+ # # => {input_tokens: 14, output_tokens: 5}
102
+ #
33
103
  def to_h
34
104
  {
35
105
  input_tokens: input,
36
106
  output_tokens: output,
37
- cached_tokens: cached,
38
- cache_creation_tokens: cache_creation,
39
- thinking_tokens: thinking
107
+ cache_read_tokens: cache_read,
108
+ cache_write_tokens: cache_write,
109
+ thinking_tokens: thinking,
110
+ server_tool_use: server_tool_use
40
111
  }.compact
41
112
  end
42
113
 
43
- def reasoning
44
- thinking
45
- end
46
-
47
- def cache_read
48
- cached
49
- end
50
-
51
- def cache_write
52
- cache_creation
114
+ def inspect_attributes # :nodoc:
115
+ {
116
+ input: input,
117
+ output: output,
118
+ cache_read: cache_read,
119
+ cache_write: cache_write,
120
+ thinking: thinking,
121
+ server_tool_use: server_tool_use
122
+ }.compact
53
123
  end
54
124
  end
55
125
  end