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,127 +1,306 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # A single message in a chat conversation.
4
+ # A Message is a single entry in a chat conversation: a user prompt, an
5
+ # assistant reply, a system instruction, or a tool result. Chat#ask
6
+ # returns the model's reply as a Message, and Chat#messages holds the
7
+ # transcript as an array of them.
8
+ #
9
+ # response = chat.ask "What is the capital of France?"
10
+ # response.role # => :assistant
11
+ # response.content # => "The capital of France is Paris."
12
+ # response.finish_reason # => :stop
13
+ #
14
+ # A Message also carries everything else the provider returned: token
15
+ # usage (#tokens), reasoning output (#thinking), source citations
16
+ # (#citations), and requested tool calls (#tool_calls).
5
17
  class Message
18
+ include Support::Inspectable
19
+ include Accounting::Usage::Result
20
+
21
+ # The valid message roles: +:system+, +:user+, +:assistant+, and +:tool+.
6
22
  ROLES = %i[system user assistant tool].freeze
7
23
 
8
- attr_reader :role, :model_id, :tool_calls, :tool_call_id, :raw, :thinking, :tokens
9
- attr_writer :content
24
+ # The role of the message: +:system+, +:user+, +:assistant+, or +:tool+.
25
+ attr_reader :role
26
+
27
+ # The message text as a String. Empty for assistant messages that only
28
+ # request tool calls.
29
+ attr_reader :content
30
+
31
+ # The files sent or returned with the message, as an array of
32
+ # Attachment objects.
33
+ attr_reader :attachments
34
+
35
+ # The ID of the model that produced the message, +nil+ on user messages.
36
+ attr_reader :model
37
+
38
+ # The tool calls the assistant requested, as a Hash of ToolCall objects
39
+ # keyed by call ID, or +nil+.
40
+ attr_reader :tool_calls
41
+
42
+ # The ID of the tool call this message answers. Set only on tool result
43
+ # messages.
44
+ attr_reader :tool_call_id
45
+
46
+ # The raw provider response: a Faraday::Response, or the result body
47
+ # Hash for messages retrieved from a Batch.
48
+ attr_reader :raw
49
+
50
+ # The model's reasoning output as a Thinking object, or +nil+ when the
51
+ # provider returned none.
52
+ attr_reader :thinking
53
+
54
+ # The source citations as an array of Citation objects, normalized
55
+ # across providers.
56
+ attr_reader :citations
57
+
58
+ # Why the model stopped: +:stop+, +:max_tokens+, +:tool_calls+, or
59
+ # +:content_filter+. Any other reason comes through as the provider
60
+ # spelled it, such as Anthropic's +:pause_turn+.
61
+ attr_reader :finish_reason
62
+
63
+ # The provider-executed tool steps in this response, as an array of
64
+ # ServerToolCall objects. Empty unless the chat enabled tools with
65
+ # Chat#with_server_tools and the model used one.
66
+ attr_reader :server_tool_calls
67
+
68
+ # The provider-shaped content blocks of this assistant message, kept
69
+ # verbatim when the response used server tools so later requests can
70
+ # replay the turn exactly. +nil+ otherwise.
71
+ attr_reader :raw_content # :nodoc:
10
72
 
11
- def initialize(options = {})
73
+ # The provider-shaped reasoning payload of this assistant message, kept
74
+ # verbatim so later requests can replay the model's reasoning exactly.
75
+ # +nil+ when the provider returned none.
76
+ attr_reader :raw_reasoning # :nodoc:
77
+
78
+ # The Chat this message belongs to, set when it is added to a
79
+ # conversation. Backs #tool_results.
80
+ attr_accessor :conversation # :nodoc:
81
+
82
+ def initialize(options = {}) # :nodoc:
12
83
  @role = options.fetch(:role).to_sym
13
- @tool_calls = options[:tool_calls]
14
- @content = normalize_content(options.fetch(:content), role: @role, tool_calls: @tool_calls)
15
- @model_id = options[:model_id]
84
+ @tool_calls = coerce_tool_calls(options[:tool_calls])
85
+ @content = normalize_content(options.fetch(:content))
86
+ @config = options[:config]
87
+ @attachments = Attachment.wrap(options[:attachments], config: @config)
88
+ @model = options[:model]
89
+ @supplied_cost = coerce_value(options[:cost], Cost)
16
90
  @tool_call_id = options[:tool_call_id]
