ruby_llm 1.16.0 → 2.0.0

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 +109 -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 +3 -1
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -9
  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 +264 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +831 -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 +255 -53
  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 +685 -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 +130 -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/provider_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,329 @@
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_provider_tools and the model used one.
68
+ attr_reader :server_tool_calls
69
+
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:
10
74
 
11
- def initialize(options = {})
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
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) }
130
+ end
131
+
132
+ def without_thinking # :nodoc:
133
+ dup.tap do |message|
134
+ message.instance_variable_set(:@thinking, nil)
135
+ message.instance_variable_set(:@raw_reasoning, nil)
136
+ message.instance_variable_set(:@tool_calls, tool_calls_without_thought_signatures)
36
137
  end
37
138
  end
38
139
 
140
+ # Returns +true+ if the assistant requested one or more tool calls,
141
+ # +false+ otherwise.
39
142
  def tool_call?
40
143
  !tool_calls.nil? && !tool_calls.empty?
41
144
  end
42
145
 
146
+ # Returns +true+ if the message carries the result of a tool call,
147
+ # +false+ otherwise.
43
148
  def tool_result?
44
149
  !tool_call_id.nil? && !tool_call_id.empty?
45
150
  end
46
151
 
152
+ # Returns the tool result messages answering this message's tool calls,
153
+ # or an empty array when it made none. Mirrors the +tool_results+
154
+ # association on acts_as_message records.
47
155
  def tool_results
48
- content if tool_result?
49
- end
156
+ return [] unless tool_call? && conversation
50
157
 
51
- def input_tokens
52
- tokens&.input
158
+ conversation.messages.select do |message|
159
+ message.tool_result? && tool_calls.key?(message.tool_call_id)
160
+ end
53
161
  end
54
162
 
55
- def output_tokens
56
- tokens&.output
163
+ # Returns +true+ if #finish_reason indicates the model finished
164
+ # normally, +false+ otherwise. A turn that stopped to call tools is
165
+ # reported by #tool_call_stop? instead, whatever the provider named it.
166
+ def stopped?
167
+ finish_reason == :stop && !tool_call?
57
168
  end
58
169
 
59
- def cached_tokens
60
- tokens&.cached
170
+ # Returns +true+ if the response was cut off by a token limit,
171
+ # +false+ otherwise.
172
+ def max_tokens?
173
+ finish_reason == :max_tokens
61
174
  end
62
175
 
63
- def cache_creation_tokens
64
- tokens&.cache_creation
176
+ # Returns +true+ if the model stopped to request tool calls,
177
+ # +false+ otherwise.
178
+ def tool_call_stop?
179
+ finish_reason == :tool_calls || (tool_call? && finish_reason == :stop)
65
180
  end
66
181
 
67
- def cache_read_tokens
68
- tokens&.cache_read
182
+ # Returns +true+ if a provider safety filter stopped the response,
183
+ # +false+ otherwise.
184
+ def content_filtered?
185
+ finish_reason == :content_filter
69
186
  end
70
187
 
71
- def cache_write_tokens
72
- tokens&.cache_write
188
+ # Returns usage aggregated across every provider attempt that produced this
189
+ # message. Messages constructed by hand report the token counts they were
190
+ # built with.
191
+ def tokens
192
+ return @tokens if ruby_llm_usage_entries.empty?
193
+
194
+ ruby_llm_usage_tokens
73
195
  end
74
196
 
75
- def thinking_tokens
76
- tokens&.thinking
197
+ # Returns a Cost pricing this message's token usage in US dollars.
198
+ # Uses recorded attempt costs, an explicitly supplied +cost:+, or pricing
199
+ # from #model_info. An explicit +model:+ overrides those costs for repricing.
200
+ #
201
+ # response.cost.total
202
+ #
203
+ def cost(model: nil)
204
+ return ruby_llm_usage_cost if model.nil? && ruby_llm_usage_entries.any?
205
+ return @supplied_cost if model.nil? && @supplied_cost
206
+
207
+ Cost.new(tokens:, model: model || model_info)
77
208
  end
78
209
 
79
- def reasoning_tokens
80
- tokens&.thinking
210
+ # Marks this message as an explicit prompt cache boundary. Providers
211
+ # with boundary controls use the conversation up to and including this
212
+ # message as the cacheable prefix. Returns +self+.
213
+ #
214
+ # chat.add_message(role: :user, content: long_context).cache_until_here
215
+ #
216
+ def cache_until_here
217
+ @cache_until_here = true
218
+ self
81
219
  end
82
220
 
