ruby_llm 1.16.0 → 2.0.0.rc4

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 (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  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/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +242 -54
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +662 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +126 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -1,127 +1,315 @@
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
+ attr_writer :model_info # :nodoc:
39
+
40
+ # The tool calls the assistant requested, as a Hash of ToolCall objects
41
+ # keyed by call ID, or +nil+.
42
+ attr_reader :tool_calls
43
+
44
+ # The ID of the tool call this message answers. Set only on tool result
45
+ # messages.
46
+ attr_reader :tool_call_id
47
+
48
+ # The raw provider response: a Faraday::Response, or the result body
49
+ # Hash for messages retrieved from a Batch.
50
+ attr_reader :raw
51
+
52
+ # The model's reasoning output as a Thinking object, or +nil+ when the
53
+ # provider returned none.
54
+ attr_reader :thinking
55
+
56
+ # The source citations as an array of Citation objects, normalized
57
+ # across providers.
58
+ attr_reader :citations
59
+
60
+ # Why the model stopped: +:stop+, +:max_tokens+, +:tool_calls+, or
61
+ # +:content_filter+. Any other reason comes through as the provider
62
+ # spelled it, such as Anthropic's +:pause_turn+.
63
+ attr_reader :finish_reason
64
+
65
+ # The provider-executed tool steps in this response, as an array of
66
+ # ServerToolCall objects. Empty unless the chat enabled tools with
67
+ # Chat#with_server_tools and the model used one.
68
+ attr_reader :server_tool_calls
10
69
 
11
- def initialize(options = {})
70
+ # The provider-shaped content blocks of this assistant message, kept
71
+ # verbatim when the response used server tools so later requests can
72
+ # replay the turn exactly. +nil+ otherwise.
73
+ attr_reader :raw_content # :nodoc:
74
+
75
+ # The provider-shaped reasoning payload of this assistant message, kept
76
+ # verbatim so later requests can replay the model's reasoning exactly.
77
+ # +nil+ when the provider returned none.
78
+ attr_reader :raw_reasoning # :nodoc:
79
+
80
+ # The Chat this message belongs to, set when it is added to a
81
+ # conversation. Backs #tool_results.
82
+ attr_accessor :conversation # :nodoc:
83
+
84
+ def initialize(options = {}) # :nodoc:
12
85
  @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]
86
+ @tool_calls = coerce_tool_calls(options[:tool_calls])
87
+ @content = normalize_content(options.fetch(:content))
88
+ @config = options[:config]
89
+ @attachments = Attachment.wrap(options[:attachments], config: @config)
90
+ @model = options[:model]
91
+ @supplied_cost = coerce_value(options[:cost], Cost)
16
92
  @tool_call_id = options[:tool_call_id]
