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
data/lib/ruby_llm/tool.rb CHANGED
@@ -1,175 +1,350 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm/schema'
3
+ require 'schematist'
4
4
 
5
5
  module RubyLLM
6
- # Parameter definition for Tool methods.
7
- class Parameter
6
+ class Parameter # :nodoc:
8
7
  attr_reader :name, :type, :description, :required
9
8
 
10
- def initialize(name, type: 'string', desc: nil, description: nil, required: true)
9
+ def initialize(name, type: 'string', description: nil, required: true)
11
10
  @name = name
12
11
  @type = type
13
- @description = desc || description
12
+ @description = description
14
13
  @required = required
15
14
  end
16
15
  end
17
16
 
18
- # Base class for creating tools that AI models can use
17
+ # A Tool is an action an AI model can call during a chat. Subclasses
18
+ # describe themselves with ::description, declare their arguments, and
19
+ # implement #execute:
20
+ #
21
+ # class Weather < RubyLLM::Tool
22
+ # description "Gets current weather for a location"
23
+ #
24
+ # def execute(latitude:, longitude:)
25
+ # response = Faraday.get "https://api.open-meteo.com/v1/forecast",
26
+ # latitude: latitude, longitude: longitude,
27
+ # current: "temperature_2m,wind_speed_10m"
28
+ # JSON.parse(response.body)
29
+ # end
30
+ # end
31
+ #
32
+ # chat.with_tools(Weather).ask "What's the weather in Berlin?"
33
+ #
34
+ # When no parameters are declared, the argument schema is inferred from
35
+ # #execute's keyword arguments: required keywords become required string
36
+ # parameters and optional keywords become optional ones. Use ::parameter
37
+ # or ::parameters when arguments need explicit types, descriptions, or
38
+ # structure.
19
39
  class Tool
20
- # Stops conversation continuation after tool execution
21
- class Halt
22
- attr_reader :content
40
+ KEYWORD_PARAMETER_KINDS = %i[keyreq key].freeze # :nodoc:
41
+ TOOL_CALL_KEYWORD = :tool_call # :nodoc:
42
+
43
+ DUPED_INHERITED_CONFIG = {
44
+ :@declared_parameters => {},
45
+ :@provider_options => {}
46
+ }.freeze
47
+ COPIED_INHERITED_CONFIG = %i[
48
+ @description
49
+ @parameters_schema_definition
50
+ @requires_approval
51
+ @approval_resolver
52
+ ].freeze
53
+ private_constant :DUPED_INHERITED_CONFIG, :COPIED_INHERITED_CONFIG
23
54
 
24
- def initialize(content)
25
- @content = content
26
- end
55
+ class << self
56
+ attr_reader :parameters_schema_definition, :approval_resolver # :nodoc:
27
57
 
28
- def to_s
29
- @content.to_s
30
- end
31
- end
58
+ def inherited(subclass) # :nodoc:
59
+ super
60
+ DUPED_INHERITED_CONFIG.each do |ivar, default|
61
+ value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : default
62
+ subclass.instance_variable_set(ivar, value.dup)
63
+ end
32
64
 
33
- POSITIONAL_PARAMETER_KINDS = %i[req opt rest].freeze
65
+ COPIED_INHERITED_CONFIG.each do |ivar|
66
+ subclass.instance_variable_set(ivar, instance_variable_get(ivar))
67
+ end
68
+ end
34
69
 
35
- class << self
36
- attr_reader :params_schema_definition
70
+ # Returns the name the model calls this tool by, derived from the class
71
+ # name: underscored, reduced to ASCII, with a trailing "_tool" removed.
72
+ # Override this method to choose a different name.
73
+ #
74
+ # WeatherLookup.tool_name # => "weather_lookup"
75
+ #
76
+ def tool_name
77
+ normalized = name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
78
+ ascii_name = normalized.encode('ASCII', replace: '').gsub(/[^a-zA-Z0-9_-]/, '-')
79
+ Support::Utils.underscore(ascii_name).delete_suffix('_tool')
80
+ end
37
81
 
82
+ # :call-seq:
83
+ # description(text) -> text
84
+ # description -> string or nil
85
+ #
86
+ # Sets the description the model sees for this tool, or returns the
87
+ # current description when called without an argument.
88
+ #
89
+ # class Weather < RubyLLM::Tool
90
+ # description "Gets current weather for a location"
91
+ # end
92
+ #
38
93
  def description(text = nil)