83
- def cost(model: nil)
84
- Cost.new(tokens:, model: model || model_info)
221
+ # Returns +true+ if the message carries an explicit prompt cache
222
+ # boundary, +false+ otherwise.
223
+ def cache_until_here?
224
+ @cache_until_here
85
225
  end
86
226
 
227
+ # Returns a Hash of the message's attributes, with token counts merged
228
+ # in as +:input_tokens+, +:output_tokens+, and related keys. Omits
229
+ # +nil+ values and empty attachment and citation lists. Includes +:cost+
230
+ # only when supplied explicitly, preserving unknown costs on round-trip.
87
231
  def to_h
88
232
  {
89
233
  role: role,
90
234
  content: content,
91
- model_id: model_id,
92
- tool_calls: tool_calls,
235
+ attachments: list_to_h(attachments),
236
+ model: model,
237
+ cost: @supplied_cost && cost.to_h,
238
+ tool_calls: tool_calls&.transform_values(&:to_h),
93
239
  tool_call_id: tool_call_id,
94
240
  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]
241
+ thinking_signature: thinking&.signature,
242
+ citations: list_to_h(citations),
243
+ server_tool_calls: list_to_h(server_tool_calls),
244
+ raw_content: raw_content,
245
+ raw_reasoning: raw_reasoning,
246
+ finish_reason: finish_reason,
247
+ cache_until_here: cache_until_here? || nil
248
+ }.merge(tokens.to_h).compact
101
249
  end
102
250
 
251
+ # Returns the response's Model from its provider's registry, falling
252
+ # back to the requested model when the response ID is unknown.
253
+ # Restored messages use the last successful attempt's provider and model.
254
+ # Messages without request context look up #model, or return +nil+ if unknown.
103
255
  def model_info
104
- return unless model_id
256
+ return @model_info if @model_info
105
257
 
106
- @model_info ||= RubyLLM.models.find(model_id)
258
+ entry = ruby_llm_usage_entries.reverse.find(&:succeeded?)
259
+ @model_info = if entry&.model
260
+ RubyLLM.models.find(entry.model, provider: entry.provider)
261
+ elsif model
262
+ RubyLLM.models.find(model)
263
+ end
107
264
  rescue ModelNotFoundError
108
265
  nil
109
266
  end
110
267
 
111
268
  private
112
269
 
113
- def normalize_content(content, role:, tool_calls:)
114
- return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
270
+ def list_to_h(list)
271
+ list.empty? ? nil : list.map(&:to_h)
272
+ end
273
+
274
+ def coerce_tool_calls(tool_calls)
275
+ return tool_calls unless tool_calls.is_a?(Hash)
276
+
277
+ tool_calls.to_h do |id, call|
278
+ next [id, call] unless call.is_a?(Hash)
279
+
280
+ attributes = call.transform_keys(&:to_sym)
281
+ [id, ToolCall.new(id: attributes[:id] || id, name: attributes[:name],
282
+ arguments: attributes[:arguments] || {},
283
+ thought_signature: attributes[:thought_signature], remote: attributes.fetch(:remote, false))]
284
+ end
285
+ end
286
+
287
+ def tool_calls_without_thought_signatures
288
+ tool_calls&.transform_values do |call|
289
+ call.dup.tap { |copy| copy.thought_signature = nil }
290
+ end
291
+ end
115
292
 
116
- case content
117
- when String then Content.new(content)
118
- when Hash then Content.new(content[:text], content)
119
- else content
293
+ def coerce_thinking(thinking, signature)
294
+ case thinking
295
+ when nil, Thinking then thinking
296
+ when Hash then Thinking.build(**thinking.transform_keys(&:to_sym).slice(:text, :signature))
297
+ else Thinking.build(text: thinking.to_s, signature: signature)
120
298
  end
121
299
  end
122
300
 
301
+ def coerce_value(value, klass)
302
+ value.is_a?(Hash) ? klass.from_h(value) : value
303
+ end
304
+
305
+ def normalize_content(content)
306
+ return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
307
+ return content if content.nil? || content.is_a?(String)
308
+
309
+ raise ArgumentError,
310
+ "Message content must be a String, got #{content.class}. " \
311
+ 'Pass files via attachments: and structured data as JSON.'
312
+ end
313
+
123
314
  def ensure_valid_role
124
315
  raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
125
316
  end
317
+
318
+ def inspect_attributes # :nodoc:
319
+ {
320
+ role: role,
321
+ content: content,
322
+ tool_calls: tool_calls&.values&.map(&:name),
323
+ tool_call_id: tool_call_id,
324
+ model: model,
325
+ finish_reason: finish_reason
326
+ }
327
+ end
126
328
  end
127
329
  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