17
- @tokens = options[:tokens] || Tokens.build(
93
+ @tokens = options[:tokens] || Tokens.new(
18
94
  input: options[:input_tokens],
19
95
  output: options[:output_tokens],
20
- cached: options[:cached_tokens],
21
- cache_creation: options[:cache_creation_tokens],
96
+ cache_read: options[:cache_read_tokens],
97
+ cache_write: options[:cache_write_tokens],
22
98
  thinking: options[:thinking_tokens],
23
- reasoning: options[:reasoning_tokens]
99
+ server_tool_use: options[:server_tool_use],
100
+ reported_cost: options[:reported_cost]
24
101
  )
25
102
  @raw = options[:raw]
26
- @thinking = options[:thinking]
103
+ @thinking = coerce_thinking(options[:thinking], options[:thinking_signature])
104
+ @citations = Array(options[:citations]).map { |citation| coerce_value(citation, Citation) }
105
+ @server_tool_calls = Array(options[:server_tool_calls]).map { |call| coerce_value(call, ServerToolCall) }
106
+ @raw_content = options[:raw_content]
107
+ @raw_reasoning = options[:raw_reasoning]
108
+ @finish_reason = options[:finish_reason]&.to_sym
109
+ self.ruby_llm_usage_entries = options[:usage_entries] if options[:usage_entries]
110
+ @cache_until_here = options.fetch(:cache_until_here, false)
27
111
 
28
112
  ensure_valid_role
29
113
  end
30
114
 
31
- def content
32
- if @content.is_a?(Content) && @content.text && @content.attachments.empty?
33
- @content.text
34
- else
35
- @content
36
- end
115
+ # Returns #content parsed as JSON, memoized after the first call.
116
+ # Useful for reading structured output responses.
117
+ #
118
+ # response = chat.with_schema(PersonSchema).ask "Generate a person"
119
+ # response.parsed # => {"name" => "Alice", "age" => 30}
120
+ #
121
+ def parsed
122
+ return if content.nil? || content.empty?
123
+
124
+ @parsed ||= JSON.parse(content)
125
+ end
126
+
127
+ def with_attachments(attachments) # :nodoc:
128
+ wrapped = Attachment.wrap(attachments, config: @config)
129
+ dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
37
130
  end
38
131
 
132
+ # Returns +true+ if the assistant requested one or more tool calls,
133
+ # +false+ otherwise.
39
134
  def tool_call?
40
135
  !tool_calls.nil? && !tool_calls.empty?
41
136
  end
42
137
 
138
+ # Returns +true+ if the message carries the result of a tool call,
139
+ # +false+ otherwise.
43
140
  def tool_result?
44
141
  !tool_call_id.nil? && !tool_call_id.empty?
45
142
  end
46
143
 
144
+ # Returns the tool result messages answering this message's tool calls,
145
+ # or an empty array when it made none. Mirrors the +tool_results+
146
+ # association on acts_as_message records.
47
147
  def tool_results
48
- content if tool_result?
49
- end
148
+ return [] unless tool_call? && conversation
50
149
 
51
- def input_tokens
52
- tokens&.input
150
+ conversation.messages.select do |message|
151
+ message.tool_result? && tool_calls.key?(message.tool_call_id)
152
+ end
53
153
  end
54
154
 
55
- def output_tokens
56
- tokens&.output
155
+ # Returns +true+ if #finish_reason indicates the model finished
156
+ # normally, +false+ otherwise. A turn that stopped to call tools is
157
+ # reported by #tool_call_stop? instead, whatever the provider named it.
158
+ def stopped?
159
+ finish_reason == :stop && !tool_call?
57
160
  end
58
161
 
59
- def cached_tokens
60
- tokens&.cached
162
+ # Returns +true+ if the response was cut off by a token limit,
163
+ # +false+ otherwise.
164
+ def max_tokens?
165
+ finish_reason == :max_tokens
61
166
  end
62
167
 
63
- def cache_creation_tokens
64
- tokens&.cache_creation
168
+ # Returns +true+ if the model stopped to request tool calls,
169
+ # +false+ otherwise.
170
+ def tool_call_stop?
171
+ finish_reason == :tool_calls || (tool_call? && finish_reason == :stop)
65
172
  end
66
173
 
67
- def cache_read_tokens
68
- tokens&.cache_read
174
+ # Returns +true+ if a provider safety filter stopped the response,
175
+ # +false+ otherwise.
176
+ def content_filtered?
177
+ finish_reason == :content_filter
69
178
  end
70
179
 
71
- def cache_write_tokens
72
- tokens&.cache_write
180
+ # Returns usage aggregated across every provider attempt that produced this
181
+ # message. Messages constructed by hand report the token counts they were
182
+ # built with.
183
+ def tokens
184
+ return @tokens if ruby_llm_usage_entries.empty?
185
+
186
+ ruby_llm_usage_tokens
73
187
  end
74
188
 
75
- def thinking_tokens
76
- tokens&.thinking
189
+ # Returns a Cost pricing this message's token usage in US dollars.
190
+ # Uses recorded attempt costs, an explicitly supplied +cost:+, or pricing
191
+ # from #model_info. An explicit +model:+ overrides those costs for repricing.
192
+ #
193
+ # response.cost.total
194
+ #
195
+ def cost(model: nil)
196
+ return ruby_llm_usage_cost if model.nil? && ruby_llm_usage_entries.any?
197
+ return @supplied_cost if model.nil? && @supplied_cost
198
+
199
+ Cost.new(tokens:, model: model || model_info)
77
200
  end
78
201
 
79
- def reasoning_tokens
80
- tokens&.thinking
202
+ # Marks this message as an explicit prompt cache boundary. Providers
203
+ # with boundary controls use the conversation up to and including this
204
+ # message as the cacheable prefix. Returns +self+.
205
+ #
206
+ # chat.add_message(role: :user, content: long_context).cache_until_here
207
+ #
208
+ def cache_until_here
209
+ @cache_until_here = true
210
+ self
81
211
  end
82
212
 
83
- def cost(model: nil)
84
- Cost.new(tokens:, model: model || model_info)
213
+ # Returns +true+ if the message carries an explicit prompt cache
214
+ # boundary, +false+ otherwise.
215
+ def cache_until_here?
216
+ @cache_until_here
85
217
  end
86
218
 
219
+ # Returns a Hash of the message's attributes, with token counts merged
220
+ # in as +:input_tokens+, +:output_tokens+, and related keys. Omits
221
+ # +nil+ values and empty attachment and citation lists. Includes +:cost+
222
+ # only when supplied explicitly, preserving unknown costs on round-trip.
87
223
  def to_h
88
224
  {
89
225
  role: role,
90
226
  content: content,
91
- model_id: model_id,
92
- tool_calls: tool_calls,
227
+ attachments: list_to_h(attachments),
228
+ model: model,
229
+ cost: @supplied_cost && cost.to_h,
230
+ tool_calls: tool_calls&.transform_values(&:to_h),
93
231
  tool_call_id: tool_call_id,
94
232
  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]
233
+ thinking_signature: thinking&.signature,
234
+ citations: list_to_h(citations),
235
+ server_tool_calls: list_to_h(server_tool_calls),
236
+ raw_content: raw_content,
237
+ raw_reasoning: raw_reasoning,
238
+ finish_reason: finish_reason,
239
+ cache_until_here: cache_until_here? || nil
240
+ }.merge(tokens.to_h).compact
101
241
  end