17
- @tokens = options[:tokens] || Tokens.build(
91
+ @tokens = options[:tokens] || Tokens.new(
18
92
  input: options[:input_tokens],
19
93
  output: options[:output_tokens],
20
- cached: options[:cached_tokens],
21
- cache_creation: options[:cache_creation_tokens],
94
+ cache_read: options[:cache_read_tokens],
95
+ cache_write: options[:cache_write_tokens],
22
96
  thinking: options[:thinking_tokens],
23
- reasoning: options[:reasoning_tokens]
97
+ server_tool_use: options[:server_tool_use],
98
+ reported_cost: options[:reported_cost]
24
99
  )
25
100
  @raw = options[:raw]
26
- @thinking = options[:thinking]
101
+ @thinking = coerce_thinking(options[:thinking], options[:thinking_signature])
102
+ @citations = Array(options[:citations]).map { |citation| coerce_value(citation, Citation) }
103
+ @server_tool_calls = Array(options[:server_tool_calls]).map { |call| coerce_value(call, ServerToolCall) }
104
+ @raw_content = options[:raw_content]
105
+ @raw_reasoning = options[:raw_reasoning]
106
+ @finish_reason = options[:finish_reason]&.to_sym
107
+ self.ruby_llm_usage_entries = options[:usage_entries] if options[:usage_entries]
108
+ @cache_until_here = options.fetch(:cache_until_here, false)
27
109
 
28
110
  ensure_valid_role
29
111
  end
30
112
 
31
- def content
32
- if @content.is_a?(Content) && @content.text && @content.attachments.empty?
33
- @content.text
34
- else
35
- @content
36
- end
113
+ # Returns #content parsed as JSON, memoized after the first call.
114
+ # Useful for reading structured output responses.
115
+ #
116
+ # response = chat.with_schema(PersonSchema).ask "Generate a person"
117
+ # response.parsed # => {"name" => "Alice", "age" => 30}
118
+ #
119
+ def parsed
120
+ return if content.nil? || content.empty?
121
+
122
+ @parsed ||= JSON.parse(content)
123
+ end
124
+
125
+ def with_attachments(attachments) # :nodoc:
126
+ wrapped = Attachment.wrap(attachments, config: @config)
127
+ dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
37
128
  end
38
129
 
130
+ # Returns +true+ if the assistant requested one or more tool calls,
131
+ # +false+ otherwise.
39
132
  def tool_call?
40
133
  !tool_calls.nil? && !tool_calls.empty?
41
134
  end
42
135
 
136
+ # Returns +true+ if the message carries the result of a tool call,
137
+ # +false+ otherwise.
43
138
  def tool_result?
44
139
  !tool_call_id.nil? && !tool_call_id.empty?
45
140
  end
46
141
 
142
+ # Returns the tool result messages answering this message's tool calls,
143
+ # or an empty array when it made none. Mirrors the +tool_results+
144
+ # association on acts_as_message records.
47
145
  def tool_results
48
- content if tool_result?
49
- end
146
+ return [] unless tool_call? && conversation
50
147
 
51
- def input_tokens
52
- tokens&.input
148
+ conversation.messages.select do |message|
149
+ message.tool_result? && tool_calls.key?(message.tool_call_id)
150
+ end
53
151
  end
54
152
 
55
- def output_tokens
56
- tokens&.output
153
+ # Returns +true+ if #finish_reason indicates the model finished
154
+ # normally, +false+ otherwise. A turn that stopped to call tools is
155
+ # reported by #tool_call_stop? instead, whatever the provider named it.
156
+ def stopped?
157
+ finish_reason == :stop && !tool_call?
57
158
  end
58
159
 
59
- def cached_tokens
60
- tokens&.cached
160
+ # Returns +true+ if the response was cut off by a token limit,
161
+ # +false+ otherwise.
162
+ def max_tokens?
163
+ finish_reason == :max_tokens
61
164
  end
62
165
 
63
- def cache_creation_tokens
64
- tokens&.cache_creation
166
+ # Returns +true+ if the model stopped to request tool calls,
167
+ # +false+ otherwise.
168
+ def tool_call_stop?
169
+ finish_reason == :tool_calls || (tool_call? && finish_reason == :stop)
65
170
  end
66
171
 
67
- def cache_read_tokens
68
- tokens&.cache_read
172
+ # Returns +true+ if a provider safety filter stopped the response,
173
+ # +false+ otherwise.
174
+ def content_filtered?
175
+ finish_reason == :content_filter
69
176
  end
70
177
 
71
- def cache_write_tokens
72
- tokens&.cache_write
178
+ # Returns usage aggregated across every provider attempt that produced this
179
+ # message. Messages constructed by hand report the token counts they were
180
+ # built with.
181
+ def tokens
182
+ return @tokens if ruby_llm_usage_entries.empty?
183
+
184
+ ruby_llm_usage_tokens
73
185
  end
74
186
 
75
- def thinking_tokens
76
- tokens&.thinking
187
+ # Returns a Cost pricing this message's token usage in US dollars.
188
+ # Uses recorded attempt costs, an explicitly supplied +cost:+, or pricing
189
+ # from #model_info. An explicit +model:+ overrides those costs for repricing.
190
+ #
191
+ # response.cost.total
192
+ #
193
+ def cost(model: nil)
194
+ return ruby_llm_usage_cost if model.nil? && ruby_llm_usage_entries.any?
195
+ return @supplied_cost if model.nil? && @supplied_cost
196
+
197
+ Cost.new(tokens:, model: model || model_info)
77
198
  end
78
199
 
79
- def reasoning_tokens
80
- tokens&.thinking
200
+ # Marks this message as an explicit prompt cache boundary. Providers
201
+ # with boundary controls use the conversation up to and including this
202
+ # message as the cacheable prefix. Returns +self+.
203
+ #
204
+ # chat.add_message(role: :user, content: long_context).cache_until_here
205
+ #
206
+ def cache_until_here
207
+ @cache_until_here = true
208
+ self
81
209
  end
82
210
 
83
- def cost(model: nil)
84
- Cost.new(tokens:, model: model || model_info)
211
+ # Returns +true+ if the message carries an explicit prompt cache
212
+ # boundary, +false+ otherwise.
213
+ def cache_until_here?
214
+ @cache_until_here
85
215
  end
86
216
 
217
+ # Returns a Hash of the message's attributes, with token counts merged
218
+ # in as +:input_tokens+, +:output_tokens+, and related keys. Omits
219
+ # +nil+ values and empty attachment and citation lists. Includes +:cost+
220
+ # only when supplied explicitly, preserving unknown costs on round-trip.
87
221
  def to_h
88
222
  {
89
223
  role: role,
90
224
  content: content,
91
- model_id: model_id,
92
- tool_calls: tool_calls,
225
+ attachments: list_to_h(attachments),
226
+ model: model,
227
+ cost: @supplied_cost && cost.to_h,
228
+ tool_calls: tool_calls&.transform_values(&:to_h),
93
229
  tool_call_id: tool_call_id,
94
230
  thinking: thinking&.text,
95
- thinking_signature: thinking&.signature
96
- }.merge(tokens ? tokens.to_h : {}).compact
97
- end
98
-
99
- def instance_variables
100
- super - [:@raw]
231
+ thinking_signature: thinking&.signature,
232
+ citations: list_to_h(citations),
233
+ server_tool_calls: list_to_h(server_tool_calls),
234
+ raw_content: raw_content,
235
+ raw_reasoning: raw_reasoning,
236
+ finish_reason: finish_reason,
237
+ cache_until_here: cache_until_here? || nil
238
+ }.merge(tokens.to_h).compact
101
239
  end