39
94
  return @description unless text
40
95
 
41
96
  @description = text
42
97
  end
43
- alias desc description
44
98
 
45
- def param(name, **options)
46
- parameters[name] = Parameter.new(name, **options)
99
+ # Declares a parameter for the tool. +options+ accepts +type:+
100
+ # (defaults to <tt>'string'</tt>), +description:+, and +required:+
101
+ # (defaults to +true+).
102
+ #
103
+ # class Distance < RubyLLM::Tool
104
+ # description "Calculates distance between two cities"
105
+ # parameter :origin, description: "Origin city name"
106
+ # parameter :destination, description: "Destination city name"
107
+ # parameter :units, type: :string, description: "metric or imperial", required: false
108
+ # end
109
+ #
110
+ def parameter(name, **options)
111
+ declared_parameters[name] = Parameter.new(name, **options)
47
112
  end
48
113
 
49
- def parameters
50
- @parameters ||= {}
114
+ def declared_parameters # :nodoc:
115
+ @declared_parameters ||= {}
51
116
  end
52
117
 
53
- def params(schema = nil, &block)
54
- @params_schema_definition = SchemaDefinition.new(schema:, block:)
118
+ # Sets the JSON Schema for the tool's arguments. Accepts a schema hash,
119
+ # a Schematist::Schema class or instance, or a block written in the
120
+ # schematist DSL. Returns +self+.
121
+ #
122
+ # class Scheduler < RubyLLM::Tool
123
+ # description "Books a meeting"
124
+ #
125
+ # parameters do
126
+ # object :window, description: "Time window to reserve" do
127
+ # string :start, description: "ISO8601 start time"
128
+ # string :finish, description: "ISO8601 end time"
129
+ # end
130
+ # array :participants, of: :string
131
+ # end
132
+ # end
133
+ #
134
+ # Raises ArgumentError when called without a schema or a block.
135
+ def parameters(schema = nil, &block)
136
+ if schema.nil? && block.nil?
137
+ raise ArgumentError, 'parameters requires a schema or a block; declare single arguments with parameter'
138
+ end
139
+
140
+ @parameters_schema_definition = SchemaDefinition.new(schema:, block:)
55
141
  self
56
142
  end
57
143
 
58
- def with_params(**params)
59
- @provider_params = params
144
+ # Declares that this tool must be approved before it executes. The
145
+ # conversation loop pauses the tool call until a decision is recorded with
146
+ # Chat#approve or Chat#deny, so Chat#complete returns cleanly and
147
+ # can be called again once the decision exists. In Rails the decision
148
+ # persists on the tool call record and survives process restarts.
149
+ #
150
+ # class IssueRefund < RubyLLM::Tool
151
+ # requires_approval
152
+ #
153
+ # def execute(order_id:)
154
+ # Refunds.issue!(order_id)
155
+ # end
156
+ # end
157
+ #
158
+ # Pass a block to resolve the decision yourself instead of using the
159
+ # recorded one. The block receives the ToolCall and returns +true+ to
160
+ # execute, +false+ to deny, or +nil+ while the decision is pending.
161
+ #
162
+ # The block never runs at class definition. The loop consults it
163
+ # whenever it needs the decision, which can be several times while
164
+ # the call is pending, including after a crashed job resumes, so
165
+ # write it as an idempotent read. If it also creates the approval
166
+ # request, make that a find-or-create.
167
+ #
168
+ # requires_approval { |tool_call| Approvals.status(tool_call.id) }
169
+ #
170
+ def requires_approval(&resolver)
171
+ @requires_approval = true
172
+ @approval_resolver = resolver
173
+ end
174
+
175
+ def requires_approval? # :nodoc:
176
+ @requires_approval || false
177
+ end
178
+
179
+ # :call-seq:
180
+ # provider_options(options) -> self
181
+ # provider_options -> hash
182
+ #
183
+ # Sets provider-specific metadata, such as Anthropic's +cache_control+
184
+ # hints, merged verbatim into the tool payload sent to the provider.
185
+ # Without an argument, returns the current options.
186
+ #
187
+ # provider_options cache_control: { type: "ephemeral" }
188
+ #
189
+ # Raises ArgumentError if +options+ is +nil+.
190
+ def provider_options(options = (get = true))
191
+ return @provider_options ||= {} if get
192
+ raise ArgumentError, 'provider_options does not accept nil' if options.nil?
193
+
194
+ @provider_options = options.to_h
60
195
  self
