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,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Anthropic
6
+ # Handles formatting of media content (images, PDFs, audio) for Anthropic
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content, attachments = [], citations: false)
11
+ parts = []
12
+ parts << format_text(content) unless content.nil? || content.empty?
13
+
14
+ attachments.each do |attachment|
15
+ if attachment.provider_file?
16
+ parts << format_provider_file(attachment, citations: citations)
17
+ next
18
+ end
19
+
20
+ case attachment.type
21
+ when :image
22
+ parts << format_image(attachment)
23
+ when :pdf
24
+ parts << format_pdf(attachment, citations: citations)
25
+ when :text
26
+ parts << (citations ? format_text_document(attachment) : format_text_file(attachment))
27
+ else
28
+ raise UnsupportedAttachmentError, attachment.mime_type
29
+ end
30
+ end
31
+
32
+ parts
33
+ end
34
+
35
+ def format_text(text)
36
+ {
37
+ type: 'text',
38
+ text: text
39
+ }
40
+ end
41
+
42
+ def format_image(image)
43
+ if image.url?
44
+ {
45
+ type: 'image',
46
+ source: {
47
+ type: 'url',
48
+ url: image.source.to_s
49
+ }
50
+ }
51
+ else
52
+ {
53
+ type: 'image',
54
+ source: {
55
+ type: 'base64',
56
+ media_type: image.mime_type,
57
+ data: image.encoded
58
+ }
59
+ }
60
+ end
61
+ end
62
+
63
+ def format_provider_file(file, citations: false)
64
+ return format_provider_image(file) if file.image?
65
+
66
+ document = {
67
+ type: 'document',
68
+ source: {
69
+ type: 'file',
70
+ file_id: file.provider_file_id
71
+ }
72
+ }
73
+ enable_citations(document, file) if citations
74
+ document
75
+ end
76
+
77
+ def format_provider_image(image)
78
+ {
79
+ type: 'image',
80
+ source: {
81
+ type: 'file',
82
+ file_id: image.provider_file_id
83
+ }
84
+ }
85
+ end
86
+
87
+ def format_pdf(pdf, citations: false)
88
+ document = {
89
+ type: 'document',
90
+ source: pdf_source(pdf)
91
+ }
92
+
93
+ enable_citations(document, pdf) if citations
94
+ document
95
+ end
96
+
97
+ def pdf_source(pdf)
98
+ if pdf.url?
99
+ {
100
+ type: 'url',
101
+ url: pdf.source.to_s
102
+ }
103
+ else
104
+ {
105
+ type: 'base64',
106
+ media_type: pdf.mime_type,
107
+ data: pdf.encoded
108
+ }
109
+ end
110
+ end
111
+
112
+ def format_text_file(text_file)
113
+ {
114
+ type: 'text',
115
+ text: text_file.for_llm
116
+ }
117
+ end
118
+
119
+ # Text attachments become citable documents when citations are enabled.
120
+ def format_text_document(text_file)
121
+ document = {
122
+ type: 'document',
123
+ source: {
124
+ type: 'text',
125
+ media_type: 'text/plain',
126
+ data: text_file.content
127
+ }
128
+ }
129
+
130
+ enable_citations(document, text_file)
131
+ document
132
+ end
133
+
134
+ def enable_citations(document, attachment)
135
+ document[:title] = attachment.filename if attachment.filename
136
+ document[:citations] = { enabled: true }
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Anthropic
8
+ # Models methods of the Anthropic API integration
9
+ module Models
10
+ ITERATION_TOKEN_KEYS = %w[
11
+ input_tokens output_tokens cache_read_input_tokens cache_creation_input_tokens
12
+ ].freeze
13
+
14
+ REPORTED_CAPABILITIES = {
15
+ 'batch' => 'batch',
16
+ 'citations' => 'citations',
17
+ 'image_input' => 'vision',
18
+ 'structured_outputs' => 'structured_output',
19
+ 'thinking' => 'reasoning'
20
+ }.freeze
21
+
22
+ def list_models
23
+ models = []
24
+ after_id = nil
25
+
26
+ loop do
27
+ response = @connection.get(models_url) do |req|
28
+ req.params = { limit: 1000 }
29
+ req.params[:after_id] = after_id if after_id
30
+ end
31
+
32
+ models.concat(parse_list_models_response(response, @provider.slug))
33
+ break unless response.body['has_more']
34
+
35
+ after_id = response.body['last_id']
36
+ end
37
+
38
+ models
39
+ end
40
+
41
+ private
42
+
43
+ def models_url
44
+ 'v1/models'
45
+ end
46
+
47
+ def parse_list_models_response(response, slug)
48
+ Array(response.body['data']).map do |model_data|
49
+ model_id = model_data['id']
50
+
51
+ Model.new(
52
+ id: model_id,
53
+ name: model_data['display_name'] || model_id,
54
+ provider: slug,
55
+ created_at: Time.parse(model_data['created_at']),
56
+ context_window: model_data['max_input_tokens'],
57
+ max_output_tokens: model_data['max_tokens'],
58
+ capabilities: model_capabilities(model_data),
59
+ metadata: {}
60
+ )
61
+ end
62
+ end
63
+
64
+ def model_capabilities(model_data)
65
+ reported = model_data['capabilities']
66
+ return [] unless reported.is_a?(Hash)
67
+
68
+ reported.filter_map do |name, detail|
69
+ REPORTED_CAPABILITIES[name] if detail.is_a?(Hash) && detail['supported']
70
+ end
71
+ end
72
+
73
+ def extract_model_id(data)
74
+ data.dig('message', 'model')
75
+ end
76
+
77
+ def extract_input_tokens(data)
78
+ message_usage(data)['input_tokens']
79
+ end
80
+
81
+ def extract_output_tokens(data)
82
+ message_usage(data)['output_tokens'] || delta_usage(data)['output_tokens']
83
+ end
84
+
85
+ def extract_thinking_tokens(data)
86
+ message_usage(data).dig('output_tokens_details', 'thinking_tokens') ||
87
+ delta_usage(data).dig('output_tokens_details', 'thinking_tokens')
88
+ end
89
+
90
+ def extract_cache_read_tokens(data)
91
+ message_usage(data)['cache_read_input_tokens'] || delta_usage(data)['cache_read_input_tokens']
92
+ end
93
+
94
+ def extract_cache_write_tokens(data)
95
+ direct = message_usage(data)['cache_creation_input_tokens'] ||
96
+ delta_usage(data)['cache_creation_input_tokens']
97
+ return direct if direct
98
+
99
+ breakdown = message_usage(data)['cache_creation'] || delta_usage(data)['cache_creation']
100
+ return unless breakdown.is_a?(Hash)
101
+
102
+ breakdown.values.compact.sum
103
+ end
104
+
105
+ def message_usage(data)
106
+ aggregate_usage(data.dig('message', 'usage'))
107
+ end
108
+
109
+ def delta_usage(data)
110
+ aggregate_usage(data['usage'])
111
+ end
112
+
113
+ # A compacted turn runs the model more than once, and the top-level
114
+ # counts cover only the final iteration. Everything billed is in
115
+ # usage.iterations, so their sum is what the request cost.
116
+ def aggregate_usage(usage)
117
+ return {} unless usage.is_a?(Hash)
118
+
119
+ iterations = usage['iterations']
120
+ return usage unless iterations.is_a?(Array) && !iterations.empty?
121
+
122
+ usage.merge(ITERATION_TOKEN_KEYS.to_h do |key|
123
+ [key, iterations.sum { |iteration| iteration[key].to_i }]
124
+ end)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Anthropic
8
+ # Streaming methods of the Anthropic API integration
9
+ module Streaming
10
+ private
11
+
12
+ def stream_response(payload, additional_headers = {}, &)
13
+ # Avoid Net::HTTP's auto-inflate: Cloudflare's gzip flushes
14
+ # infrequently for SSE, batching chunks until each flush.
15
+ super(payload, additional_headers.merge('Accept-Encoding' => 'identity'), &)
16
+ end
17
+
18
+ def stream_url
19
+ completion_url
20
+ end
21
+
22
+ def build_chunk(data)
23
+ delta_type = data.dig('delta', 'type')
24
+ track_stream_blocks(data, delta_type)
25
+
26
+ Chunk.new(
27
+ role: :assistant,
28
+ model: extract_model_id(data),
29
+ content: extract_content_delta(data, delta_type),
30
+ citations: extract_citations_delta(data, delta_type),
31
+ thinking: Thinking.build(
32
+ text: extract_thinking_delta(data, delta_type),
33
+ signature: extract_signature_delta(data, delta_type)
34
+ ),
35
+ input_tokens: extract_input_tokens(data),
36
+ output_tokens: extract_output_tokens(data),
37
+ thinking_tokens: extract_thinking_tokens(data),
38
+ cache_read_tokens: extract_cache_read_tokens(data),
39
+ cache_write_tokens: extract_cache_write_tokens(data),
40
+ server_tool_use: extract_server_tool_use(data),
41
+ tool_calls: extract_tool_calls(data),
42
+ finish_reason: normalize_finish_reason(data.dig('delta', 'stop_reason')),
43
+ **stream_end_fields(data)
44
+ )
45
+ end
46
+
47
+ def extract_server_tool_use(data)
48
+ data.dig('message', 'usage', 'server_tool_use') || data.dig('usage', 'server_tool_use')
49
+ end
50
+
51
+ # Reconstructs the response's content blocks from stream events so
52
+ # turns that used server tools can be replayed verbatim, exactly as
53
+ # the non-streaming path does with the response body.
54
+ # Resetting on message_start matters for pause_turn continuations,
55
+ # which stream several messages through one protocol instance.
56
+ def track_stream_blocks(data, delta_type)
57
+ case data['type']
58
+ when 'message_start' then reset_stream_blocks
59
+ when 'content_block_start' then track_stream_block_start(data)
60
+ when 'content_block_delta' then track_stream_block_delta(data, delta_type)
61
+ when 'content_block_stop' then finalize_stream_block(data['index'])
62
+ end
63
+ end
64
+
65
+ def reset_stream_blocks
66
+ @stream_blocks = {}
67
+ @stream_block_json = {}
68
+ @saw_server_block = false
69
+ end
70
+
71
+ def track_stream_block_start(data)
72
+ block = Support::Utils.deep_dup(data['content_block'] || {})
73
+ @stream_blocks ||= {}
74
+ @stream_blocks[data['index']] = block
75
+ @saw_server_block = true if server_tool_block?(block)
76
+ return unless block['type'].to_s.end_with?('tool_use')
77
+
78
+ @stream_block_json ||= {}
79
+ @stream_block_json[data['index']] = +''
80
+ end
81
+
82
+ def track_stream_block_delta(data, delta_type)
83
+ block = @stream_blocks&.dig(data['index'])
84
+ return unless block
85
+
86
+ case delta_type
87
+ when 'text_delta' then block['text'] = block['text'].to_s + data.dig('delta', 'text').to_s
88
+ when 'thinking_delta' then block['thinking'] = block['thinking'].to_s + data.dig('delta', 'thinking').to_s
89
+ when 'signature_delta' then block['signature'] = data.dig('delta', 'signature')
90
+ when 'input_json_delta' then @stream_block_json&.dig(data['index'])&.<< data.dig('delta', 'partial_json').to_s
91
+ when 'citations_delta' then (block['citations'] ||= []) << data.dig('delta', 'citation')
92
+ when 'compaction_delta' then block['content'] = block['content'].to_s + data.dig('delta', 'content').to_s
93
+ end
94
+ end
95
+
96
+ def finalize_stream_block(index)
97
+ json = @stream_block_json&.dig(index)
98
+ block = @stream_blocks&.dig(index)
99
+ return unless json && block
100
+
101
+ block['input'] = json.empty? ? block['input'] || {} : parse_stream_block_input(json, block)
102
+ end
103
+
104
+ def parse_stream_block_input(json, block)
105
+ JSON.parse(json)
106
+ rescue JSON::ParserError
107
+ block['input'] || {}
108
+ end
109
+
110
+ def stream_end_fields(data)
111
+ return {} unless data['type'] == 'message_stop'
112
+
113
+ blocks = (@stream_blocks || {}).sort.map { |_index, block| block }
114
+ {
115
+ server_tool_calls: extract_server_tool_calls(blocks),
116
+ raw_content: @saw_server_block ? blocks : nil,
117
+ raw_reasoning: parse_thinking_blocks(blocks)
118
+ }
119
+ end
120
+
121
+ def extract_content_delta(data, delta_type)
122
+ return data.dig('delta', 'text') if delta_type == 'text_delta'
123
+
124
+ nil
125
+ end
126
+
127
+ def extract_citations_delta(data, delta_type)
128
+ return nil unless delta_type == 'citations_delta'
129
+
130
+ citation = data.dig('delta', 'citation')
131
+ citation ? [Chat.parse_citation(citation)] : nil
132
+ end
133
+
134
+ def extract_thinking_delta(data, delta_type)
135
+ return data.dig('delta', 'thinking') if delta_type == 'thinking_delta'
136
+
137
+ nil
138
+ end
139
+
140
+ def extract_signature_delta(data, delta_type)
141
+ return data.dig('delta', 'signature') if delta_type == 'signature_delta'
142
+
143
+ nil
144
+ end
145
+
146
+ def json_delta?(data)
147
+ data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
148
+ end
149
+
150
+ def parse_streaming_error(data)
151
+ error_data = JSON.parse(data)
152
+ return unless error_data.is_a?(Hash) && error_data['type'] == 'error'
153
+
154
+ error = error_data['error']
155
+ return [500, error.to_s] unless error.is_a?(Hash)
156
+
157
+ case error['type']
158
+ when 'overloaded_error'
159
+ [529, error['message']]
160
+ else
161
+ [500, error['message']]
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Providers
4
+ module Protocols
5
5
  class Anthropic