102
240
 
241
+ # Returns the Model record for #model from the model registry, or
242
+ # +nil+ when the message has no model or the model is unknown.
103
243
  def model_info
104
- return unless model_id
244
+ return unless model
105
245
 
106
- @model_info ||= RubyLLM.models.find(model_id)
246
+ @model_info ||= RubyLLM.models.find(model)
107
247
  rescue ModelNotFoundError
108
248
  nil
109
249
  end
110
250
 
111
251
  private
112
252
 
113
- def normalize_content(content, role:, tool_calls:)
114
- return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
253
+ def list_to_h(list)
254
+ list.empty? ? nil : list.map(&:to_h)
255
+ end
115
256
 
116
- case content
117
- when String then Content.new(content)
118
- when Hash then Content.new(content[:text], content)
119
- else content
257
+ def coerce_tool_calls(tool_calls)
258
+ return tool_calls unless tool_calls.is_a?(Hash)
259
+
260
+ tool_calls.to_h do |id, call|
261
+ next [id, call] unless call.is_a?(Hash)
262
+
263
+ attributes = call.transform_keys(&:to_sym)
264
+ [id, ToolCall.new(id: attributes[:id] || id, name: attributes[:name],
265
+ arguments: attributes[:arguments] || {},
266
+ thought_signature: attributes[:thought_signature], remote: attributes.fetch(:remote, false))]
267
+ end
268
+ end
269
+
270
+ def coerce_thinking(thinking, signature)
271
+ case thinking
272
+ when nil, Thinking then thinking
273
+ when Hash then Thinking.build(**thinking.transform_keys(&:to_sym).slice(:text, :signature))
274
+ else Thinking.build(text: thinking.to_s, signature: signature)
120
275
  end
