ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A hosted, single-turn research task. RubyLLM.research_later returns a
5
+ # job immediately; #wait polls it and #message returns its report.
6
+ # The provider's agent identity is separate from an inference model.
7
+ #
8
+ # job = RubyLLM.research_later(question, provider: provider, agent: agent_id)
9
+ # job.wait
10
+ # puts job.message.content
11
+ #
12
+ class ResearchJob
13
+ include Support::Inspectable
14
+
15
+ class DeadlineExpired < StandardError; end # :nodoc:
16
+ private_constant :DeadlineExpired
17
+
18
+ # A research failure that retains the job for inspection or recovery.
19
+ class Error < RubyLLM::Error
20
+ # The research job, including its remote ID and last known state.
21
+ attr_reader :job
22
+
23
+ # Creates an error retaining +job+ and the optional HTTP response.
24
+ def initialize(message, job:, response: nil)
25
+ @job = job
26
+ super(message, response:)
27
+ end
28
+ end
29
+
30
+ # Raised when polling exceeds its deadline. The independent job may
31
+ # still be running; Error#job retains its ID.
32
+ class TimeoutError < Error; end
33
+
34
+ # An interrupted blocking research call retaining its remote job.
35
+ class InterruptedError < Interrupt
36
+ # The job whose cancellation was attempted before interrupting.
37
+ attr_reader :job
38
+
39
+ # Creates an interruption retaining +job+ for recovery.
40
+ def initialize(message, job:)
41
+ @job = job
42
+ super(message)
43
+ end
44
+ end
45
+
46
+ # The provider-assigned job ID.
47
+ attr_reader :id
48
+
49
+ # The provider slug.
50
+ attr_reader :provider
51
+
52
+ # The hosted agent's identity, separate from a model ID.
53
+ attr_reader :agent
54
+
55
+ # The normalized state: +:pending+, +:completed+, +:incomplete+,
56
+ # +:failed+, or +:cancelled+.
57
+ attr_reader :status
58
+
59
+ # The provider's failure explanation, if any.
60
+ attr_reader :error
61
+
62
+ # The original provider response from submission or the latest poll.
63
+ attr_reader :raw
64
+
65
+ # The error from an unsuccessful automatic cancellation attempt, if any.
66
+ attr_reader :cancellation_error
67
+
68
+ # Submits one research task without waiting. +provider:+ and +agent:+
69
+ # are required; +with:+ attaches documents or images where supported.
70
+ # +server_tools:+ accepts an array of aliases or a Hash of aliases
71
+ # and their options, as on Chat#with_server_tools.
72
+ def self.research_later(prompt, provider:, agent:, with: nil, server_tools: nil,
73
+ context: nil, provider_options: {}, metadata: nil)
74
+ config = context&.config || RubyLLM.config
75
+ instance = Provider.resolve!(provider).new(config)
76
+ payload = { provider: instance.slug, agent:, prompt:, metadata: }
77
+ RubyLLM.instrument('research_job.ruby_llm', payload, config:) do |event|
78
+ job = instance.research_later(prompt, agent:, with:, server_tools:, provider_options:)
79
+ event[:job_id] = job.id
80
+ event[:status] = job.status
81
+ job
82
+ end
83
+ end
84
+
85
+ # Runs a research task and returns its Message. On a timeout or
86
+ # interrupt, attempts to cancel the remote task before raising an
87
+ # error retaining the job. +timeout:+ and +interval:+ are seconds.
88
+ def self.research(prompt, timeout: 600, interval: 5, **options)
89
+ validate_polling_options(timeout, interval)
90
+ job = research_later(prompt, **options)
91
+ job.wait(timeout:, interval:).message
92
+ rescue Interrupt => e
93
+ raise unless job
94
+
95
+ job.send(:attempt_cancellation)
96
+ raise InterruptedError.new("Research interrupted (job #{job.id})", job:), cause: e
97
+ rescue StandardError => e
98
+ job ||= e.job if e.is_a?(Error)
99
+ raise unless job
100
+
101
+ job.send(:attempt_cancellation)
102
+ raise if e.is_a?(Error)
103
+
104
+ response = e.response if e.respond_to?(:response)
105
+ raise Error.new("Research failed: #{e.message} (job #{job.id})", job:, response:), cause: e
106
+ end
107
+
108
+ # Retrieves an existing task by ID without submitting another one.
109
+ # Use the same provider configuration that created the task.
110
+ def self.find(id, provider:, context: nil)
111
+ config = context&.config || RubyLLM.config
112
+ Provider.resolve!(provider).new(config).find_research_job(id)
113
+ end
114
+
115
+ def self.validate_polling_options(timeout, interval) # :nodoc:
116
+ return if [timeout, interval].all? { |value| value.is_a?(Numeric) && value.positive? && value.finite? }
117
+
118
+ raise ArgumentError, 'Research timeout and interval must be positive finite numbers'
119
+ end
120
+
121
+ def initialize(id:, provider:, agent:, protocol:, **state) # :nodoc:
122
+ @id = id
123
+ @provider = provider.to_sym
124
+ @agent = agent
125
+ @protocol = protocol
126
+ apply_state(state)
127
+ end
128
+
129
+ # Returns whether the task is waiting or running.
130
+ def pending? = status == :pending
131
+
132
+ # Returns whether the task reached any terminal state.
133
+ def done? = !pending?
134
+
135
+ # Returns whether the task finished with a complete report.
136
+ def completed? = status == :completed
137
+
138
+ # Returns whether the provider stopped before completing the report.
139
+ def incomplete? = status == :incomplete
140
+
141
+ # Returns whether the provider reported failure.
142
+ def failed? = status == :failed
143
+
144
+ # Returns whether the provider confirmed cancellation.
145
+ def cancelled? = status == :cancelled
146
+
147
+ # Fetches the latest state and returns self. Does nothing after the
148
+ # task finishes. +timeout:+ limits this request in seconds.
149
+ def refresh(timeout: nil)
150
+ self.class.validate_polling_options(timeout, 1) unless timeout.nil?
151
+ unless done?
152
+ state = request_with_timeout(timeout) { @protocol.refresh_research_job(self, timeout:) }
153
+ apply_state(state)
154
+ end
155
+ self
156
+ end
157
+
158
+ # Polls until a terminal state and returns self. A timeout leaves the
159
+ # independent task running and raises TimeoutError with this job.
160
+ # Incomplete reports remain available through #message.
161
+ def wait(timeout: 600, interval: 5)
162
+ self.class.validate_polling_options(timeout, interval)
163
+ deadline = monotonic_time + timeout
164
+ until done?
165
+ remaining = deadline - monotonic_time
166
+ raise TimeoutError.new("Research timed out (job #{id})", job: self) unless remaining.positive?
167
+
168
+ refresh(timeout: remaining)
169
+ sleep [interval, deadline - monotonic_time].min if pending? && monotonic_time < deadline
170
+ end
171
+ raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?
172
+
173
+ self
174
+ end
175
+
176
+ # Requests cancellation and returns self. Only the provider's response
177
+ # can confirm cancellation; this does not delete stored task data.
178
+ def cancel(timeout: 5)
179
+ self.class.validate_polling_options(timeout, 1)
180
+ unless done?
181
+ state = request_with_timeout(timeout) { @protocol.cancel_research_job(self, timeout:) }
182
+ apply_state(state)
183
+ end
184
+ self
185
+ end
186
+
187
+ # Returns the report, or +nil+ while pending. An incomplete report has
188
+ # Message#finish_reason +:max_tokens+. Raises Error for failed or
189
+ # cancelled tasks.
190
+ def message
191
+ raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?
192
+
193
+ @message
194
+ end
195
+
196
+ # Returns the provider-reported task usage. Unreported fields are nil.
197
+ attr_reader :tokens
198
+
199
+ # Returns the reported cost, or unknown cost when the provider supplies
200
+ # no price. Agent IDs are never used to look up model token prices.
201
+ def cost
202
+ Cost.from_h({ total: tokens.reported_cost }.compact, tokens:)
203
+ end
204
+
205
+ private
206
+
207
+ def request_with_timeout(timeout, &)
208
+ Timeout.timeout(timeout, DeadlineExpired, &)
209
+ rescue DeadlineExpired, Faraday::TimeoutError => e
210
+ raise TimeoutError.new("Research request timed out (job #{id})", job: self), cause: e
211
+ rescue Error
212
+ raise
213
+ rescue RubyLLM::Error, Faraday::Error => e
214
+ response = e.response if e.respond_to?(:response)
215
+ @raw = response.body if response.respond_to?(:body)
216
+ raise Error.new("Research request failed: #{e.message} (job #{id})", job: self, response:), cause: e
217
+ end
218
+
219
+ def apply_state(state)
220
+ @status = state.fetch(:status)
221
+ @raw = state[:raw]
222
+ @error = state[:error]
223
+ @message = state[:message]
224
+ @tokens = state[:tokens] || @message&.tokens || Tokens.new
225
+ end
226
+
227
+ def attempt_cancellation
228
+ cancel(timeout: 5)
229
+ rescue StandardError => e
230
+ @cancellation_error = e
231
+ end
232
+
233
+ def monotonic_time
234
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
235
+ end
236
+
237
+ def inspect_attributes # :nodoc:
238
+ { id:, provider:, agent:, status: }
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A SearchResults wraps documents a Tool returns so the model can cite
5
+ # each source. RubyLLM renders them in the selected provider's citation
6
+ # format; cited passages appear on Message#citations.
7
+ #
8
+ # def execute(query:)
9
+ # docs = MyVectorStore.search(query)
10
+ #
11
+ # RubyLLM::SearchResults.new(
12
+ # *docs.map { |doc| { title: doc.name, url: doc.link, text: doc.body } }
13
+ # )
14
+ # end
15
+ #
16
+ # Cited passages come back on the response as Message#citations.
17
+ class SearchResults
18
+ KEY = 'search_results' # :nodoc:
19
+
20
+ # The normalized results, as an array of hashes with +:title+, +:text+,
21
+ # and optional +:url+ keys.
22
+ attr_reader :results
23
+
24
+ # Returns a new SearchResults built from one or more result hashes, or
25
+ # from a single result given as keywords.
26
+ #
27
+ # RubyLLM::SearchResults.new(title: 'Q4 Report', url: report_url, text: report_text)
28
+ # RubyLLM::SearchResults.new({ title: 'A', text: '...' }, { title: 'B', text: '...' })
29
+ #
30
+ # Each result is reduced to its +:title+, +:url+, and +:text+ entries.
31
+ # Raises ArgumentError if no results are given or a result is missing
32
+ # +:title+ or +:text+.
33
+ def initialize(*results, **result)
34
+ results << result if result.any?
35
+ @results = results.map { |entry| normalize(entry) }
36
+ raise ArgumentError, 'SearchResults requires at least one result' if @results.empty?
37
+ end
38
+
39
+ def self.from_content(content) # :nodoc:
40
+ return unless content.is_a?(String) && content.lstrip.start_with?('{')
41
+
42
+ parsed = JSON.parse(content)
43
+ entries = parsed[KEY]
44
+ return unless entries.is_a?(Array) && entries.any? && entries.all?(Hash)
45
+
46
+ new(*entries)
47
+ rescue JSON::ParserError, ArgumentError
48
+ nil
49
+ end
50
+
51
+ def to_h # :nodoc:
52
+ { KEY => results }
53
+ end
54
+
55
+ def to_json(*args) # :nodoc:
56
+ JSON.generate(to_h, *args)
57
+ end
58
+
59
+ private
60
+
61
+ def normalize(entry)
62
+ entry = Support::Utils.deep_symbolize_keys(entry.to_h)
63
+ raise ArgumentError, 'Search results require :title and :text' unless entry[:title] && entry[:text]
64
+
65
+ entry.slice(:title, :url, :text)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A ServerToolCall records one provider-executed tool step in an assistant
5
+ # response: a web search the model ran, a code execution, or the results
6
+ # block a provider returned for one. They appear on
7
+ # Message#server_tool_calls when a chat enables tools with
8
+ # Chat#with_server_tools.
9
+ #
10
+ # response = chat.with_server_tools(:web_search).ask "What changed in Ruby 3.5?"
11
+ # response.server_tool_calls.map(&:type) # => ["server_tool_use", "web_search_tool_result"]
12
+ #
13
+ # RubyLLM does not model each tool's result schema. #raw always holds the
14
+ # provider's block exactly as received, and is what RubyLLM replays to the
15
+ # provider in subsequent turns when the wire format requires it.
16
+ class ServerToolCall
17
+ include Support::Inspectable
18
+
19
+ # The provider's block or item type, such as <tt>"server_tool_use"</tt>,
20
+ # <tt>"web_search_tool_result"</tt>, or <tt>"web_search_call"</tt>.
21
+ attr_reader :type
22
+
23
+ # The tool name when the provider reports one, such as
24
+ # <tt>"web_search"</tt>, or +nil+.
25
+ attr_reader :name
26
+
27
+ # The provider's identifier for the call, or +nil+.
28
+ attr_reader :id
29
+
30
+ # The input the model gave the tool (a query, code to run), in the
31
+ # provider's shape, or +nil+.
32
+ attr_reader :input
33
+
34
+ # The tool's output in the provider's shape, or +nil+ for blocks that
35
+ # only record the invocation.
36
+ attr_reader :result
37
+
38
+ # The complete provider block as received, used verbatim when the
39
+ # conversation is sent back to the provider.
40
+ attr_reader :raw
41
+
42
+ def self.from_h(data) # :nodoc:
43
+ data = Support::Utils.deep_symbolize_keys(data)
44
+ new(type: data[:type], name: data[:name], id: data[:id], input: data[:input],
45
+ result: data[:result], raw: data[:raw])
46
+ end
47
+
48
+ def initialize(type:, raw:, name: nil, id: nil, input: nil, result: nil) # :nodoc:
49
+ @type = type
50
+ @name = name
51
+ @id = id
52
+ @input = input
53
+ @result = result
54
+ @raw = raw
55
+ end
56
+
57
+ # Returns the call's attributes as a Hash, omitting +nil+ values.
58
+ def to_h
59
+ {
60
+ type: type,
61
+ name: name,
62
+ id: id,
63
+ input: input,
64
+ result: result,
65
+ raw: raw
66
+ }.compact
67
+ end
68
+
69
+ def inspect_attributes # :nodoc:
70
+ { type: type, name: name, id: id, input: input }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A Speech is audio generated from text. RubyLLM.speak returns one. It
5
+ # holds the raw audio bytes along with the model, voice, and format used.
6
+ #
7
+ # speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
8
+ # speech.save "welcome.mp3"
9
+ #
10
+ class Speech
11
+ include Support::Inspectable
12
+ include Accounting::Usage::Result
13
+
14
+ # Maps audio format names to their MIME types.
15
+ MIME_TYPES = {
16
+ 'aac' => 'audio/aac',
17
+ 'flac' => 'audio/flac',
18
+ 'mp3' => 'audio/mpeg',
19
+ 'opus' => 'audio/opus',
20
+ 'pcm' => 'audio/pcm',
21
+ 'wav' => 'audio/wav'
22
+ }.freeze
23
+
24
+ # The raw audio bytes returned by the provider.
25
+ attr_reader :data
26
+
27
+ # The id of the model that generated the audio.
28
+ attr_reader :model
29
+
30
+ # The voice used for synthesis. When no +voice:+ was given, this is the
31
+ # provider default.
32
+ attr_reader :voice
33
+
34
+ # The audio format name, such as <tt>"mp3"</tt> or <tt>"pcm"</tt>.
35
+ attr_reader :format
36
+
37
+ # The MIME type of the audio, such as <tt>"audio/mpeg"</tt>.
38
+ attr_reader :mime_type
39
+
40
+ def initialize(data:, model:, voice: nil, format: 'mp3', mime_type: nil, # :nodoc:
41
+ input_tokens: nil, output_tokens: nil)
42
+ @data = data
43
+ @model = model
44
+ @voice = voice
45
+ @format = (format || 'mp3').to_s
46
+ @mime_type = mime_type || MIME_TYPES.fetch(@format, "audio/#{@format}")
47
+ @input_tokens = input_tokens
48
+ @output_tokens = output_tokens
49
+ end
50
+
51
+ # Generates speech for +input+ and returns a Speech holding the audio.
52
+ # Uses <tt>config.default_speech_model</tt> unless +model:+ is given.
53
+ # Pass +provider:+ and <tt>assume_model_exists: true</tt> to use a model
54
+ # that is not in the registry. +provider_options:+ takes options in the
55
+ # provider's request vocabulary, such as +instructions:+ and +speed:+
56
+ # for OpenAI, and merges them into the request as-is.
57
+ #
58
+ # speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
59
+ # speech.save "welcome.mp3"
60
+ #
61
+ # RubyLLM.speak "Welcome back.", voice: "nova"
62
+ # RubyLLM.speak "Save this as a WAV file.", format: "wav"
63
+ # RubyLLM.speak "Say cheerfully: Have a wonderful day!",
64
+ # model: "gemini-3.1-flash-tts-preview", provider: :gemini
65
+ #
66
+ # Given a block, yields SpeechChunk objects as audio arrives and still
67
+ # returns the complete Speech. Chunks contain consecutive bytes of the
68
+ # recording and are not separate audio files.
69
+ #
70
+ # File.open("welcome.mp3", "wb") do |file|
71
+ # RubyLLM.speak("Welcome back.") { |chunk| file.write(chunk.data) }
72
+ # end
73
+ #
74
+ # Raises RubyLLM::Error when the selected protocol cannot stream speech,
75
+ # or RubyLLM::ModelNotFoundError if +model:+ is not in the registry.
76
+ def self.speak(input,
77
+ model: nil,
78
+ provider: nil,
79
+ assume_model_exists: false,
80
+ voice: nil,
81
+ format: nil,
82
+ context: nil,
83
+ provider_options: {},
84
+ metadata: nil,
85
+ &block)
86
+ config = context&.config || RubyLLM.config
87
+ model ||= config.default_speech_model
88
+ model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
89
+ config: config)
90
+ empty_tokens = Tokens.new
91
+
92
+ payload = {
93
+ provider: provider_instance.slug,
94
+ provider_class: provider_instance.class.display_name,
95
+ model: model.id,
96
+ model_info: model,
97
+ input: input,
98
+ voice: voice,
99
+ format: format,
100
+ provider_options: provider_options,
101
+ metadata: metadata,
102
+ streaming: !block.nil?,
103
+ tokens: empty_tokens,
104
+ cost: Cost.new(tokens: empty_tokens, model:, category: :audio_tokens)
105
+ }
106
+
107
+ RubyLLM.instrument('speech.ruby_llm', payload, config: config) do |event|
108
+ result = provider_instance.speak(input, model:, voice:, format:, provider_options:, &block)
109
+ event[:result] = result
110
+ event[:response_model] = result.model
111
+ event[:voice] = result.voice
112
+ event[:format] = result.format
113
+ event[:audio_bytes] = result.to_blob.bytesize
114
+ event[:tokens] = result.tokens
115
+ event[:cost] = result.cost
116
+ result
117
+ end
118
+ end
119
+
120
+ # Returns the raw audio bytes. Alias for #data, mirroring Image#to_blob.
121
+ def to_blob
122
+ data
123
+ end
124
+
125
+ # Returns provider-reported usage across every attempt. Its fields are
126
+ # +nil+ when the provider did not report any.
127
+ def tokens
128
+ return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
129
+
130
+ Tokens.new(input: @input_tokens, output: @output_tokens)
131
+ end
132
+
133
+ # Returns the speech cost across every provider attempt.
134
+ def cost
135
+ return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
136
+
137
+ Cost.new(tokens:, model: model_info, category: :audio_tokens)
138
+ end
139
+
140
+ def model_info # :nodoc:
141
+ @model_info ||= RubyLLM.models.find(model)
142
+ rescue ModelNotFoundError
143
+ nil
144
+ end
145
+
146
+ # Writes the audio to +path+ in binary mode and returns +path+.
147
+ #
148
+ # speech.save "welcome.mp3"
149
+ #
150
+ def save(path)
151
+ File.binwrite(File.expand_path(path), to_blob)
152
+ path
153
+ end
154
+
155
+ def inspect_attributes # :nodoc:
156
+ { model: model, voice: voice, format: format, data: data && "#{data.bytesize} bytes" }
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A piece of generated audio, yielded by RubyLLM.speak as it arrives.
5
+ # Chunks are consecutive bytes of one recording, not separate audio files.
6
+ class SpeechChunk
7
+ include Support::Inspectable
8
+
9
+ # The raw audio bytes in this chunk.
10
+ attr_reader :data
11
+
12
+ # The audio format name, such as <tt>"mp3"</tt> or <tt>"pcm"</tt>.
13
+ attr_reader :format
14
+
15
+ # The MIME type of the audio, such as <tt>"audio/mpeg"</tt>.
16
+ attr_reader :mime_type
17
+
18
+ def initialize(data:, format:, mime_type: nil) # :nodoc:
19
+ @data = data.b
20
+ @format = format.to_s
21
+ @mime_type = mime_type || Speech::MIME_TYPES.fetch(@format, "audio/#{@format}")
22
+ end
23
+
24
+ # Returns the raw audio bytes. Alias for #data.
25
+ def to_blob
26
+ data
27
+ end
28
+
29
+ def inspect_attributes # :nodoc:
30
+ { format: format, data: "#{data.bytesize} bytes" }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Support # :nodoc:
5
+ class Deprecator # :nodoc:
6
+ def warn(message)
7
+ case RubyLLM.config.deprecation_behavior
8
+ when :silence
9
+ nil
10
+ when :raise
11
+ raise DeprecationError, message
12
+ else
13
+ RubyLLM.logger.warn(message)
14
+ end
15
+ end
16
+
17
+ def deprecate(name, replacement:, removal:)
18
+ warn("#{name} is deprecated and will be removed in RubyLLM #{removal}. Use #{replacement} instead.")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Support # :nodoc:
5
+ # Concise console output, the way Active Record prints records. Including
6
+ # classes declare what matters in +inspect_attributes+; #inspect shows
7
+ # those on one line with long strings truncated, and pretty printing
8
+ # follows #inspect so IRB stays readable. Inspection is a summary, not
9
+ # the data: everything remains reachable through the readers
10
+ # (+message.raw+, +embedding.vectors+, +chat.messages+).
11
+ module Inspectable
12
+ TRUNCATE_AT = 64 # :nodoc:
13
+
14
+ # Returns a one-line summary of the object built from its
15
+ # +inspect_attributes+, omitting empty ones and truncating long values.
16
+ def inspect
17
+ attributes = inspect_attributes
18
+ .reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
19
+ .map { |name, value| "#{name}: #{format_for_inspect(value)}" }
20
+ return "#<#{self.class.name}>" if attributes.empty?
21
+
22
+ "#<#{self.class.name} #{attributes.join(', ')}>"
23
+ end
24
+
25
+ def pretty_print(printer) # :nodoc:
26
+ printer.text(inspect)
27
+ end
28
+
29
+ # Returns the stock Ruby dump with every instance variable, for when
30
+ # the one-line summary is not enough.
31
+ #
32
+ # message.full_inspect
33
+ #
34
+ def full_inspect
35
+ Object.instance_method(:inspect).bind_call(self)
36
+ end
37
+
38
+ private
39
+
40
+ def format_for_inspect(value)
41
+ if value.is_a?(String) && value.length > TRUNCATE_AT
42
+ "#{value[0, TRUNCATE_AT]}...".inspect
43
+ else
44
+ value.inspect
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Support # :nodoc:
5
+ module Instrumentation # :nodoc:
6
+ WORKFLOW_CONTEXT_KEY = :ruby_llm_workflow_context
7
+
8
+ module_function
9
+
10
+ def instrument(name, payload = nil, config: nil, **attributes) # rubocop:disable Metrics/PerceivedComplexity
11
+ payload ||= {}
12
+ payload = payload.merge(attributes) unless attributes.empty?
13
+ workflow_context = current_workflow
14
+ payload = payload.merge(workflow_context) if workflow_context
15
+ instrumenter = (config || RubyLLM.config).instrumenter
16
+
17
+ if instrumenter.respond_to?(:instrument)
18
+ if block_given?
19
+ instrumenter.instrument(name, payload) { yield(payload) }
20
+ else
21
+ instrumenter.instrument(name, payload)
22
+ end
23
+ elsif block_given?
24
+ yield(payload)
25
+ end
26
+ end
27
+
28
+ def current_workflow
29
+ Thread.current[WORKFLOW_CONTEXT_KEY]
30
+ end
31
+
32
+ def with_workflow(context)
33
+ previous_context = current_workflow
34
+ Thread.current[WORKFLOW_CONTEXT_KEY] = context
35
+ yield
36
+ ensure
37
+ Thread.current[WORKFLOW_CONTEXT_KEY] = previous_context
38
+ end
39
+ end
40
+ end
41
+ end