6
6
  # Tools methods of the Anthropic API integration
7
7
  module Tools
@@ -11,62 +11,45 @@ module RubyLLM
11
11
  blocks.select { |c| c['type'] == 'tool_use' }
12
12
  end
13
13
 
14
- def format_tool_call(msg)
15
- return { role: 'assistant', content: msg.content.value } if msg.content.is_a?(RubyLLM::Content::Raw)
16
-
17
- content = []
18
-
19
- append_formatted_content(content, msg.content) unless msg.content.nil? || msg.content.empty?
20
-
21
- msg.tool_calls.each_value do |tool_call|
22
- content << format_tool_use_block(tool_call)
23
- end
24
-
25
- {
26
- role: 'assistant',
27
- content:
28
- }
29
- end
30
-
31
14
  def format_tool_result(msg)
32
15
  {
33
16
  role: 'user',
34
- content: msg.content.is_a?(RubyLLM::Content::Raw) ? msg.content.value : [format_tool_result_block(msg)]
17
+ content: [format_tool_result_block(msg)]
35
18
  }
36
19
  end
37
20
 
38
- def format_tool_use_block(tool_call)
21
+ def format_tool_result_block(msg)
39
22
  {
40
- type: 'tool_use',
41
- id: tool_call.id,
42
- name: tool_call.name,
43
- input: tool_call.arguments
23
+ type: 'tool_result',
24
+ tool_use_id: msg.tool_call_id,
25
+ content: format_tool_result_content(msg)
44
26
  }
