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
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # The Message Batches API: Messages requests processed together,
7
+ # asynchronously, at half price.
8
+ module Batches
9
+ include RubyLLM::Batch::Helpers
10
+
11
+ def create_batch(requests)
12
+ requests = requests.map { |request| { custom_id: request[:custom_id], params: request[:payload] } }
13
+ response = @connection.post batches_url, { requests: requests }, idempotent: false
14
+ parse_batch_response response.body
15
+ end
16
+
17
+ def find_batch(id)
18
+ parse_batch_response @connection.get(batch_url(id)).body
19
+ end
20
+
21
+ def cancel_batch(id)
22
+ parse_batch_response @connection.post("#{batch_url(id)}/cancel", {}).body
23
+ end
24
+
25
+ # Results come back as JSONL in no particular order; each line carries
26
+ # the custom_id (our submission index) to match results to requests.
27
+ def batch_results(id)
28
+ response = @connection.get "#{batch_url(id)}/results"
29
+ response.body.each_line.map { |line| parse_batch_result JSON.parse(line) }
30
+ end
31
+
32
+ private
33
+
34
+ def batches_url
35
+ 'v1/messages/batches'
36
+ end
37
+
38
+ def batch_url(id)
39
+ "v1/messages/batches/#{id}"
40
+ end
41
+
42
+ def parse_batch_response(data)
43
+ request_counts = data['request_counts']
44
+
45
+ {
46
+ id: data['id'],
47
+ raw_status: data['processing_status'],
48
+ completed: data['processing_status'] == 'ended',
49
+ request_counts:,
50
+ request_count: request_counts&.values&.sum
51
+ }
52
+ end
53
+
54
+ def parse_batch_status(raw_status, completed:)
55
+ completed && raw_status == 'ended' ? :succeeded : :pending
56
+ end
57
+
58
+ def parse_batch_result(line)
59
+ index = batch_result_index(line['custom_id'])
60
+ result = line['result']
61
+
62
+ if result['type'] == 'succeeded'
63
+ body = result['message']
64
+ [index, parse_completion_body(body, raw: body)]
65
+ else
66
+ status = batch_failure(line['custom_id'], result.dig('error', 'error', 'message'), status: result['type'])
67
+ [index, nil, status]
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # Chat methods for the Anthropic API implementation
7
+ module Chat
8
+ FINISH_REASONS = {
9
+ 'end_turn' => :stop, 'stop_sequence' => :stop, 'max_tokens' => :max_tokens,
10
+ 'model_context_window_exceeded' => :max_tokens, 'tool_use' => :tool_calls, 'refusal' => :content_filter
11
+ }.freeze
12
+
13
+ ANTHROPIC_INLINE_REQUEST_LIMIT = 24 * 1024 * 1024
14
+ ANTHROPIC_FILE_UPLOAD_LIMIT = 500 * 1024 * 1024
15
+ CACHE_CONTROL_TYPE = 'ephemeral'
16
+ PROMPT_CACHE_OPTIONS = %i[ttl].freeze
17
+ BETA_HEADER = 'anthropic-beta'
18
+ COMPACTION_BETA = 'compact-2026-01-12'
19
+ COMPACTION_EDIT_TYPE = 'compact_20260112'
20
+ COUNT_TOKENS_KEYS = %i[model messages system tools tool_choice thinking].freeze
21
+ THINKING_BLOCK_TYPES = %w[thinking redacted_thinking].freeze
22
+
23
+ module_function
24
+
25
+ def finish_reasons = FINISH_REASONS
26
+
27
+ def normalize_finish_reason(reason)
28
+ return nil if reason.nil?
29
+
30
+ finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
31
+ end
32
+
33
+ def completion_url
34
+ 'v1/messages'
35
+ end
36
+
37
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
38
+ schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
39
+ warn_unsupported_citations(model) if citations && !model.supports?(:citations)
40
+ tool_prefs ||= {}
41
+ system_messages, chat_messages = separate_messages(messages)
42
+ system_content = build_system_content(system_messages, caching:)
43
+
44
+ build_base_payload(chat_messages, model, stream, thinking, citations: citations, caching:,
45
+ max_output_tokens:).tap do |payload|
46
+ add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
47
+ payload[:cache_control] = prompt_cache_control(caching) if caching
48
+ end
49
+ end
50
+
51
+ def count_tokens_url
52
+ 'v1/messages/count_tokens'
53
+ end
54
+
55
+ def render_count_tokens_payload(messages, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil,
56
+ citations: false, caching: nil)
57
+ render_payload(
58
+ messages,
59
+ tools: tools,
60
+ tool_prefs: tool_prefs,
61
+ temperature: nil,
62
+ model: model,
63
+ schema: schema,
64
+ thinking: thinking,
65
+ citations: citations,
66
+ caching: caching
67
+ ).slice(*COUNT_TOKENS_KEYS)
68
+ end
69
+
70
+ def parse_count_tokens_response(response)
71
+ response.body['input_tokens']
72
+ end
73
+
74
+ def warn_unsupported_citations(model)
75
+ RubyLLM.logger.warn(
76
+ "#{model.id} does not support citations according to the model registry. " \
77
+ 'with_citations may have no effect.'
78
+ )
79
+ end
80
+
81
+ def separate_messages(messages)
82
+ messages.partition { |msg| msg.role == :system }
83
+ end
84
+
85
+ def build_system_content(system_messages, caching: nil)
86
+ return [] if system_messages.empty?
87
+
88
+ # Anthropic's `system` parameter accepts an array of text content blocks
89
+ # (each optionally with cache_control); each :system message becomes its
90
+ # own block in the resulting array.
91
+ system_messages.flat_map do |msg|
92
+ blocks = Media.format_content(msg.content, msg.attachments).dup
93
+ cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:) : blocks
94
+ end
95
+ end
96
+
97
+ def build_base_payload(chat_messages, model, stream, thinking, citations: false, caching: nil,
98
+ max_output_tokens: nil)
99
+ payload = {
100
+ model: model.id,
101
+ messages: format_messages(chat_messages, thinking:, citations:, caching:),
102
+ stream: stream,
103
+ max_tokens: max_output_tokens || model.max_output_tokens || 4096
104
+ }
105
+
106
+ add_thinking_fields(payload, thinking, model)
107
+
108
+ payload
109
+ end
110
+
111
+ def format_messages(messages, thinking: nil, citations: false, caching: nil)
112
+ rendered = []
113
+ tool_result_blocks = []
114
+
115
+ messages.each do |msg|
116
+ if msg.tool_result?
117
+ tool_result_blocks << Tools.format_tool_result_block(msg)
118
+ inject_cache_control(tool_result_blocks, caching:) if cache_boundary?(msg, caching:)
119
+ next
120
+ end
121
+
122
+ unless tool_result_blocks.empty?
123
+ rendered << { role: 'user', content: tool_result_blocks }
124
+ tool_result_blocks = []
125
+ end
126
+
127
+ formatted = format_message(msg, thinking:, citations:, caching:)
128
+ rendered << formatted unless formatted[:content].empty?
129
+ end
130
+
131
+ rendered << { role: 'user', content: tool_result_blocks } unless tool_result_blocks.empty?
132
+ rendered
133
+ end
134
+
135
+ def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil)
136
+ if tools.any?
137
+ payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
138
+ unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
139
+ payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
140
+ end
141
+ end
142
+ payload[:system] = system_content unless system_content.empty?
143
+ payload[:temperature] = temperature unless temperature.nil?
144
+ payload[:output_config] = payload.fetch(:output_config, {}).merge(build_output_config(schema)) if schema
145
+ end
146
+
147
+ def apply_end_user(payload, identifier)
148
+ Support::Utils.deep_merge(payload, { metadata: { user_id: identifier } })
149
+ end
150
+
151
+ # Anthropic compacts through a context_management edit. Omitting the
152
+ # trigger leaves the API on its own default threshold; the API
153
+ # rejects an explicit one below its minimum, so RubyLLM passes the
154
+ # value through rather than second-guessing it.
155
+ def apply_compaction(payload, compaction)
156
+ Support::Utils.deep_merge(payload, { context_management: { edits: [compaction_edit(compaction)] } })
157
+ end
158
+
159
+ def compaction_edit(compaction)
160
+ edit = { type: COMPACTION_EDIT_TYPE }
161
+ edit[:trigger] = { type: 'input_tokens', value: compaction[:at] } if compaction[:at]
162
+ edit[:instructions] = compaction[:instructions] if compaction[:instructions]
163
+ edit[:pause_after_compaction] = true if compaction[:pause_after]
164
+ edit
165
+ end
166
+
167
+ def apply_compaction_headers(headers, _compaction)
168
+ headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], COMPACTION_BETA))
169
+ end
170
+
171
+ # The Files API is still a beta, so a request that references an
172
+ # uploaded file has to carry its beta header too.
173
+ def apply_files_beta(headers, payload)
174
+ return headers unless provider_file_source?(payload)
175
+
176
+ headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], Files::BETA_HEADER))
177
+ end
178
+
179
+ def provider_file_source?(payload)
180
+ blocks = Array(payload[:messages]).flat_map { |message| Array(message[:content]) }
181
+ blocks.concat(Array(payload[:system]))
182
+ blocks.any? { |block| block.is_a?(Hash) && block[:source].is_a?(Hash) && block[:source][:type] == 'file' }
183
+ end
184
+
185
+ # Anthropic takes several betas as one comma-separated header, so an
186
+ # added beta joins whatever a server tool or with_headers already set.
187
+ def join_betas(existing, beta)
188
+ (existing.to_s.split(',').map(&:strip).reject(&:empty?) + [beta]).uniq.join(',')
189
+ end
190
+
191
+ def supports_provider_file_references?
192
+ true
193
+ end
194
+
195
+ def default_large_file_upload_threshold
196
+ ANTHROPIC_INLINE_REQUEST_LIMIT
197
+ end
198
+
199
+ def provider_file_upload_limit
200
+ ANTHROPIC_FILE_UPLOAD_LIMIT
201
+ end
202
+
203
+ def provider_file_attachable?(attachment)
204
+ attachment.image? || attachment.pdf? || attachment.text?
205
+ end
206
+
207
+ def build_output_config(schema)
208
+ normalized = RubyLLM::Support::Utils.deep_dup(schema[:schema])
209
+ normalized.delete(:strict)
210
+ normalized.delete('strict')
211
+ { format: { type: 'json_schema', schema: normalized } }
212
+ end
213
+
214
+ def parse_completion_body(data, raw:)
215
+ content_blocks = data['content'] || []
216
+
217
+ text_content, citations = extract_text_and_citations(content_blocks)
218
+ thinking_content = extract_thinking_content(content_blocks)
219
+ thinking_signature = extract_thinking_signature(content_blocks)
220
+ tool_use_blocks = Tools.find_tool_uses(content_blocks)
221
+ server_tool_calls = extract_server_tool_calls(content_blocks)
222
+
223
+ build_message(data, content: text_content, citations:, thinking: thinking_content,
224
+ thinking_signature:, tool_use_blocks:, server_tool_calls:,
225
+ raw_content: server_tool_calls.any? ? content_blocks : nil, raw:)
226
+ end
227
+
228
+ # Any block that is not text, thinking, or a function tool_use is a
229
+ # provider-executed tool step. Matching by shape rather than by an
230
+ # allowlist keeps tools Anthropic ships later flowing through.
231
+ def server_tool_block?(block)
232
+ type = block['type'].to_s
233
+ type == 'server_tool_use' || type == 'mcp_tool_use' || type == 'compaction' ||
234
+ type.end_with?('_tool_result')
235
+ end
236
+
237
+ def extract_server_tool_calls(blocks)
238
+ blocks.select { |block| server_tool_block?(block) }.map do |block|
239
+ ServerToolCall.new(
240
+ type: block['type'],
241
+ name: block['name'],
242
+ id: block['id'] || block['tool_use_id'],
243
+ input: block['input'],
244
+ result: block['content'],
245
+ raw: block
246
+ )
247
+ end
248
+ end
249
+
250
+ def extract_text_and_citations(blocks)
251
+ text = +''
252
+ citations = []
253
+
254
+ blocks.each do |block|
255
+ next unless block['type'] == 'text'
256
+
257
+ block_text = block['text'].to_s
258
+ Array(block['citations']).each do |citation|
259
+ citations << parse_citation(citation, text: block_text,
260
+ start_index: text.length,
261
+ end_index: text.length + block_text.length)
262
+ end
263
+ text << block_text
264
+ end
265
+
266
+ [text, citations]
267
+ end
268
+
269
+ def parse_citation(data, text: nil, start_index: nil, end_index: nil)
270
+ end_page = data['end_page_number']
271
+
272
+ Citation.new(
273
+ url: citation_url(data),
274
+ title: data['document_title'] || data['title'],
275
+ cited_text: data['cited_text'],
276
+ text: text,
277
+ start_index: start_index,
278
+ end_index: end_index,
279
+ source_index: data['document_index'] || data['search_result_index'],
280
+ start_page: data['start_page_number'],
281
+ end_page: end_page && (end_page - 1)
282
+ )
283
+ end
284
+
285
+ # Search result citations carry the developer-provided source string.
286
+ def citation_url(data)
287
+ url = data['url'] || data['source']
288
+ url if url&.match?(%r{\Ahttps?://}i)
289
+ end
290
+
291
+ # An empty thinking text is a real block whose display was omitted;
292
+ # collapsing it to nil would replay the signature as redacted data.
293
+ def extract_thinking_content(blocks)
294
+ thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
295
+ return nil if thinking_blocks.empty?
296
+
297
+ thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
298
+ end
299
+
300
+ def extract_thinking_signature(blocks)
301
+ thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
302
+ blocks.find { |c| c['type'] == 'redacted_thinking' }
303
+ thinking_block&.dig('signature') || thinking_block&.dig('data')
304
+ end
305
+
306
+ def parse_thinking_blocks(blocks)
307
+ thinking = blocks.select { |block| THINKING_BLOCK_TYPES.include?(block['type']) }
308
+ { 'anthropic' => thinking } unless thinking.empty?
309
+ end
310
+
311
+ def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
312
+ server_tool_calls: [], raw_content: nil)
313
+ usage = aggregate_usage(data['usage'])
314
+ thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
315
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
316
+ usage['thinking_tokens'] ||
317
+ usage['reasoning_tokens']
318
+
319
+ Message.new(
320
+ role: :assistant,
321
+ content: content,
322
+ citations: citations,
323
+ thinking: Thinking.build(text: thinking, signature: thinking_signature),
324
+ raw_reasoning: parse_thinking_blocks(data['content'] || []),
325
+ tool_calls: Tools.parse_tool_calls(tool_use_blocks),
326
+ server_tool_calls: server_tool_calls,
327
+ raw_content: raw_content,
328
+ input_tokens: usage['input_tokens'],
329
+ output_tokens: usage['output_tokens'],
330
+ cache_read_tokens: extract_cache_read_tokens(data),
331
+ cache_write_tokens: extract_cache_write_tokens(data),
332
+ thinking_tokens: thinking_tokens,
333
+ server_tool_use: usage['server_tool_use'],
334
+ finish_reason: normalize_finish_reason(data['stop_reason']),
335
+ model: data['model'],
336
+ raw: raw
337
+ )
338
+ end
339
+
340
+ # Stored thinking blocks replay whether or not this request asks for
341
+ # thinking: Claude emits them on its own and requires them back on
342
+ # tool-use turns.
343
+ def format_message(msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument
344
+ if msg.role == :assistant && msg.raw_content
345
+ format_raw_assistant_message(msg, caching:)
346
+ elsif msg.tool_call?
347
+ format_tool_call_with_thinking(msg, caching:)
348
+ elsif msg.tool_result?
349
+ Tools.format_tool_result(msg)
350
+ else
351
+ format_basic_message_with_thinking(msg, citations: citations, caching:)
352
+ end
353
+ end
354
+
355
+ # Turns that used server tools replay their provider-shaped blocks
356
+ # verbatim: the API requires the tool_use/result blocks and their
357
+ # citations back exactly as returned.
358
+ def format_raw_assistant_message(msg, caching: nil)
359
+ blocks = msg.raw_content.dup
360
+ inject_cache_control(blocks, caching:) if cache_boundary?(msg, caching:)
361
+
362
+ { role: 'assistant', content: blocks }
363
+ end
364
+
365
+ def format_basic_message_with_thinking(msg, citations: false, caching: nil)
366
+ content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
367
+
368
+ append_formatted_content(content_blocks, msg, citations: citations)
369
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
370
+
371
+ {
372
+ role: convert_role(msg.role),
373
+ content: content_blocks
374
+ }
375
+ end
376
+
377
+ def format_tool_call_with_thinking(msg, caching: nil)
378
+ content_blocks = prepend_thinking_blocks([], msg)
379
+ append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
380
+
381
+ msg.tool_calls.each_value do |tool_call|
382
+ content_blocks << {
383
+ type: 'tool_use',
384
+ id: tool_call.id,
385
+ name: tool_call.name,
386
+ input: tool_call.arguments
387
+ }
388
+ end
389
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
390
+
391
+ {
392
+ role: 'assistant',
393
+ content: content_blocks
394
+ }
395
+ end
396
+
397
+ def prepend_thinking_blocks(content_blocks, msg)
398
+ content_blocks.unshift(*format_thinking_blocks(msg))
399
+
400
+ content_blocks
401
+ end
402
+
403
+ def format_thinking_blocks(msg)
404
+ blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
405
+ return Support::Utils.deep_dup(blocks) if blocks
406
+
407
+ [build_thinking_block(msg.thinking)].compact
408
+ end
409
+
410
+ def build_thinking_block(thinking)
411
+ return nil unless thinking
412
+
413
+ if thinking.text
414
+ {
415
+ type: 'thinking',
416
+ thinking: thinking.text,
417
+ signature: thinking.signature
418
+ }.compact
419
+ elsif thinking.signature
420
+ {
421
+ type: 'redacted_thinking',
422
+ data: thinking.signature
423
+ }
424
+ end
425
+ end
426
+
427
+ def append_formatted_content(content_blocks, msg, citations: false)
428
+ content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
429
+ end
430
+
431
+ def cache_boundary?(message, caching: nil)
432
+ caching != false && message.cache_until_here?
433
+ end
434
+
435
+ def inject_cache_control(blocks, caching: nil)
436
+ return blocks if blocks.empty?
437
+
438
+ last = blocks.last
439
+ return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
440
+ return blocks unless last.is_a?(Hash)
441
+
442
+ blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
443
+ blocks
444
+ end
445
+
446
+ def prompt_cache_control(caching = nil)
447
+ options = prompt_cache_options(caching)
448
+
449
+ { type: CACHE_CONTROL_TYPE }.tap do |control|
450
+ control[:ttl] = options[:ttl] if options[:ttl]
451
+ end
452
+ end
453
+
454
+ def prompt_cache_options(caching)
455
+ return {} unless caching
456
+
457
+ options = caching.to_h.transform_keys(&:to_sym)
458
+ unsupported = options.keys - PROMPT_CACHE_OPTIONS
459
+ return options if unsupported.empty?
460
+
461
+ raise ArgumentError,
462
+ "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
463
+ end
464
+
465
+ def format_cache_option_keys(keys)
466
+ keys.map { |key| ":#{key}" }.join(', ')
467
+ end
468
+
469
+ def convert_role(role)
470
+ case role
471
+ when :tool, :user then 'user'
472
+ else 'assistant'
473
+ end
474
+ end
475
+
476
+ def add_thinking_fields(payload, thinking, model)
477
+ thinking_payload = build_thinking_payload(thinking, model, payload[:max_tokens])
478
+ return unless thinking_payload
479
+
480
+ payload[:thinking] = thinking_payload[:thinking] if thinking_payload[:thinking]
481
+ return unless thinking_payload[:output_config]
482
+
483
+ payload[:output_config] = payload.fetch(:output_config, {}).merge(thinking_payload[:output_config])
484
+ end
485
+
486
+ def build_thinking_payload(thinking, model, max_tokens)
487
+ return nil unless thinking&.enabled?
488
+ return { thinking: { type: 'disabled' } } if thinking.enabled == false
489
+
490
+ effort = resolve_effort(thinking)
491
+ return nil if effort == 'none'
492
+
493
+ payload = {}
494
+ mode = thinking_mode(thinking, model, effort, max_tokens)
495
+ payload[:thinking] = mode if mode
496
+ payload[:output_config] = { effort: effort } if effort
497
+ payload
498
+ end
499
+
500
+ # Effort alone never turns thinking on; Claude needs a thinking block.
501
+ # Generations that take a budget get one sized from the effort with
502
+ # the levels Bedrock publishes. Generations without a budget option
503
+ # think adaptively.
504
+ def thinking_mode(thinking, model, effort, max_tokens)
505
+ return { type: 'adaptive' } if thinking.enabled == true
506
+
507
+ budget = thinking.budget || effort_budget(effort, model, max_tokens)
508
+ mode = if budget
509
+ { type: 'enabled', budget_tokens: budget }
510
+ elsif adaptive_thinking?(thinking, effort, model)
511
+ { type: 'adaptive' }
512
+ end
513
+ mode[:display] = thinking.display.to_s if mode && thinking.display
514
+ mode
515
+ end
516
+
517
+ EFFORT_BUDGETS = { 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze
518
+
519
+ def adaptive_thinking?(thinking, effort, model)
520
+ return true if thinking.display
521
+ return false unless effort
522
+
523
+ model.reasoning_option(:effort) && !model.reasoning_option(:budget_tokens)
524
+ end
525
+
526
+ def effort_budget(effort, model, max_tokens)
527
+ return nil unless effort && model.reasoning_option(:budget_tokens)
528
+
529
+ budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
530
+ minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
531
+ return [budget, minimum].max unless max_tokens
532
+
533
+ budget.clamp(minimum, [max_tokens - 1, minimum].max)
534
+ end
535
+
536
+ def resolve_effort(thinking)
537
+ effort = thinking.effort.to_s
538
+ effort.empty? ? nil : effort
539
+ end
540
+ end
541
+ end
542
+ end
543
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # Embeddings methods of the Anthropic API integration
7
+ module Embeddings
8
+ def embed(...)
9
+ raise Error, "Anthropic doesn't support embeddings"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # Anthropic beta Files API.
7
+ class Files < Protocols::Files
8
+ BETA_HEADER = 'files-api-2025-04-14'
9
+
10
+ private
11
+
12
+ def files_url
13
+ 'v1/files'
14
+ end
15
+
16
+ def upload_headers(request)
17
+ file_headers(request)
18
+ end
19
+
20
+ def file_headers(request)
21
+ request.headers['anthropic-beta'] = BETA_HEADER
22
+ end
23
+
24
+ def parse_file_response(data)
25
+ uploaded_file(
26
+ data,
27
+ id: data['id'],
28
+ filename: data['filename'],
29
+ byte_size: data['size_bytes'],
30
+ created_at: timestamp(data['created_at']),
31
+ mime_type: data['mime_type'],
32
+ downloadable: data['downloadable']
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end