61
196
  end
62
197
 
63
- def provider_params
64
- @provider_params ||= {}
198
+ def split_result(result) # :nodoc:
199
+ case result
200
+ when Attachment then ['', [result]]
201
+ when Array then split_array_result(result)
202
+ else [result_content(result), []]
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def split_array_result(result)
209
+ parts = result.flatten.compact
210
+ return [result_content(result), []] if parts.none?(Attachment)
211
+
212
+ texts, attachments = parts.partition { |part| part.is_a?(String) }
213
+ unless attachments.all?(Attachment)
214
+ raise ArgumentError, 'Tool results mixing attachments can only contain Strings and RubyLLM::Attachments'
215
+ end
216
+
217
+ [texts.join("\n\n"), attachments]
218
+ end
219
+
220
+ def result_content(result)
221
+ case result
222
+ when String then result
223
+ when Hash, Array, SearchResults then result.to_json
224
+ else result.to_s
225
+ end
65
226
  end
66
227
  end
67
228
 
229
+ # Returns the name the model calls this tool by, delegating to
230
+ # ::tool_name. Override either one to choose a different name.
231
+ #
232
+ # WeatherLookup.new.name # => "weather_lookup"
233
+ #
68
234
  def name
69
- klass_name = self.class.name
70
- normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
71
- normalized.encode('ASCII', replace: '')
72
- .gsub(/[^a-zA-Z0-9_-]/, '-')
73
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
74
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
75
- .downcase
76
- .delete_suffix('_tool')
235
+ self.class.tool_name
77
236
  end
78
237
 
238
+ # Returns the tool description declared on the class with ::description.
79
239
  def description
80
240
  self.class.description
81
241
  end
82
242
 
83
- def parameters
84
- self.class.parameters
243
+ # Returns whether this tool was declared with ::requires_approval.
244
+ def requires_approval?
245
+ self.class.requires_approval?
85
246
  end
86
247
 
87
- def provider_params
88
- self.class.provider_params
248
+ def approval_resolver # :nodoc:
249
+ self.class.approval_resolver
89
250
  end
90
251
 
91
- def params_schema
92
- return @params_schema if defined?(@params_schema)
252
+ def declared_parameters # :nodoc:
253
+ self.class.declared_parameters
254
+ end
93
255
 
94
- @params_schema = begin
95
- definition = self.class.params_schema_definition
256
+ # Returns the provider-specific tool metadata declared on the class.
257
+ def provider_options
258
+ self.class.provider_options
259
+ end
260
+
261
+ # Returns the JSON Schema for the tool's arguments, whether declared
262
+ # explicitly or inferred from the +execute+ signature.
263
+ def parameters_schema
264
+ return @parameters_schema if defined?(@parameters_schema)
265
+
266
+ @parameters_schema = begin
267
+ definition = self.class.parameters_schema_definition
96
268
  if definition&.present?
97
269
  definition.json_schema
98
- elsif parameters.any?
99
- SchemaDefinition.from_parameters(parameters)&.json_schema
270
+ elsif declared_parameters.any?
271
+ SchemaDefinition.from_parameters(declared_parameters)&.json_schema
100
272
  else
101
273
  SchemaDefinition.from_parameters(inferred_parameters, allow_empty: true)&.json_schema
102
274
  end
103
275
  end
104
276
  end
105
277
 
106
- def call(args)
107
- normalized_args = normalize_args(args)
278
+ # Runs the tool with keyword arguments, validating them against the
279
+ # #execute signature before invoking it. RubyLLM supplies +tool_call:+
280
+ # when the call comes from a chat; pass it yourself only when you need
281
+ # the ToolCall in a direct invocation.
282
+ #
283
+ # Weather.new.call(latitude: 52.52, longitude: 13.405)
284
+ #
285
+ def call(tool_call: nil, **arguments)
286
+ normalized_args = arguments.transform_keys(&:to_sym)
108
287
  validation_error = validate_keyword_arguments(normalized_args)
109
288
  return { error: "Invalid tool arguments: #{validation_error}" } if validation_error
110
289
 
111
290
  RubyLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
