ruby_llm 1.15.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 (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  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 -136
  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 +119 -75
  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 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  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 +1208 -150
  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 +274 -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 +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  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 +155 -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 +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -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 +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  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 +43 -34
  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/protocols/chat_completions/media.rb +121 -0
  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 +17 -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 +33 -20
  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 +565 -124
  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 +6 -15
  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 +61 -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 +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  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 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  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 -9
  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 -60
  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 +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  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 +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  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 -104
  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 +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  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 +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  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 -48
  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 +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -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 +139 -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 +385 -4
  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 +340 -92
  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 -530
  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 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. 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,166 @@
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' && @saw_server_block
112
+
113
+ blocks = @stream_blocks.sort.map { |_index, block| block }
114
+ {
115
+ server_tool_calls: extract_server_tool_calls(blocks),
116
+ raw_content: blocks
117
+ }
118
+ end
119
+
120
+ def extract_content_delta(data, delta_type)
121
+ return data.dig('delta', 'text') if delta_type == 'text_delta'
122
+
123
+ nil
124
+ end
125
+
126
+ def extract_citations_delta(data, delta_type)
127
+ return nil unless delta_type == 'citations_delta'
128
+
129
+ citation = data.dig('delta', 'citation')
130
+ citation ? [Chat.parse_citation(citation)] : nil
131
+ end
132
+
133
+ def extract_thinking_delta(data, delta_type)
134
+ return data.dig('delta', 'thinking') if delta_type == 'thinking_delta'
135
+
136
+ nil
137
+ end
138
+
139
+ def extract_signature_delta(data, delta_type)
140
+ return data.dig('delta', 'signature') if delta_type == 'signature_delta'
141
+
142
+ nil
143
+ end
144
+
145
+ def json_delta?(data)
146
+ data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
147
+ end
148
+
149
+ def parse_streaming_error(data)
150
+ error_data = JSON.parse(data)
151
+ return unless error_data.is_a?(Hash) && error_data['type'] == 'error'
152
+
153
+ error = error_data['error']
154
+ return [500, error.to_s] unless error.is_a?(Hash)
155
+
156
+ case error['type']
157
+ when 'overloaded_error'
158
+ [529, error['message']]
159
+ else
160
+ [500, error['message']]
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ 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,53 +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
- content << Media.format_text(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 format_tool_result_block(msg)
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
32
+
48
33
  content = msg.content
49
- 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
50
39
 
40
+ def search_result_block(result)
51
41
  {
52
- type: 'tool_result',
53
- tool_use_id: msg.tool_call_id,
54
- 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 }
55
47
  }
56
48
  end
57
49
 
58
50
  def function_for(tool)
59
- input_schema = tool.params_schema ||
60
- 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
61
53
 
62
54
  declaration = {
63
55
  name: tool.name,
@@ -65,19 +57,36 @@ module RubyLLM
65
57
  input_schema: input_schema || default_input_schema
66
58
  }
67
59
 
68
- return declaration if tool.provider_params.empty?
60
+ return declaration if tool.provider_options.empty?
69
61
 
70
- RubyLLM::Utils.deep_merge(declaration, tool.provider_params)
62
+ RubyLLM::Support::Utils.deep_merge(declaration, tool.provider_options)
71
63
  end
72
64
 
73
65
  def extract_tool_calls(data)
74
66
  if json_delta?(data)
75
- { nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
67
+ extract_tool_call_delta(data)
68
+ elsif content_block_start?(data)
69
+ extract_tool_call_start(data)
76
70
  else
77
71
  parse_tool_calls(data['content_block'])
78
72
  end
79
73
  end
80
74
 
75
+ def extract_tool_call_delta(data)
76
+ { data['index'] => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
77
+ end
78
+
79
+ def extract_tool_call_start(data)
80
+ tool_calls = parse_tool_calls(data['content_block'])
81
+ return tool_calls if tool_calls.nil? || data['index'].nil?
82
+
83
+ { data['index'] => tool_calls.values.first }
84
+ end
85
+
86
+ def content_block_start?(data)
87
+ data['type'] == 'content_block_start'
88
+ end
89
+
81
90
  def parse_tool_calls(content_blocks)
82
91
  return nil if content_blocks.nil?
83
92
 
@@ -0,0 +1,100 @@
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
+ merged = segments.flat_map { |segment| segment.raw_content || [] }
91
+ merged.empty? ? nil : merged
92
+ end
93
+
94
+ def merge_tool_calls(segments)
95
+ merged = segments.filter_map(&:tool_calls).reduce({}, :merge)
96
+ merged.empty? ? nil : merged
97
+ end
98
+ end
99
+ end
100
+ 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