ruby_llm 1.16.0 → 2.0.0.rc1

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 (474) 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 -6
  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 +25 -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 +100 -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 +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -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 +71172 -33253
  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 +540 -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 +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -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 +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +560 -128
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  301. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +16 -4
  330. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +15 -5
  381. data/lib/ruby_llm/railtie.rb +7 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +138 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +380 -6
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +338 -97
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -41
  432. data/lib/ruby_llm/connection.rb +0 -159
  433. data/lib/ruby_llm/content.rb +0 -91
  434. data/lib/ruby_llm/deprecator.rb +0 -24
  435. data/lib/ruby_llm/error_middleware.rb +0 -81
  436. data/lib/ruby_llm/instrumentation.rb +0 -36
  437. data/lib/ruby_llm/mime_type.rb +0 -96
  438. data/lib/ruby_llm/model/info.rb +0 -164
  439. data/lib/ruby_llm/model_registry.rb +0 -39
  440. data/lib/ruby_llm/models_schema.json +0 -171
  441. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  442. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  443. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  444. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  445. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  446. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  447. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  448. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  449. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  450. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  451. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  452. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  453. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  454. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  455. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  456. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  457. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  459. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  460. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  461. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  462. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  463. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  464. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  465. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  466. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  467. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  468. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  469. data/lib/ruby_llm/streaming.rb +0 -179
  470. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  471. data/lib/ruby_llm/utils.rb +0 -130
  472. data/lib/tasks/models.rake +0 -593
  473. data/lib/tasks/release.rake +0 -94
  474. 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,540 @@
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
+
22
+ module_function
23
+
24
+ def finish_reasons = FINISH_REASONS
25
+
26
+ def normalize_finish_reason(reason)
27
+ return nil if reason.nil?
28
+
29
+ finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
30
+ end
31
+
32
+ def completion_url
33
+ 'v1/messages'
34
+ end
35
+
36
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
37
+ schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
38
+ warn_unsupported_citations(model) if citations && !model.supports?(:citations)
39
+ tool_prefs ||= {}
40
+ system_messages, chat_messages = separate_messages(messages)
41
+ explicit_boundaries = cache_boundaries?(messages, caching:)
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 && !explicit_boundaries
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 build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
307
+ server_tool_calls: [], raw_content: nil)
308
+ usage = aggregate_usage(data['usage'])
309
+ thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
310
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
311
+ usage['thinking_tokens'] ||
312
+ usage['reasoning_tokens']
313
+
314
+ Message.new(
315
+ role: :assistant,
316
+ content: content,
317
+ citations: citations,
318
+ thinking: Thinking.build(text: thinking, signature: thinking_signature),
319
+ tool_calls: Tools.parse_tool_calls(tool_use_blocks),
320
+ server_tool_calls: server_tool_calls,
321
+ raw_content: raw_content,
322
+ input_tokens: usage['input_tokens'],
323
+ output_tokens: usage['output_tokens'],
324
+ cache_read_tokens: extract_cache_read_tokens(data),
325
+ cache_write_tokens: extract_cache_write_tokens(data),
326
+ thinking_tokens: thinking_tokens,
327
+ server_tool_use: usage['server_tool_use'],
328
+ finish_reason: normalize_finish_reason(data['stop_reason']),
329
+ model: data['model'],
330
+ raw: raw
331
+ )
332
+ end
333
+
334
+ # Stored thinking blocks replay whether or not this request asks for
335
+ # thinking: Claude emits them on its own and requires them back on
336
+ # tool-use turns.
337
+ def format_message(msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument
338
+ if msg.role == :assistant && msg.raw_content
339
+ format_raw_assistant_message(msg, caching:)
340
+ elsif msg.tool_call?
341
+ format_tool_call_with_thinking(msg, caching:)
342
+ elsif msg.tool_result?
343
+ Tools.format_tool_result(msg)
344
+ else
345
+ format_basic_message_with_thinking(msg, citations: citations, caching:)
346
+ end
347
+ end
348
+
349
+ # Turns that used server tools replay their provider-shaped blocks
350
+ # verbatim: the API requires the tool_use/result blocks and their
351
+ # citations back exactly as returned.
352
+ def format_raw_assistant_message(msg, caching: nil)
353
+ blocks = msg.raw_content.dup
354
+ inject_cache_control(blocks, caching:) if cache_boundary?(msg, caching:)
355
+
356
+ { role: 'assistant', content: blocks }
357
+ end
358
+
359
+ def format_basic_message_with_thinking(msg, citations: false, caching: nil)
360
+ content_blocks = []
361
+
362
+ if msg.role == :assistant
363
+ thinking_block = build_thinking_block(msg.thinking)
364
+ content_blocks << thinking_block if thinking_block
365
+ end
366
+
367
+ append_formatted_content(content_blocks, msg, citations: citations)
368
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
369
+
370
+ {
371
+ role: convert_role(msg.role),
372
+ content: content_blocks
373
+ }
374
+ end
375
+
376
+ def format_tool_call_with_thinking(msg, caching: nil)
377
+ content_blocks = prepend_thinking_block([], msg)
378
+ append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
379
+
380
+ msg.tool_calls.each_value do |tool_call|
381
+ content_blocks << {
382
+ type: 'tool_use',
383
+ id: tool_call.id,
384
+ name: tool_call.name,
385
+ input: tool_call.arguments
386
+ }
387
+ end
388
+ inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
389
+
390
+ {
391
+ role: 'assistant',
392
+ content: content_blocks
393
+ }
394
+ end
395
+
396
+ def prepend_thinking_block(content_blocks, msg)
397
+ thinking_block = build_thinking_block(msg.thinking)
398
+ content_blocks.unshift(thinking_block) if thinking_block
399
+
400
+ content_blocks
401
+ end
402
+
403
+ def build_thinking_block(thinking)
404
+ return nil unless thinking
405
+
406
+ if thinking.text
407
+ {
408
+ type: 'thinking',
409
+ thinking: thinking.text,
410
+ signature: thinking.signature
411
+ }.compact
412
+ elsif thinking.signature
413
+ {
414
+ type: 'redacted_thinking',
415
+ data: thinking.signature
416
+ }
417
+ end
418
+ end
419
+
420
+ def append_formatted_content(content_blocks, msg, citations: false)
421
+ content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
422
+ end
423
+
424
+ def cache_boundaries?(messages, caching: nil)
425
+ caching != false && messages.any?(&:cache_until_here?)
426
+ end
427
+
428
+ def cache_boundary?(message, caching: nil)
429
+ caching != false && message.cache_until_here?
430
+ end
431
+
432
+ def inject_cache_control(blocks, caching: nil)
433
+ return blocks if blocks.empty?
434
+
435
+ last = blocks.last
436
+ return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
437
+ return blocks unless last.is_a?(Hash)
438
+
439
+ blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
440
+ blocks
441
+ end
442
+
443
+ def prompt_cache_control(caching = nil)
444
+ options = prompt_cache_options(caching)
445
+
446
+ { type: CACHE_CONTROL_TYPE }.tap do |control|
447
+ control[:ttl] = options[:ttl] if options[:ttl]
448
+ end
449
+ end
450
+
451
+ def prompt_cache_options(caching)
452
+ return {} unless caching
453
+
454
+ options = caching.to_h.transform_keys(&:to_sym)
455
+ unsupported = options.keys - PROMPT_CACHE_OPTIONS
456
+ return options if unsupported.empty?
457
+
458
+ raise ArgumentError,
459
+ "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
460
+ end
461
+
462
+ def format_cache_option_keys(keys)
463
+ keys.map { |key| ":#{key}" }.join(', ')
464
+ end
465
+
466
+ def convert_role(role)
467
+ case role
468
+ when :tool, :user then 'user'
469
+ else 'assistant'
470
+ end
471
+ end
472
+
473
+ def add_thinking_fields(payload, thinking, model)
474
+ thinking_payload = build_thinking_payload(thinking, model, payload[:max_tokens])
475
+ return unless thinking_payload
476
+
477
+ payload[:thinking] = thinking_payload[:thinking] if thinking_payload[:thinking]
478
+ return unless thinking_payload[:output_config]
479
+
480
+ payload[:output_config] = payload.fetch(:output_config, {}).merge(thinking_payload[:output_config])
481
+ end
482
+
483
+ def build_thinking_payload(thinking, model, max_tokens)
484
+ return nil unless thinking&.enabled?
485
+ return { thinking: { type: 'disabled' } } if thinking.enabled == false
486
+
487
+ effort = resolve_effort(thinking)
488
+ return nil if effort == 'none'
489
+
490
+ payload = {}
491
+ mode = thinking_mode(thinking, model, effort, max_tokens)
492
+ payload[:thinking] = mode if mode
493
+ payload[:output_config] = { effort: effort } if effort
494
+ payload
495
+ end
496
+
497
+ # Effort alone never turns thinking on; Claude needs a thinking block.
498
+ # Generations that take a budget get one sized from the effort with
499
+ # the levels Bedrock publishes. Generations without a budget option
500
+ # think adaptively.
501
+ def thinking_mode(thinking, model, effort, max_tokens)
502
+ return { type: 'adaptive' } if thinking.enabled == true
503
+
504
+ budget = thinking.budget || effort_budget(effort, model, max_tokens)
505
+ mode = if budget
506
+ { type: 'enabled', budget_tokens: budget }
507
+ elsif adaptive_thinking?(thinking, effort, model)
508
+ { type: 'adaptive' }
509
+ end
510
+ mode[:display] = thinking.display.to_s if mode && thinking.display
511
+ mode
512
+ end
513
+
514
+ EFFORT_BUDGETS = { 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze
515
+
516
+ def adaptive_thinking?(thinking, effort, model)
517
+ return true if thinking.display
518
+ return false unless effort
519
+
520
+ model.reasoning_option(:effort) && !model.reasoning_option(:budget_tokens)
521
+ end
522
+
523
+ def effort_budget(effort, model, max_tokens)
524
+ return nil unless effort && model.reasoning_option(:budget_tokens)
525
+
526
+ budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
527
+ minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
528
+ return [budget, minimum].max unless max_tokens
529
+
530
+ budget.clamp(minimum, [max_tokens - 1, minimum].max)
531
+ end
532
+
533
+ def resolve_effort(thinking)
534
+ effort = thinking.effort.to_s
535
+ effort.empty? ? nil : effort
536
+ end
537
+ end
538
+ end
539
+ end
540
+ 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