291
+ normalized_args[TOOL_CALL_KEYWORD] = tool_call if execute_accepts_tool_call?
112
292
  result = execute(**normalized_args)
113
293
  RubyLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
114
294
  result
115
295
  end
116
296
 
297
+ # Runs the tool with the arguments chosen by the model. Subclasses must
298
+ # implement this method; the base implementation raises
299
+ # NotImplementedError. The return value is sent back to the model.
300
+ # Return a Hash like <tt>{ error: "..." }</tt> to report a recoverable
301
+ # failure.
302
+ #
303
+ # Declare an optional +tool_call:+ keyword to receive the ToolCall being
304
+ # executed. The keyword is reserved: it never appears in the tool's
305
+ # argument schema and is filled in by RubyLLM, not by the model.
306
+ #
307
+ # def execute(query:, tool_call: nil)
308
+ # AuditLog.create!(tool_call_id: tool_call&.id)
309
+ # Search.run(query)
310
+ # end
311
+ #
117
312
  def execute(...)
118
313
  raise NotImplementedError, 'Subclasses must implement #execute'
119
314
  end
120
315
 
121
316
  protected
122
317
 
123
- def halt(message)
124
- Halt.new(message)
125
- end
126
-
127
- def normalize_args(args)
128
- return {} if args.nil?
129
- return args.transform_keys(&:to_sym) if args.respond_to?(:transform_keys)
130
-
131
- {}
132
- end
133
-
134
- def validate_keyword_arguments(arguments)
135
- required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments =
136
- execute_keyword_signature
137
-
138
- return nil if required_keywords.empty? && optional_keywords.empty? && accepts_positional_arguments
318
+ def validate_keyword_arguments(arguments) # :nodoc:
319
+ required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
139
320
 
140
321
  argument_keys = arguments.keys
141
- missing_keyword = first_missing_keyword(required_keywords, argument_keys)
322
+ missing_keyword = (required_keywords - argument_keys).first
142
323
  return "missing keyword: #{missing_keyword}" if missing_keyword
143
324
  return nil if accepts_extra_keywords
144
325
 
145
- allowed_keywords = required_keywords + optional_keywords
146
- unknown_keyword = first_unknown_keyword(argument_keys, allowed_keywords)
326
+ unknown_keyword = (argument_keys - (required_keywords + optional_keywords)).first
147
327
  return "unknown keyword: #{unknown_keyword}" if unknown_keyword
148
328
 
149
329
  nil
150
330
  end
151
331
 
152
- def execute_keyword_signature
153
- keyword_signature = method(:execute).parameters
154
- required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
155
- optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
156
- accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
157
- accepts_positional_arguments = keyword_signature.any? do |kind, _|
158
- POSITIONAL_PARAMETER_KINDS.include?(kind)
332
+ def execute_accepts_tool_call? # :nodoc:
333
+ method(:execute).parameters.any? do |kind, name|
334
+ name == TOOL_CALL_KEYWORD && KEYWORD_PARAMETER_KINDS.include?(kind)
159
335
  end
160
-
161
- [required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments]
162
336
  end
163
337
 
164
- def first_missing_keyword(required_keywords, argument_keys)
165
- (required_keywords - argument_keys).first
166
- end
338
+ def execute_keyword_signature # :nodoc:
339
+ keyword_signature = method(:execute).parameters.reject { |_, name| name == TOOL_CALL_KEYWORD }
340
+ required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
341
+ optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
342
+ accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
167
343
 
168
- def first_unknown_keyword(argument_keys, allowed_keywords)
169
- (argument_keys - allowed_keywords).first
344
+ [required_keywords, optional_keywords, accepts_extra_keywords]
170
345
  end
171
346
 
172
- def inferred_parameters
347
+ def inferred_parameters # :nodoc:
173
348
  required_keywords, optional_keywords, = execute_keyword_signature
174
349
 
175
350
  (required_keywords + optional_keywords).to_h do |name|
@@ -177,9 +352,7 @@ module RubyLLM
177
352
  end
178
353
  end
179
354
 
180
- # Wraps schema handling for tool parameters, supporting JSON Schema hashes,
181
- # RubyLLM::Schema instances/classes, and DSL blocks.
182
- class SchemaDefinition
355
+ class SchemaDefinition # :nodoc:
183
356
  def self.from_parameters(parameters, allow_empty: false)