45
27
  end
46
28
 
47
- def append_formatted_content(content_blocks, content)
48
- formatted_content = Media.format_content(content)
49
- if formatted_content.is_a?(Array)
50
- content_blocks.concat(formatted_content)
51
- else
52
- content_blocks << formatted_content
53
- end
54
- end
29
+ def format_tool_result_content(msg)
30
+ search_results = RubyLLM::SearchResults.from_content(msg.content)
31
+ return search_results.results.map { |result| search_result_block(result) } if search_results
55
32
 
56
- def format_tool_result_block(msg)
57
33
  content = msg.content
58
- content = '(no output)' if content.nil? || (content.respond_to?(:empty?) && content.empty?)
34
+ content = nil if content && content.empty?
35
+ content = '(no output)' if content.nil? && msg.attachments.empty?
36
+
37
+ Media.format_content(content, msg.attachments)
38
+ end
59
39
 
40
+ def search_result_block(result)
60
41
  {
61
- type: 'tool_result',
62
- tool_use_id: msg.tool_call_id,
63
- content: Media.format_content(content)
42
+ type: 'search_result',
43
+ source: result[:url] || result[:title],
44
+ title: result[:title],
45
+ content: [{ type: 'text', text: result[:text] }],
46
+ citations: { enabled: true }
64
47
  }
