ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,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,548 @@
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
+ explicit_boundaries = cache_boundaries?(messages, caching:)
43
+ system_content = build_system_content(system_messages, caching:)
44
+
45
+ build_base_payload(chat_messages, model, stream, thinking, citations: citations, caching:,
46
+ max_output_tokens:).tap do |payload|
47
+ add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
48
+ payload[:cache_control] = prompt_cache_control(caching) if caching && !explicit_boundaries
49
+ end
50
+ end
51
+
52
+ def count_tokens_url
53
+ 'v1/messages/count_tokens'
54
+ end
55
+
56
+ def render_count_tokens_payload(messages, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil,
57
+ citations: false, caching: nil)
58
+ render_payload(
59
+ messages,
60
+ tools: tools,
61
+ tool_prefs: tool_prefs,
62
+ temperature: nil,
63
+ model: model,
64
+ schema: schema,
65
+ thinking: thinking,
66
+ citations: citations,
67
+ caching: caching
68
+ ).slice(*COUNT_TOKENS_KEYS)
69
+ end
70
+
71
+ def parse_count_tokens_response(response)
72
+ response.body['input_tokens']
73
+ end
74
+
75
+ def warn_unsupported_citations(model)
76
+ RubyLLM.logger.warn(
77
+ "#{model.id} does not support citations according to the model registry. " \
78
+ 'with_citations may have no effect.'
79
+ )
80
+ end
81
+
82
+ def separate_messages(messages)
83
+ messages.partition { |msg| msg.role == :system }
84
+ end
85
+
86
+ def build_system_content(system_messages, caching: nil)
87
+ return [] if system_messages.empty?
88
+
89
+ # Anthropic's `system` parameter accepts an array of text content blocks
90
+ # (each optionally with cache_control); each :system message becomes its
91
+ # own block in the resulting array.
92
+ system_messages.flat_map do |msg|
93
+ blocks = Media.format_content(msg.content, msg.attachments).dup
94
+ cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:) : blocks
95
+ end
96
+ end
97
+
98
+ def build_base_payload(chat_messages, model, stream, thinking, citations: false, caching: nil,
99
+ max_output_tokens: nil)
100
+ payload = {
101
+ model: model.id,
102
+ messages: format_messages(chat_messages, thinking:, citations:, caching:),
103
+ stream: stream,
104
+ max_tokens: max_output_tokens || model.max_output_tokens || 4096
105
+ }
106
+
107
+ add_thinking_fields(payload, thinking, model)
108
+
109
+ payload
110
+ end
111
+
112
+ def format_messages(messages, thinking: nil, citations: false, caching: nil)
113
+ rendered = []
114
+ tool_result_blocks = []
115
+
116
+ messages.each do |msg|
117
+ if msg.tool_result?
118
+ tool_result_blocks << Tools.format_tool_result_block(msg)
119
+ inject_cache_control(tool_result_blocks, caching:) if cache_boundary?(msg, caching:)
120
+ next
121
+ end
122
+
123
+ unless tool_result_blocks.empty?
124
+ rendered << { role: 'user', content: tool_result_blocks }
125
+ tool_result_blocks = []
126
+ end
127
+
128
+ formatted = format_message(msg, thinking:, citations:, caching:)
129
+ rendered << formatted unless formatted[:content].empty?
130
+ end
131
+
132
+ rendered << { role: 'user', content: tool_result_blocks } unless tool_result_blocks.empty?
133
+ rendered
134
+ end
135
+
136
+ def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil)
137
+ if tools.any?
138
+ payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
139
+ unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
140
+ payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
141
+ end
142
+ end
143
+ payload[:system] = system_content unless system_content.empty?
144
+ payload[:temperature] = temperature unless temperature.nil?
145
+ payload[:output_config] = payload.fetch(:output_config, {}).merge(build_output_config(schema)) if schema
146
+ end
147
+
148
+ def apply_end_user(payload, identifier)
149
+ Support::Utils.deep_merge(payload, { metadata: { user_id: identifier } })
150
+ end
151
+
152
+ # Anthropic compacts through a context_management edit. Omitting the
153
+ # trigger leaves the API on its own default threshold; the API
154
+ # rejects an explicit one below its minimum, so RubyLLM passes the
155
+ # value through rather than second-guessing it.
156
+ def apply_compaction(payload, compaction)
157
+ Support::Utils.deep_merge(payload, { context_management: { edits: [compaction_edit(compaction)] } })
158
+ end
159
+
160
+ def compaction_edit(compaction)
161
+ edit = { type: COMPACTION_EDIT_TYPE }
162
+ edit[:trigger] = { type: 'input_tokens', value: compaction[:at] } if compaction[:at]
163
+ edit[:instructions] = compaction[:instructions] if compaction[:instructions]
164
+ edit[:pause_after_compaction] = true if compaction[:pause_after]
165
+ edit
166
+ end
167
+
168
+ def apply_compaction_headers(headers, _compaction)
169
+ headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], COMPACTION_BETA))
170
+ end
171
+
172
+ # The Files API is still a beta, so a request that references an
173
+ # uploaded file has to carry its beta header too.
174
+ def apply_files_beta(headers, payload)
175
+ return headers unless provider_file_source?(payload)
176
+
177
+ headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], Files::BETA_HEADER))
178
+ end
179
+
180
+ def provider_file_source?(payload)
181
+ blocks = Array(payload[:messages]).flat_map { |message| Array(message[:content]) }
182
+ blocks.concat(Array(payload[:system]))
183
+ blocks.any? { |block| block.is_a?(Hash) && block[:source].is_a?(Hash) && block[:source][:type] == 'file' }
184
+ end
185
+
186
+ # Anthropic takes several betas as one comma-separated header, so an
187
+ # added beta joins whatever a server tool or with_headers already set.
188
+ def join_betas(existing, beta)
189
+ (existing.to_s.split(',').map(&:strip).reject(&:empty?) + [beta]).uniq.join(',')
190
+ end
191
+
192
+ def supports_provider_file_references?
193
+ true
194
+ end
195
+
196
+ def default_large_file_upload_threshold
197
+ ANTHROPIC_INLINE_REQUEST_LIMIT
198
+ end
199
+
200
+ def provider_file_upload_limit
201
+ ANTHROPIC_FILE_UPLOAD_LIMIT
202
+ end
203
+
204
+ def provider_file_attachable?(attachment)
205
+ attachment.image? || attachment.pdf? || attachment.text?
206
+ end
207
+
208
+ def build_output_config(schema)
209
+ normalized = RubyLLM::Support::Utils.deep_dup(schema[:schema])
210
+ normalized.delete(:strict)
211
+ normalized.delete('strict')
212
+ { format: { type: 'json_schema', schema: normalized } }
213
+ end
214
+
215
+ def parse_completion_body(data, raw:)
216
+ content_blocks = data['content'] || []
217
+
218
+ text_content, citations = extract_text_and_citations(content_blocks)
219
+ thinking_content = extract_thinking_content(content_blocks)
220
+ thinking_signature = extract_thinking_signature(content_blocks)
221
+ tool_use_blocks = Tools.find_tool_uses(content_blocks)
222
+ server_tool_calls = extract_server_tool_calls(content_blocks)
223
+
224
+ build_message(data, content: text_content, citations:, thinking: thinking_content,
225
+ thinking_signature:, tool_use_blocks:, server_tool_calls:,
226
+ raw_content: server_tool_calls.any? ? content_blocks : nil, raw:)
227
+ end
228
+
229
+ # Any block that is not text, thinking, or a function tool_use is a
230
+ # provider-executed tool step. Matching by shape rather than by an
231
+ # allowlist keeps tools Anthropic ships later flowing through.
232
+ def server_tool_block?(block)
233
+ type = block['type'].to_s
234
+ type == 'server_tool_use' || type == 'mcp_tool_use' || type == 'compaction' ||
235
+ type.end_with?('_tool_result')
236
+ end
237
+
238
+ def extract_server_tool_calls(blocks)
239
+ blocks.select { |block| server_tool_block?(block) }.map do |block|
240
+ ServerToolCall.new(
241
+ type: block['type'],
242
+ name: block['name'],
243
+ id: block['id'] || block['tool_use_id'],
244
+ input: block['input'],
245
+ result: block['content'],
246
+ raw: block
247
+ )
248
+ end
249
+ end
250
+
251
+ def extract_text_and_citations(blocks)
252
+ text = +''
253
+ citations = []
254
+
255
+ blocks.each do |block|
256
+ next unless block['type'] == 'text'
257
+
258
+ block_text = block['text'].to_s
259
+ Array(block['citations']).each do |citation|
260
+ citations << parse_citation(citation, text: block_text,
261
+ start_index: text.length,
262
+ end_index: text.length + block_text.length)
263
+ end
264
+ text << block_text
265
+ end
266
+
267
+ [text, citations]
268
+ end
269
+
270
+ def parse_citation(data, text: nil, start_index: nil, end_index: nil)
271
+ end_page = data['end_page_number']
272
+
273
+ Citation.new(
274
+ url: citation_url(data),
275
+ title: data['document_title'] || data['title'],
276
+ cited_text: data['cited_text'],
277
+ text: text,
278
+ start_index: start_index,
279
+ end_index: end_index,
280
+ source_index: data['document_index'] || data['search_result_index'],
281
+ start_page: data['start_page_number'],
282
+ end_page: end_page && (end_page - 1)
283
+ )
284
+ end
285
+
286
+ # Search result citations carry the developer-provided source string.
287
+ def citation_url(data)
288
+ url = data['url'] || data['source']
289
+ url if url&.match?(%r{\Ahttps?://}i)
290
+ end
291
+
292
+ # An empty thinking text is a real block whose display was omitted;
293
+ # collapsing it to nil would replay the signature as redacted data.
294
+ def extract_thinking_content(blocks)
295
+ thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
296
+ return nil if thinking_blocks.empty?
297
+
298
+ thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
299
+ end
300
+
301
+ def extract_thinking_signature(blocks)
302
+ thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
303
+ blocks.find { |c| c['type'] == 'redacted_thinking' }
304
+ thinking_block&.dig('signature') || thinking_block&.dig('data')
305
+ end
306
+
307
+ def parse_thinking_blocks(blocks)
308
+ thinking = blocks.select { |block| THINKING_BLOCK_TYPES.include?(block['type']) }
309
+ { 'anthropic' => thinking } unless thinking.empty?
310
+ end
311
+
312
+ def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
313
+ server_tool_calls: [], raw_content: nil)
314
+ usage = aggregate_usage(data['usage'])
315
+ thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
316
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
317
+ usage['thinking_tokens'] ||
318
+ usage['reasoning_tokens']
319
+
320
+ Message.new(
321
+ role: :assistant,
322
+ content: content,
323
+ citations: citations,
324
+ thinking: Thinking.build(text: thinking, signature: thinking_signature),
325
+ raw_reasoning: parse_thinking_blocks(data['content'] || []),
326
+ tool_calls: Tools.parse_tool_calls(tool_use_blocks),
327
+ server_tool_calls: server_tool_calls,
328
+ raw_content: raw_content,
329
+ input_tokens: usage['input_tokens'],
330
+ output_tokens: usage['output_tokens'],
331
+ cache_read_tokens: extract_cache_read_tokens(data),
332
+ cache_write_tokens: extract_cache_write_tokens(data),
333
+ thinking_tokens: thinking_tokens,
334
+ server_tool_use: usage['server_tool_use'],
335
+ finish_reason: normalize_finish_reason(data['stop_reason']),
336
+ model: data['model'],
337
+ raw: raw
338
+ )
339
+ end
340
+
341
+ # Stored thinking blocks replay whether or not this request asks for
342
+ # thinking: Claude emits them on its own and requires them back on
343
+ # tool-use turns.
344
+ def format_message(msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument
345
+ if msg.role == :assistant && msg.raw_content
346
+ format_raw_assistant_message(msg, caching:)
347
+ elsif msg.tool_call?
348
+ format_tool_call_with_thinking(msg, caching:)
349
+ elsif msg.tool_result?
350
+ Tools.format_tool_result(msg)
351
+ else
352
+ format_basic_message_with_thinking(msg, citations: citations, caching:)
353
+ end
354
+ end
355
+
356
+ # Turns that used server tools replay their provider-shaped blocks
357
+ # verbatim: the API requires the tool_use/result blocks and their
358
+ # citations back exactly as returned.
359
+ def format_raw_assistant_message(msg, caching: nil)
360
+ blocks = msg.raw_content.dup
361
+ inject_cache_control(blocks, caching:) if cache_boundary?(msg, caching:)
362
+
363
+ { role: 'assistant', content: blocks }
364
+ end
365
+
366
+ def format_basic_message_with_thinking(msg, citations: false, caching: nil)
367
+ content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
368
+
369
+ append_formatted_content(content_blocks, msg, citations: citations)
370
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
371
+
372
+ {
373
+ role: convert_role(msg.role),
374
+ content: content_blocks
375
+ }
376
+ end
377
+
378
+ def format_tool_call_with_thinking(msg, caching: nil)
379
+ content_blocks = prepend_thinking_blocks([], msg)
380
+ append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
381
+
382
+ msg.tool_calls.each_value do |tool_call|
383
+ content_blocks << {
384
+ type: 'tool_use',
385
+ id: tool_call.id,
386
+ name: tool_call.name,
387
+ input: tool_call.arguments
388
+ }
389
+ end
390
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
391
+
392
+ {
393
+ role: 'assistant',
394
+ content: content_blocks
395
+ }
396
+ end
397
+
398
+ def prepend_thinking_blocks(content_blocks, msg)
399
+ content_blocks.unshift(*format_thinking_blocks(msg))
400
+
401
+ content_blocks
402
+ end
403
+
404
+ def format_thinking_blocks(msg)
405
+ blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
406
+ return Support::Utils.deep_dup(blocks) if blocks
407
+
408
+ [build_thinking_block(msg.thinking)].compact
409
+ end
410
+
411
+ def build_thinking_block(thinking)
412
+ return nil unless thinking
413
+
414
+ if thinking.text
415
+ {
416
+ type: 'thinking',
417
+ thinking: thinking.text,
418
+ signature: thinking.signature
419
+ }.compact
420
+ elsif thinking.signature
421
+ {
422
+ type: 'redacted_thinking',
423
+ data: thinking.signature
424
+ }
425
+ end
426
+ end
427
+
428
+ def append_formatted_content(content_blocks, msg, citations: false)
429
+ content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
430
+ end
431
+
432
+ def cache_boundaries?(messages, caching: nil)
433
+ caching != false && messages.any?(&:cache_until_here?)
434
+ end
435
+
436
+ def cache_boundary?(message, caching: nil)
437
+ caching != false && message.cache_until_here?
438
+ end
439
+
440
+ def inject_cache_control(blocks, caching: nil)
441
+ return blocks if blocks.empty?
442
+
443
+ last = blocks.last
444
+ return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
445
+ return blocks unless last.is_a?(Hash)
446
+
447
+ blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
448
+ blocks
449
+ end
450
+
451
+ def prompt_cache_control(caching = nil)
452
+ options = prompt_cache_options(caching)
453
+
454
+ { type: CACHE_CONTROL_TYPE }.tap do |control|
455
+ control[:ttl] = options[:ttl] if options[:ttl]
456
+ end
457
+ end
458
+
459
+ def prompt_cache_options(caching)
460
+ return {} unless caching
461
+
462
+ options = caching.to_h.transform_keys(&:to_sym)
463
+ unsupported = options.keys - PROMPT_CACHE_OPTIONS
464
+ return options if unsupported.empty?
465
+
466
+ raise ArgumentError,
467
+ "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
468
+ end
469
+
470
+ def format_cache_option_keys(keys)
471
+ keys.map { |key| ":#{key}" }.join(', ')
472
+ end
473
+
474
+ def convert_role(role)
475
+ case role
476
+ when :tool, :user then 'user'
477
+ else 'assistant'
478
+ end
479
+ end
480
+
481
+ def add_thinking_fields(payload, thinking, model)
482
+ thinking_payload = build_thinking_payload(thinking, model, payload[:max_tokens])
483
+ return unless thinking_payload
484
+
485
+ payload[:thinking] = thinking_payload[:thinking] if thinking_payload[:thinking]
486
+ return unless thinking_payload[:output_config]
487
+
488
+ payload[:output_config] = payload.fetch(:output_config, {}).merge(thinking_payload[:output_config])
489
+ end
490
+
491
+ def build_thinking_payload(thinking, model, max_tokens)
492
+ return nil unless thinking&.enabled?
493
+ return { thinking: { type: 'disabled' } } if thinking.enabled == false
494
+
495
+ effort = resolve_effort(thinking)
496
+ return nil if effort == 'none'
497
+
498
+ payload = {}
499
+ mode = thinking_mode(thinking, model, effort, max_tokens)
500
+ payload[:thinking] = mode if mode
501
+ payload[:output_config] = { effort: effort } if effort
502
+ payload
503
+ end
504
+
505
+ # Effort alone never turns thinking on; Claude needs a thinking block.
506
+ # Generations that take a budget get one sized from the effort with
507
+ # the levels Bedrock publishes. Generations without a budget option
508
+ # think adaptively.
509
+ def thinking_mode(thinking, model, effort, max_tokens)
510
+ return { type: 'adaptive' } if thinking.enabled == true
511
+
512
+ budget = thinking.budget || effort_budget(effort, model, max_tokens)
513
+ mode = if budget
514
+ { type: 'enabled', budget_tokens: budget }
515
+ elsif adaptive_thinking?(thinking, effort, model)
516
+ { type: 'adaptive' }
517
+ end
518
+ mode[:display] = thinking.display.to_s if mode && thinking.display
519
+ mode
520
+ end
521
+
522
+ EFFORT_BUDGETS = { 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze
523
+
524
+ def adaptive_thinking?(thinking, effort, model)
525
+ return true if thinking.display
526
+ return false unless effort
527
+
528
+ model.reasoning_option(:effort) && !model.reasoning_option(:budget_tokens)
529
+ end
530
+
531
+ def effort_budget(effort, model, max_tokens)
532
+ return nil unless effort && model.reasoning_option(:budget_tokens)
533
+
534
+ budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
535
+ minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
536
+ return [budget, minimum].max unless max_tokens
537
+
538
+ budget.clamp(minimum, [max_tokens - 1, minimum].max)
539
+ end
540
+
541
+ def resolve_effort(thinking)
542
+ effort = thinking.effort.to_s
543
+ effort.empty? ? nil : effort
544
+ end
545
+ end
546
+ end
547
+ end
548
+ 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