184
357
  return nil if parameters.nil? || (parameters.empty? && !allow_empty)
185
358
 
@@ -233,7 +406,9 @@ module RubyLLM
233
406
  end
234
407
 
235
408
  def json_schema
236
- @json_schema ||= RubyLLM::Utils.deep_stringify_keys(resolve_schema)
409
+ @json_schema ||= RubyLLM::Support::Utils.strip_schema_metadata(
410
+ RubyLLM::Support::Utils.deep_stringify_keys(resolve_schema)
411
+ )
237
412
  end
238
413
 
239
414
  private
@@ -247,7 +422,7 @@ module RubyLLM
247
422
 
248
423
  def resolve_direct_schema(schema)
249
424
  return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema)
250
- return RubyLLM::Utils.deep_dup(schema) if schema.is_a?(Hash)
425
+ return RubyLLM::Support::Utils.deep_dup(schema) if schema.is_a?(Hash)
251
426
  if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
252
427
  return extract_schema(schema.new.to_json_schema)
253
428
  end
@@ -256,7 +431,7 @@ module RubyLLM
256
431
  end
257
432
 
258
433
  def build_from_block(&)
259
- schema_class = RubyLLM::Schema.create(&)
434
+ schema_class = Schematist::Schema.create(&)
260
435
  extract_schema(schema_class.new.to_json_schema)
261
436
  end
262
437
 
@@ -264,7 +439,7 @@ module RubyLLM
264
439
  return nil unless schema_hash.is_a?(Hash)
265
440
 
266
441
  schema = schema_hash[:schema] || schema_hash['schema'] || schema_hash
267
- RubyLLM::Utils.deep_dup(schema)
442
+ RubyLLM::Support::Utils.deep_dup(schema)
268
443
  end
269
444
  end
270
445
  end
@@ -1,23 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Represents a function call from an AI model to a Tool.
4
+ # A ToolCall is a request to run a tool with specific arguments. Instances
5
+ # appear in Message#tool_calls. Local calls are yielded to tool callbacks
6
+ # such as Chat#before_tool_call. Remote calls awaiting approval return
7
+ # +true+ from #remote? and appear in Chat#pending_approvals.
8
+ #
9
+ # chat.before_tool_call do |tool_call|
10
+ # puts "Calling tool: #{tool_call.name}"
11
+ # puts "Arguments: #{tool_call.arguments}"
12
+ # end
13
+ #
5
14
  class ToolCall
6
- attr_reader :id, :name, :arguments
15
+ include Support::Inspectable
16
+
17
+ # The unique identifier for this call. The tool result message
18
+ # answering this call carries the same id.
19
+ attr_reader :id
20
+
21
+ # The name of the tool the model wants to invoke.
22
+ attr_reader :name
23
+
24
+ # The arguments the model supplied for the invocation, as a Hash.
25
+ attr_reader :arguments
26
+
27
+ # Returns +true+ if the provider executes this call after approval.
28
+ # Local tools executed by the application return +false+.
29
+ def remote? = @remote
30
+
31
+ # The Gemini thought signature attached to this call, or +nil+.
32
+ # RubyLLM replays it to the provider on later requests.
7
33
  attr_accessor :thought_signature
8
34
 
9
- def initialize(id:, name:, arguments: {}, thought_signature: nil)
35
+ def initialize(id:, name:, arguments: {}, thought_signature: nil, remote: false) # :nodoc:
10
36
  @id = id
11
37
  @name = name
12
38
  @arguments = arguments
13
39
  @thought_signature = thought_signature
40
+ @remote = remote
41
+ end
42
+
43
+ def inspect_attributes # :nodoc:
44
+ { id: id, name: name, arguments: arguments, remote: remote? || nil }.compact
14
45
  end
15
46
 
47
+ # Returns the call as a Hash, omitting +nil+ values.
16
48
  def to_h
17
49
  {
18
50
  id: @id,
19
51
  name: @name,
20
52
  arguments: @arguments,
53
+ remote: remote? || nil,
21
54
  thought_signature: @thought_signature
22
55
  }.compact