121
276
  end
122
277
 
278
+ def coerce_value(value, klass)
279
+ value.is_a?(Hash) ? klass.from_h(value) : value
280
+ end
281
+
282
+ def normalize_content(content)
283
+ return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
284
+ return content if content.nil? || content.is_a?(String)
285
+
286
+ raise ArgumentError,
287
+ "Message content must be a String, got #{content.class}. " \
288
+ 'Pass files via attachments: and structured data as JSON.'
289
+ end
290
+
123
291
  def ensure_valid_role
124
292
  raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
125
293
  end
294
+
295
+ def inspect_attributes # :nodoc:
296
+ {
297
+ role: role,
298
+ content: content,
299
+ tool_calls: tool_calls&.values&.map(&:name),
300
+ tool_call_id: tool_call_id,
301
+ model: model,
302
+ finish_reason: finish_reason
303
+ }
304
+ end
126
305
  end
127
306
  end
@@ -1,16 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # Holds and manages input and output modalities for a language model
4
+ class Model
5
+ # A Model::Modalities lists the kinds of content a model accepts and
6
+ # produces, as arrays of Strings. Instances come from Model#modalities.
7
+ #
8
+ # model = RubyLLM.models.find('gpt-5.6')
9
+ # model.modalities.input # => ["text", "image", "pdf"]
10
+ # model.modalities.output # => ["text"]
11
+ #
6
12
  class Modalities
7
- attr_reader :input, :output
13
+ # The input modalities as an array of Strings,
14
+ # e.g. <tt>["text", "image", "pdf"]</tt>.
15
+ attr_reader :input
8
16
 
9
- def initialize(data)
17
+ # The output modalities as an array of Strings,
18
+ # e.g. <tt>["text"]</tt> or <tt>["embeddings"]</tt>.
19
+ attr_reader :output
20
+
21
+ def initialize(data) # :nodoc:
10
22
  @input = Array(data[:input]).map(&:to_s)
11
23
  @output = Array(data[:output]).map(&:to_s)
12
24
  end
13
25
 
26
+ # Returns the modalities as a Hash with +:input+ and +:output+ keys.
14
27
  def to_h