102
242
 
243
+ # Returns the response's Model from its provider's registry, falling
244
+ # back to the requested model when the response ID is unknown.
245
+ # Restored messages use the last successful attempt's provider and model.
246
+ # Messages without request context look up #model, or return +nil+ if unknown.
103
247
  def model_info
104
- return unless model_id
248
+ return @model_info if @model_info
105
249
 
106
- @model_info ||= RubyLLM.models.find(model_id)
250
+ entry = ruby_llm_usage_entries.reverse.find(&:succeeded?)
251
+ @model_info = if entry&.model
252
+ RubyLLM.models.find(entry.model, provider: entry.provider)
253
+ elsif model
254
+ RubyLLM.models.find(model)
255
+ end
107
256
  rescue ModelNotFoundError
108
257
  nil
109
258
  end
110
259
 
111
260
  private
112
261
 
113
- def normalize_content(content, role:, tool_calls:)
114
- return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
262
+ def list_to_h(list)
263
+ list.empty? ? nil : list.map(&:to_h)
264
+ end
265
+
266
+ def coerce_tool_calls(tool_calls)
267
+ return tool_calls unless tool_calls.is_a?(Hash)
268
+
269
+ tool_calls.to_h do |id, call|
270
+ next [id, call] unless call.is_a?(Hash)
271
+
272
+ attributes = call.transform_keys(&:to_sym)
273
+ [id, ToolCall.new(id: attributes[:id] || id, name: attributes[:name],
274
+ arguments: attributes[:arguments] || {},
275
+ thought_signature: attributes[:thought_signature], remote: attributes.fetch(:remote, false))]
276
+ end
277
+ end
115
278
 
116
- case content
117
- when String then Content.new(content)
118
- when Hash then Content.new(content[:text], content)
119
- else content
279
+ def coerce_thinking(thinking, signature)
280
+ case thinking
281
+ when nil, Thinking then thinking
282
+ when Hash then Thinking.build(**thinking.transform_keys(&:to_sym).slice(:text, :signature))
283
+ else Thinking.build(text: thinking.to_s, signature: signature)
120
284
  end
121
285
  end
122
286
 
287
+ def coerce_value(value, klass)
288
+ value.is_a?(Hash) ? klass.from_h(value) : value
289
+ end
290
+
291
+ def normalize_content(content)
292
+ return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
293
+ return content if content.nil? || content.is_a?(String)
294
+
295
+ raise ArgumentError,
296
+ "Message content must be a String, got #{content.class}. " \
297
+ 'Pass files via attachments: and structured data as JSON.'
298
+ end
299
+
123
300
  def ensure_valid_role
124
301
  raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
125
302
  end
303
+
304
+ def inspect_attributes # :nodoc:
305
+ {
306
+ role: role,
307
+ content: content,
308
+ tool_calls: tool_calls&.values&.map(&:name),
309
+ tool_call_id: tool_call_id,
310
+ model: model,
311
+ finish_reason: finish_reason
312
+ }
313
+ end
126
314
  end
127
315
  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