23
56
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Tools # :nodoc:
5
+ # Normalizes and resolves the server tools a chat enabled with
6
+ # Chat#with_server_tools. Protocols declare an alias table mapping portable
7
+ # names such as +:web_search+ to their wire format; raw Hashes pass through
8
+ # verbatim so new provider tools work without a gem update. :nodoc:
9
+ module ServerTools # :nodoc: all
10
+ # The result of resolving a chat's server tools against a protocol's
11
+ # alias table: tool entries for the payload's tools slot, extra payload
12
+ # fields to merge, and request headers to add.
13
+ class Resolution
14
+ attr_reader :tools, :payload, :headers
15
+
16
+ def initialize
17
+ @tools = []
18
+ @payload = {}
19
+ @headers = {}
20
+ end
21
+
22
+ def add(spec, options)
23
+ entry = spec.is_a?(Proc) ? spec.call(options || {}) : merge_tool_options(spec, options)
24
+ @tools << entry[:tool] if entry[:tool]
25
+ merge_payload(entry[:payload]) if entry[:payload]
26
+ merge_headers(entry[:headers]) if entry[:headers]
27
+ self
28
+ end
29
+
30
+ def add_raw(hash)
31
+ @tools << hash
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ def merge_tool_options(spec, options)
38
+ return spec if options.nil? || options.empty?
39
+
40
+ { tool: Support::Utils.deep_merge(spec.fetch(:tool, spec), Support::Utils.deep_symbolize_keys(options)) }
41
+ .merge(spec.slice(:payload, :headers))
42
+ end
43
+
44
+ # Payload arrays accumulate: every entry contributes its own MCP server.
45
+ def merge_payload(payload)
46
+ @payload = @payload.merge(payload) do |_key, current, addition|
47
+ next current | addition if current.is_a?(Array) && addition.is_a?(Array)
48
+ next Support::Utils.deep_merge(current, addition) if current.is_a?(Hash) && addition.is_a?(Hash)
49
+
50
+ addition
51
+ end
52
+ end
53
+
54
+ # Anthropic-style beta headers combine as comma-separated values.
55
+ def merge_headers(headers)
56
+ headers.each do |key, value|
57
+ @headers[key] = [@headers[key], value].compact.uniq.join(',')
58
+ end
59
+ end
60
+ end
61
+
62
+ module_function
63
+
64
+ # Normalizes with_server_tools arguments into entry hashes:
65
+ # {name:, options:} for aliases and {raw:} for verbatim tool specs.
66
+ def normalize(tools, tools_with_options)
67
+ entries = tools.compact.map { |tool| normalize_positional(tool) }
68
+ entries + tools_with_options.map { |name, options| { name: name.to_sym, options: options.to_h } }
69
+ end
70
+
71
+ def normalize_positional(tool)
72
+ case tool
73
+ when Symbol, String then { name: tool.to_sym, options: {} }
74
+ when Hash then normalized_entry?(tool) ? tool : { raw: tool }
75
+ else
76
+ raise ArgumentError,
77
+ "Server tools must be Symbols or Hashes, got #{tool.class}. " \
78
+ 'Pass :web_search for a portable alias or the provider\'s tool definition as a Hash.'
79
+ end
80
+ end
81
+
82
+ # Entries that already went through normalize (an Agent's declared
83
+ # server_tools, for example) pass through unchanged.
84
+ def normalized_entry?(hash)
85
+ hash.key?(:raw) ? hash.size == 1 : hash.size == 2 && hash.key?(:name) && hash.key?(:options)
86
+ end
87
+
88
+ # Resolves normalized entries against a protocol's alias table.
89
+ def resolve(entries, aliases:, owner:)
90
+ entries.each_with_object(Resolution.new) do |entry, resolution|
91
+ if entry[:raw]
92
+ resolution.add_raw(entry[:raw])
93
+ else
94
+ spec = aliases[entry[:name]]
95
+ raise_unknown_alias(entry[:name], aliases, owner) unless spec
96
+ resolution.add(spec, entry[:options])
97
+ end
98
+ end
99
+ end
100
+
101
+ def raise_unknown_alias(name, aliases, owner)
102
+ known = aliases.keys.map { |key| ":#{key}" }.join(', ')
103
+ raise UnsupportedServerToolError,
104
+ "#{owner} has no server tool alias :#{name}. Known aliases: #{known}. " \
105
+ 'New or unlisted tools work by passing the provider\'s tool definition as a Hash.'
106
+ end
107
+ end
108
+ end
109
+ end