65
48
  end
66
49
 
67
50
  def function_for(tool)
68
- input_schema = tool.params_schema ||
69
- RubyLLM::Tool::SchemaDefinition.from_parameters(tool.parameters)&.json_schema
51
+ input_schema = tool.parameters_schema ||
52
+ RubyLLM::Tool::SchemaDefinition.from_parameters(tool.declared_parameters)&.json_schema
70
53
 
71
54
  declaration = {
72
55
  name: tool.name,
@@ -74,9 +57,9 @@ module RubyLLM
74
57
  input_schema: input_schema || default_input_schema
75
58
  }
76
59
 
77
- return declaration if tool.provider_params.empty?
60
+ return declaration if tool.provider_options.empty?
78
61
 
79
- RubyLLM::Utils.deep_merge(declaration, tool.provider_params)
62
+ RubyLLM::Support::Utils.deep_merge(declaration, tool.provider_options)
80
63
  end
81
64
 
82
65
  def extract_tool_calls(data)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The Anthropic Messages API.
6
+ class Anthropic < Protocol
7
+ include Anthropic::Chat
8
+ include Anthropic::Embeddings
9
+ include Anthropic::Media
10
+ include Anthropic::Models
11
+ include Anthropic::Streaming
12
+ include Anthropic::Tools
13
+
14
+ # How many times a turn that stops with pause_turn is automatically
15
+ # continued before RubyLLM returns what it has.
16
+ MAX_PAUSE_TURN_CONTINUATIONS = 8
17
+
18
+ # web_search/web_fetch pin allowed_callers to direct invocation: their
19
+ # 2026 tool versions otherwise default to requiring programmatic tool
20
+ # calling, which not every model supports.
21
+ SERVER_TOOL_ALIASES = {
22
+ web_search: { tool: { type: 'web_search_20260318', name: 'web_search', allowed_callers: ['direct'] } },
23
+ web_fetch: { tool: { type: 'web_fetch_20260318', name: 'web_fetch', allowed_callers: ['direct'] } },
24
+ url_context: { tool: { type: 'web_fetch_20260318', name: 'web_fetch', allowed_callers: ['direct'] } },
25
+ code_execution: { tool: { type: 'code_execution_20260521', name: 'code_execution' } },
26
+ mcp: lambda do |options|
27
+ options = Support::Utils.deep_symbolize_keys(options)
28
+ name = options[:name] || 'mcp'
29
+ server = { type: 'url', name: name }.merge(options.slice(:url, :authorization_token))
30
+ {
31
+ tool: { type: 'mcp_toolset', mcp_server_name: name }.merge(options.slice(:default_config, :configs)),
32
+ payload: { mcp_servers: [server] },
33
+ headers: { 'anthropic-beta' => 'mcp-client-2025-11-20' }
34
+ }
35
+ end
36
+ }.freeze
37
+
38
+ def server_tool_aliases
39
+ SERVER_TOOL_ALIASES
40
+ end
41
+
42
+ # Server tools can pause a long turn with stop_reason pause_turn; the
43
+ # API expects the conversation back verbatim to continue it. Segments
44
+ # are merged into the single Message the caller sees.
45
+ def complete(messages, ...)
46
+ segments = []
47
+ current_messages = messages
48
+
49
+ MAX_PAUSE_TURN_CONTINUATIONS.times do
50
+ message = super(current_messages, ...)
51
+ segments << message
52
+ break unless message.finish_reason == :pause_turn
53
+
54
+ current_messages = current_messages + [message] # rubocop:disable Style/SelfAssignment
55
+ end
56
+
57
+ merge_turn_segments(segments)
58
+ end
59
+
60
+ private
61
+
62
+ def sync_response(payload, additional_headers = {})
63
+ super(payload, apply_files_beta(additional_headers, payload))
64
+ end
65
+
66
+ def stream_response(payload, additional_headers = {}, &)
67
+ super(payload, apply_files_beta(additional_headers, payload), &)
68
+ end
69
+
70
+ def merge_turn_segments(segments)
71
+ return segments.first if segments.one?
72
+
73
+ final = segments.last
74
+ Message.new(
75
+ role: :assistant,
76
+ content: segments.filter_map(&:content).join,
77
+ citations: segments.flat_map(&:citations),
78
+ server_tool_calls: segments.flat_map(&:server_tool_calls),
79
+ raw_content: merge_raw_content(segments),
80
+ thinking: final.thinking,
81
+ tool_calls: merge_tool_calls(segments),
82
+ tokens: Tokens.aggregate(segments.map(&:tokens)),
83
+ finish_reason: final.finish_reason,
84
+ model: final.model,
85
+ raw: final.raw
86
+ )
87
+ end
88
+
89
+ def merge_raw_content(segments)
90
+ return unless segments.any?(&:raw_content)
91
+
92
+ segments.flat_map { |segment| segment.raw_content || format_message(segment)[:content] }
93
+ end
94
+
95
+ def merge_tool_calls(segments)
96
+ merged = segments.filter_map(&:tool_calls).reduce({}, :merge)
97
+ merged.empty? ? nil : merged
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Azure
6
+ # Azure OpenAI exposes the OpenAI Files API under /openai/v1.
7
+ class Files < Protocols::OpenAI::Files
8
+ private
9
+
10
+ def files_url
11
+ "#{@provider.azure_openai_v1_base}/files"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end