15
28
  {
16
29
  input: input,
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # A collection that manages and provides access to different categories of pricing information
4
+ class Model
5
+ # A Pricing groups a model's prices by usage category: text tokens,
6
+ # images, audio tokens, and embeddings. Each category is a
7
+ # PricingCategory. Prices are in USD per million tokens. Instances come
8
+ # from Model#pricing.
9
+ #
10
+ # model = RubyLLM.models.find "claude-sonnet-5"
11
+ # model.pricing.text_tokens.input # => 3
12
+ # model.pricing.text_tokens.output # => 15
13
+ #
6
14
  class Pricing
15
+ # The pricing categories a model may define.
7
16
  CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
8
17
 
9
- def initialize(data)
18
+ def initialize(data) # :nodoc:
10
19
  @data = {}
11
20
 
12
21
  CATEGORIES.each do |category|
@@ -14,22 +23,32 @@ module RubyLLM
14
23
  end
15
24
  end
16
25
 
26
+ # Returns the PricingCategory for text token prices, or an empty
27
+ # category if the model has none.
17
28
  def text_tokens
18
29
  category(:text_tokens)
19
30
  end
20
31
 
32
+ # Returns the PricingCategory for image generation prices, or an empty
33
+ # category if the model has none.
21
34
  def images
22
35
  category(:images)
23
36
  end
24
37
 
38
+ # Returns the PricingCategory for audio token prices, or an empty
39
+ # category if the model has none.
25
40
  def audio_tokens
26
41
  category(:audio_tokens)
27
42
  end
28
43
 
44
+ # Returns the PricingCategory for embedding prices, or an empty
45
+ # category if the model has none.
29
46
  def embeddings
30
47
  category(:embeddings)
31
48
  end
32
49
 
50
+ # Returns the pricing data as a nested Hash keyed by category.
51
+ # Categories without prices are omitted.
33
52
  def to_h
34
53
  @data.transform_values(&:to_h)
35
54
  end
@@ -43,11 +62,11 @@ module RubyLLM
43
62
  def empty_pricing?(data)
44
63
  return true unless data
45
64
 
46
- %i[standard batch].each do |tier|
65
+ PricingCategory::TIERS.each do |tier|
47
66
  next unless data[tier]
48
67
 
49
68
  data[tier].each_value do |value|
50
- return false if value && value != 0.0
69
+ return false unless value.nil?
51
70
  end
52
71
  end
53
72
 
@@ -1,56 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Model
5
- # Represents pricing tiers for different usage categories (standard and batch)
4
+ class Model
5
+ # A PricingCategory holds the standard, batch, and long-context pricing
6
+ # tiers for one kind of model usage, such as text tokens or images.
7
+ # Model#pricing returns a Pricing collection whose categories are
8
+ # PricingCategory instances. Prices are in USD per million tokens.
9
+ #
10
+ # model = RubyLLM.models.find "claude-sonnet-5"
11
+ # category = model.pricing.text_tokens
12
+ # category.input # => 3
13
+ # category.output # => 15
14
+ #
6
15
  class PricingCategory
7
- attr_reader :standard, :batch
16
+ # The billing tiers a category may define.
17
+ TIERS = %i[standard batch long_context].freeze
8
18
 
9
- def initialize(data = {})
10
- @standard = PricingTier.new(data[:standard] || {}) unless empty_tier?(data[:standard])
11
- @batch = PricingTier.new(data[:batch] || {}) unless empty_tier?(data[:batch])
19
+ # The standard-tier PricingTier, or +nil+ when the model has no
20
+ # standard pricing for this category.
21
+ attr_reader :standard
22
+
23
+ # The batch-tier PricingTier, or +nil+ when the model has no batch
24
+ # pricing for this category.
25
+ attr_reader :batch
26
+
27
+ # The long-context PricingTier, or +nil+ when the model has no
28
+ # separate rates for prompts above #long_context_threshold.
29
+ attr_reader :long_context
30
+
31
+ # Prompt-size threshold (in tokens) above which long-context rates
32
+ # apply, or +nil+ when the model has no long-context tier.
33
+ attr_reader :long_context_threshold
34
+
35
+ def initialize(data = {}) # :nodoc:
36
+ data = data.transform_keys(&:to_sym) if data.respond_to?(:transform_keys)
37
+
38
+ @standard = tier_from(data[:standard])
39
+ @batch = tier_from(data[:batch])
40
+ @long_context = tier_from(data[:long_context])
41
+ @long_context_threshold = Integer(data[:long_context_threshold], exception: false)
12
42
  end
13
43
 
44
+ # Returns the standard-tier input price in USD per million tokens,
45
+ # or +nil+ if the price is missing.
14
46
  def input
15
47
  standard&.input_per_million
16
48
  end
17
49
 
50
+ # Returns the standard-tier output price in USD per million tokens,
51
+ # or +nil+ if the price is missing.
18
52
  def output
19
53
  standard&.output_per_million
20
54
  end
21
55
 
56
+ # Returns the standard-tier cache read price in USD per million
57
+ # tokens, or +nil+ if the price is missing.
22
58
  def cache_read_input
23
- standard&.cache_read_input_per_million || standard&.cached_input_per_million
59
+ standard&.cache_read_input_per_million
24
60
  end
25
61
 
62
+ # Returns the standard-tier cache write price in USD per million
63
+ # tokens, or +nil+ if the price is missing.
26
64
  def cache_write_input
27
- standard&.cache_write_input_per_million || standard&.cache_creation_input_per_million
65
+ standard&.cache_write_input_per_million
28
66
  end
29
67
 
68
+ # Returns the standard-tier reasoning output price in USD per million
69
+ # tokens, or +nil+ if the price is missing.
30
70
  def reasoning_output
31
71
  standard&.reasoning_output_per_million
32
72
  end
33
73
 
34
- alias cached_input cache_read_input
35
- alias cache_creation_input cache_write_input
36
-
37
- def [](key)
38
- key == :batch ? batch : standard
74
+ # Returns the PricingTier that applies for a prompt of the given size.
75
+ # Uses #long_context when that tier exists and +prompt_tokens+ is
76
+ # greater than #long_context_threshold; otherwise returns #standard.
77
+ def tier_for(prompt_tokens)
78
+ if long_context && long_context_threshold &&
79
+ prompt_tokens.to_i > long_context_threshold
80
+ long_context
81
+ else
82
+ standard
83
+ end
39
84
  end
40
85
 
86
+ # Returns a Hash with present tier hashes and optional
87
+ # +:long_context_threshold+, omitting absent entries.
41
88
  def to_h
42
89
  result = {}
43
90
  result[:standard] = standard.to_h if standard
44
91
  result[:batch] = batch.to_h if batch
92
+ result[:long_context] = long_context.to_h if long_context
93
+ result[:long_context_threshold] = long_context_threshold if long_context_threshold
45
94
  result
46
95
  end
47
96
 
97
+ # Builds long-context rates and threshold from a models.dev-style cost
98
+ # Hash (as stored on Model#metadata under +:cost+). Returns
99
+ # <tt>[rates_hash, threshold]</tt>, or <tt>[nil, nil]</tt> when the
100
+ # cost has no context tier.
101
+ def self.long_context_from_cost(cost) # :nodoc:
102
+ cost = RubyLLM::Support::Utils.deep_symbolize_keys(cost || {})
103
+ return [nil, nil] if cost.empty?
104
+
105
+ entry, threshold = context_cost(cost)
106
+ return [nil, nil] unless entry
107
+
108
+ rates = {
109
+ input_per_million: entry[:input],
110
+ output_per_million: entry[:output],
111
+ cache_read_input_per_million: entry[:cache_read],
112
+ cache_write_input_per_million: entry[:cache_write],
113
+ reasoning_output_per_million: entry[:reasoning]
114
+ }.compact
115
+ rates.empty? ? [nil, nil] : [rates, threshold]
116
+ end
117
+
118
+ def self.context_cost(cost) # :nodoc:
119
+ context_tier = Array(cost[:tiers]).find do |entry|
120
+ entry.is_a?(Hash) && entry.dig(:tier, :type).to_s == 'context'
121
+ end
122
+ if context_tier
123
+ [context_tier, Integer(context_tier.dig(:tier, :size), exception: false)]
124
+ elsif cost[:context_over_200k].is_a?(Hash)
125
+ [cost[:context_over_200k], 200_000]
126
+ end
127
+ end
128
+
129
+ private_class_method :context_cost
130
+
48
131
  private
49
132
 
133
+ def tier_from(tier_data)
134
+ return nil if empty_tier?(tier_data)
135
+
136
+ PricingTier.new(tier_data || {})
137
+ end
138
+
50
139
  def empty_tier?(tier_data)
51
140
  return true unless tier_data
52
141
 
53
- tier_data.values.all? { |v| v.nil? || v == 0.0 }
142
+ tier_data.values.all?(&:nil?)
54
143
  end
55
144
  end
56
145
  end