ruby_llm 2.1.0 → 2.2.0

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 (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -18,34 +18,18 @@ module RubyLLM
18
18
  include Enumerable
19
19
  include Lookup
20
20
 
21
- MODELS_DEV_PROVIDER_MAP = { # :nodoc:
22
- 'openai' => 'openai',
23
- 'anthropic' => 'anthropic',
24
- 'azure' => 'azure',
25
- 'google' => 'gemini',
26
- 'google-vertex' => 'vertexai',
27
- 'amazon-bedrock' => 'bedrock',
28
- 'cohere' => 'cohere',
29
- 'deepseek' => 'deepseek',
30
- 'hetzner' => 'hetzner',
31
- 'mistral' => 'mistral',
32
- 'ollama-cloud' => 'ollama_cloud',
33
- 'openrouter' => 'openrouter',
34
- 'perplexity' => 'perplexity',
35
- 'perplexity-agent' => 'perplexity',
36
- 'xai' => 'xai'
37
- }.freeze
38
21
  MODELS_DEV_INPUT_MODALITIES = %w[text image audio pdf video file].freeze # :nodoc:
39
22
  MODELS_DEV_OUTPUT_MODALITIES = %w[text image audio video embeddings moderation rerank judgment].freeze # :nodoc:
40
23
  # First-party providers outrank the aggregators that resell their models.
41
24
  PROVIDER_PREFERENCE = %w[
42
- openai anthropic gemini deepseek mistral cohere typesafe perplexity xai
25
+ openai anthropic gemini deepseek mistral cohere typesafe perplexity xai moonshot minimax zai meta
43
26
  vertexai bedrock openrouter azure hetzner ollama_cloud ollama gpustack
44
27
  ].freeze # :nodoc:
45
28
  INSTANCE_DELEGATES = (Enumerable.instance_methods(false) + %i[
46
29
  all
47
30
  each
48
31
  find
32
+ find_configured
49
33
  listed
50
34
  unlisted
51
35
  chat_models
@@ -149,9 +133,7 @@ module RubyLLM
149
133
  existing_models = read_existing_models
150
134
 
151
135
  provider_fetch = fetch_provider_models(remote_only: remote_only)
152
- @last_provider_failures = provider_fetch[:failed]
153
- log_provider_fetch(provider_fetch)
154
- payload[:failed_providers] = provider_fetch[:failed].map { |failure| failure[:slug] }
136
+ record_provider_fetch(provider_fetch, payload)
155
137
 
156
138
  models_dev_fetch = fetch_models_dev_models(existing_models)
157
139
  log_models_dev_fetch(models_dev_fetch)
@@ -162,6 +144,12 @@ module RubyLLM
162
144
  end
163
145
  end
164
146
 
147
+ def record_provider_fetch(provider_fetch, payload) # :nodoc:
148
+ @last_provider_failures = provider_fetch[:failed]
149
+ log_provider_fetch(provider_fetch)
150
+ payload[:failed_providers] = provider_fetch[:failed].map { |failure| failure[:slug] }
151
+ end
152
+
165
153
  def fetch_provider_models(remote_only: true) # :nodoc:
166
154
  config = RubyLLM.config
167
155
  providers = remote_only ? Provider.configured_remote_providers(config) : Provider.configured_providers(config)
@@ -210,7 +198,7 @@ module RubyLLM
210
198
 
211
199
  model ||= Model.default(model_id, provider_class.slug)
212
200
  else
213
- model = Models.find model_id, provider: provider, config: config
201
+ model = Models.find_configured model_id, provider: provider, config: config
214
202
  provider_class = Provider.resolve!(model.provider)
215
203
  end
216
204
  [model, provider_class.new(config)]
@@ -237,18 +225,26 @@ module RubyLLM
237
225
  def parse_models_dev_catalog(body) # :nodoc:
238
226
  raise ModelRegistryError, "models.dev returned #{body.class} instead of a catalog" unless body.is_a?(Hash)
239
227
 
240
- models = body.flat_map { |provider_key, data| models_dev_provider_models(provider_key, data) }
228
+ slugs = models_dev_provider_slugs
229
+ models = body.flat_map do |provider_key, data|
230
+ models_dev_provider_models(provider_key.to_s, data, slugs[provider_key.to_s])
231
+ end
241
232
  raise ModelRegistryError, 'models.dev returned no models RubyLLM knows a provider for' if models.empty?
242
233
 
243
234
  models
244
235
  end
245
236
 
246
- def models_dev_provider_models(provider_key, provider_data) # :nodoc:
247
- provider_slug = MODELS_DEV_PROVIDER_MAP[provider_key.to_s]
237
+ def models_dev_provider_slugs # :nodoc:
238
+ Provider.providers.each_with_object({}) do |(slug, provider), slugs|
239
+ provider.models_dev_provider_ids.each { |id| slugs[id] ||= slug.to_s }
240
+ end
241
+ end
242
+
243
+ def models_dev_provider_models(provider_key, provider_data, provider_slug) # :nodoc:
248
244
  return [] unless provider_slug
249
245
 
250
246
  (provider_data[:models] || {}).values.filter_map do |model_data|
251
- model = Model.new(models_dev_model_attributes(model_data, provider_slug, provider_key.to_s))
247
+ model = Model.new(models_dev_model_attributes(model_data, provider_slug, provider_key))
252
248
  model unless model.provider.nil? || model.id.nil?
253
249
  end
254
250
  end
@@ -285,13 +281,7 @@ module RubyLLM
285
281
  .flatten
286
282
 
287
283
  provider_models = provider_fetch[:models] + preserved_models
288
- models_dev_models = if models_dev_fetch[:fetched]
289
- models_dev_fetch[:models]
290
- else
291
- existing_models.select { |model| model.metadata[:source] == 'models.dev' }
292
- end
293
-
294
- merge_models(provider_models, models_dev_models)
284
+ merge_models(provider_models, models_dev_fetch[:models])
295
285
  end
296
286
 
297
287
  def merge_models(provider_models, models_dev_models) # :nodoc:
@@ -472,7 +462,13 @@ module RubyLLM
472
462
  text_tokens = models_dev_text_tokens_pricing(text_standard, cost)
473
463
  pricing[:text_tokens] = text_tokens if text_tokens
474
464
  pricing[:audio_tokens] = { standard: audio_standard } if audio_standard.any?
475
- pricing
465
+ pricing.merge(models_dev_unit_pricing(cost))
466
+ end
467
+
468
+ def models_dev_unit_pricing(cost) # :nodoc:
469
+ %i[characters audio_seconds search_units].each_with_object({}) do |unit, pricing|
470
+ pricing[unit] = { standard: { input_per_million: cost[unit] } } unless cost[unit].nil?
471
+ end
476
472
  end
477
473
 
478
474
  def models_dev_text_tokens_pricing(text_standard, cost) # :nodoc:
@@ -603,7 +599,7 @@ module RubyLLM
603
599
  # the model, first-party providers before aggregators. Raises
604
600
  # RubyLLM::ModelNotFoundError if no model matches.
605
601
  #
606
- # RubyLLM.models.find 'gpt-5.6'
602
+ # RubyLLM.models.find 'gpt-6.1-sol'
607
603
  # RubyLLM.models.find 'claude-sonnet-5', provider: :bedrock
608
604
  #
609
605
  def find(model_id, provider: nil, config: nil)
@@ -614,6 +610,15 @@ module RubyLLM
614
610
  end
615
611
  end
616
612
 
613
+ # Returns the Model that resolution uses for +model_id+. Without
614
+ # +provider+, it prefers providers configured in +config+, falling back
615
+ # to #find's choice when none of them carries the model.
616
+ def find_configured(model_id, provider: nil, config: nil) # :nodoc:
617
+ return find_with_provider(model_id, provider, config) if provider
618
+
619
+ find_on_configured_providers(model_id, config || RubyLLM.config)
620
+ end
621
+
617
622
  # Returns a new Models registry containing only chat models.
618
623
  def chat_models
619
624
  select_models { |m| m.type == :chat }
@@ -638,7 +643,7 @@ module RubyLLM
638
643
 
639
644
  # Returns a new Models registry containing only models in +family+.
640
645
  #
641
- # RubyLLM.models.by_family('claude3_sonnet')
646
+ # RubyLLM.models.by_family('claude-sonnet')
642
647
  #
643
648
  def by_family(family)
644
649
  select_models { |m| m.family == family.to_s }
@@ -668,7 +673,7 @@ module RubyLLM
668
673
  def refresh(remote_only: false)
669
674
  RubyLLM.instrument('models.refresh.ruby_llm', remote_only:) do |payload|
670
675
  published = fetch_published_models
671
- main_models = merge_discovered_models(published.models, remote_only:)
676
+ main_models = merge_discovered_models(published.models, payload, remote_only:)
672
677
  merged_models = self.class.merge_models(self.class.models_from_provider_gems, main_models)
673
678
  persisted_models = RubyLLM.config.model_registry_store ? merged_models : main_models
674
679
  persist_registry!(persisted_models, published:)
@@ -728,9 +733,9 @@ module RubyLLM
728
733
  nil
729
734
  end
730
735
 
731
- def merge_discovered_models(published, remote_only:)
736
+ def merge_discovered_models(published, payload, remote_only:)
732
737
  provider_fetch = self.class.fetch_provider_models(remote_only: remote_only)
733
- self.class.log_provider_fetch(provider_fetch)
738
+ self.class.record_provider_fetch(provider_fetch, payload)
734
739
  preserved = preserved_providers(provider_fetch, published)
735
740
  preserved_models = all.select { |model| preserved.include?(model.provider) }
736
741
  self.class.merge_models(provider_fetch[:models] + preserved_models, published)
@@ -57,8 +57,9 @@ module RubyLLM
57
57
  # The provider-assigned identifier of the moderation request.
58
58
  attr_reader :id
59
59
 
60
- # The id of the model that performed the moderation, or +nil+ for an
61
- # operation that does not select a model.
60
+ # The id of the model that performed the moderation. A moderation run
61
+ # by a provider resource rather than a model names that resource, such
62
+ # as a Bedrock guardrail's <tt>"id:version"</tt>, as its usage does.
62
63
  attr_reader :model
63
64
 
64
65
  # The per-input verdicts, as an array of Result objects, one per
@@ -82,7 +83,7 @@ module RubyLLM
82
83
  # to use a model that is not in the registry. An explicitly selected
83
84
  # provider may instead use a configured resource without a model.
84
85
  # +owner:+ attributes the usage to a record, such as a user, and wins
85
- # over RubyLLM.with_usage_owner.
86
+ # over RubyLLM.with_owner.
86
87
  #
87
88
  # RubyLLM.moderate("User message")
88
89
  # RubyLLM.moderate(["First comment", "Second comment"]).results
@@ -100,33 +101,12 @@ module RubyLLM
100
101
  attachments = Attachment.wrap(with)
101
102
  raise ArgumentError, 'must provide input text, image attachment, or both' if input.nil? && attachments.empty?
102
103
 
103
- config = context&.config || RubyLLM.config
104
- model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
105
- config: config, operation: :moderate,
106
- default_model: config.default_moderation_model)
107
- empty_tokens = Tokens.new
108
- payload = {
109
- provider: provider_instance.slug,
110
- provider_class: provider_instance.name,
111
- model: model&.id,
112
- model_info: model,
113
- input: input,
114
- attachment_count: attachments.size,
115
- provider_options: provider_options,
116
- metadata: metadata,
117
- tokens: empty_tokens,
118
- cost: Cost.new(tokens: empty_tokens, model:)
119
- }
120
-
121
- RubyLLM.instrument('moderation.ruby_llm', payload, config: config) do |event|
122
- result = Accounting::Usage.owned_by(owner) do
123
- provider_instance.moderate(input, model:, with: attachments, provider_options:)
124
- end
125
- event[:result] = result
126
- event[:flagged] = result.flagged?
127
- event[:tokens] = result.tokens
128
- event[:cost] = result.cost
129
- result
104
+ Accounting::Operation.new(
105
+ 'moderation', model, operation: :moderate, default: :default_moderation_model, provider:, assume_model_exists:,
106
+ context:, owner:,
107
+ details: ->(event, result) { event[:flagged] = result.flagged? }
108
+ ).run(input:, attachment_count: attachments.size, provider_options:, metadata:) do |provider_instance, model|
109
+ provider_instance.moderate(input, model:, with: attachments, provider_options:)
130
110
  end
131
111
  end
132
112
 
data/lib/ruby_llm/ocr.rb CHANGED
@@ -42,7 +42,7 @@ module RubyLLM
42
42
  # into the request in the provider's own vocabulary, such as Mistral's
43
43
  # +include_image_base64:+ or +table_format:+.
44
44
  # +owner:+ attributes the usage to a record, such as a user, and wins
45
- # over RubyLLM.with_usage_owner.
45
+ # over RubyLLM.with_owner.
46
46
  #
47
47
  # RubyLLM.ocr("report.pdf")
48
48
  # RubyLLM.ocr("https://example.com/scan.png")
@@ -59,27 +59,10 @@ module RubyLLM
59
59
  provider_options: {},
60
60
  metadata: nil,
61
61
  owner: nil)
62
- config = context&.config || RubyLLM.config
63
- model ||= config.default_ocr_model
64
- model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
65
- config: config)
66
- payload = {
67
- provider: provider_instance.slug,
68
- provider_class: provider_instance.name,
69
- model: model.id,
70
- model_info: model,
71
- pages: pages,
72
- provider_options: provider_options,
73
- metadata: metadata
74
- }
75
-
76
- RubyLLM.instrument('ocr.ruby_llm', payload, config: config) do |event|
77
- result = Accounting::Usage.owned_by(owner) do
78
- provider_instance.ocr(file, model:, pages:, provider_options:)
79
- end
80
- event[:result] = result
81
- event[:response_model] = result.model
82
- result
62
+ Accounting::Operation.new(
63
+ 'ocr', model, default: :default_ocr_model, usage: false, provider:, assume_model_exists:, context:, owner:
64
+ ).run(pages:, provider_options:, metadata:) do |provider_instance, model|
65
+ provider_instance.ocr(file, model:, pages:, provider_options:)
83
66
  end
84
67
  end
85
68
 
@@ -4,6 +4,7 @@ module RubyLLM
4
4
  class OpenTelemetry
5
5
  module Attributes # :nodoc:
6
6
  OPERATIONS = {
7
+ 'agent.ruby_llm' => 'invoke_agent',
7
8
  'chat.ruby_llm' => 'chat',
8
9
  'embedding.ruby_llm' => 'embeddings',
9
10
  'image.ruby_llm' => 'generate_content',
@@ -14,51 +15,60 @@ module RubyLLM
14
15
  'moderation.ruby_llm' => 'moderation',
15
16
  'judgment.ruby_llm' => 'judgment',
16
17
  'tool_call.ruby_llm' => 'execute_tool',
18
+ 'code_execution.ruby_llm' => 'ruby_llm.code_execution',
17
19
  'workflow.ruby_llm' => 'invoke_workflow',
18
20
  'workflow_step.ruby_llm' => 'ruby_llm.workflow_step'
19
21
  }.freeze
20
- PROVIDERS = {
21
- 'bedrock' => 'aws.bedrock',
22
- 'azure' => 'azure.ai.openai',
23
- 'gemini' => 'gcp.gen_ai',
24
- 'vertexai' => 'gcp.vertex_ai',
25
- 'mistral' => 'mistral_ai',
26
- 'xai' => 'x_ai'
27
- }.freeze
28
- INTERNAL_OPERATIONS = %w[execute_tool invoke_workflow ruby_llm.workflow_step].freeze
22
+ INTERNAL_OPERATIONS = %w[
23
+ invoke_agent execute_tool invoke_workflow ruby_llm.workflow_step ruby_llm.code_execution
24
+ ].freeze
29
25
 
30
26
  module_function
31
27
 
32
- def span_name(operation, payload)
28
+ def span_name(operation, payload, content: false)
33
29
  target = case operation
34
- when 'execute_tool' then payload[:tool_name]
30
+ when 'invoke_agent' then payload[:agent_name]
31
+ when 'execute_tool' then [payload[:tool_name], *skill(payload, content:).values]
35
32
  when 'invoke_workflow' then payload[:workflow_name]
36
33
  when 'ruby_llm.workflow_step' then payload[:workflow_step_name]
34
+ when 'ruby_llm.code_execution' then payload[:language]
37
35
  else payload[:model]
38
36
  end
39
- [operation, target].compact.join(' ')
37
+ [operation, *target].compact.join(' ')
40
38
  end
41
39
 
42
40
  def span_kind(operation)
43
41
  INTERNAL_OPERATIONS.include?(operation) ? :internal : :client
44
42
  end
45
43
 
46
- def request(operation, payload, event:)
44
+ def request(operation, payload, event:, content: false)
47
45
  return workflow(operation, payload) if %w[invoke_workflow ruby_llm.workflow_step].include?(operation)
48
- return tool(payload) if operation == 'execute_tool'
46
+ return agent(payload) if operation == 'invoke_agent'
47
+ return tool(payload, content:) if operation == 'execute_tool'
48
+ return code_execution_request(payload) if operation == 'ruby_llm.code_execution'
49
49
 
50
50
  {
51
51
  'gen_ai.operation.name' => operation,
52
- 'gen_ai.provider.name' => PROVIDERS.fetch(payload[:provider], payload[:provider]),
52
+ 'gen_ai.provider.name' => provider_name(payload[:provider]),
53
53
  'gen_ai.request.model' => payload[:model],
54
54
  'gen_ai.request.temperature' => payload[:temperature],
55
55
  'gen_ai.request.max_tokens' => payload[:max_output_tokens],
56
56
  'gen_ai.request.stream' => (true if payload[:streaming]),
57
+ 'gen_ai.request.reasoning.level' => reasoning_level(payload[:thinking]),
57
58
  'gen_ai.embeddings.dimension.count' => payload[:dimensions],
58
59
  'gen_ai.output.type' => output_type(event, payload)
59
60
  }.compact
60
61
  end
61
62
 
63
+ def provider_name(slug)
64
+ provider = Provider.resolve(slug) if slug
65
+ provider ? provider.open_telemetry_name : slug
66
+ end
67
+
68
+ def reasoning_level(thinking)
69
+ thinking.effort.to_s if thinking.respond_to?(:effort) && thinking.effort
70
+ end
71
+
62
72
  def output_type(event, payload)
63
73
  return 'json' if payload[:schema]
64
74
  return 'image' if event == 'image.ruby_llm'
@@ -69,10 +79,10 @@ module RubyLLM
69
79
 
70
80
  def response(payload)
71
81
  response = payload[:response]
72
- attributes = {
82
+ attributes = code_execution(payload).merge(
73
83
  'gen_ai.response.model' => payload[:response_model],
74
84
  'gen_ai.response.finish_reasons' => ([response.finish_reason.to_s] if response&.finish_reason)
75
- }.compact
85
+ ).compact
76
86
  attributes.merge!(tokens(payload[:response_tokens] || payload[:tokens])) if payload[:tokens]
77
87
  attributes
78
88
  end
@@ -88,12 +98,51 @@ module RubyLLM
88
98
  }.compact
89
99
  end
90
100
 
91
- def tool(payload)
101
+ def code_execution_request(payload)
102
+ { 'ruby_llm.code_execution.language' => payload[:language].to_s }
103
+ end
104
+
105
+ def code_execution(payload)
106
+ return {} unless payload.key?(:timed_out)
107
+
108
+ {
109
+ 'ruby_llm.code_execution.exit_code' => payload[:exit_code],
110
+ 'ruby_llm.code_execution.timed_out' => payload[:timed_out],
111
+ 'ruby_llm.code_execution.duration' => payload[:duration]
112
+ }
113
+ end
114
+
115
+ def tool(payload, content:)
92
116
  {
93
117
  'gen_ai.operation.name' => 'execute_tool',
94
118
  'gen_ai.tool.name' => payload[:tool_name],
95
119
  'gen_ai.tool.call.id' => payload[:tool_call_id],
96
- 'gen_ai.tool.type' => 'function'
120
+ 'gen_ai.tool.description' => payload[:tool]&.description,
121
+ 'gen_ai.tool.type' => 'function',
122
+ 'gen_ai.agent.name' => payload[:agent_name]
123
+ }.merge(skill(payload, content:)).compact
124
+ end
125
+
126
+ # A skill name the chat does not have, or the file a call asks for,
127
+ # is a model's argument, so it is content.
128
+ def skill(payload, content:)
129
+ loader = payload[:tool]
130
+ arguments = payload[:tool_arguments].to_h.transform_keys(&:to_s)
131
+ name = arguments['skill'].to_s
132
+ return {} unless loader.is_a?(Skill::Loader) && loader.skill?(name)
133
+
134
+ {
135
+ 'gen_ai.skill.name' => name,
136
+ 'gen_ai.skill.resource.name' => (Skill::Loader.resource_name(name, arguments['file']) if content)
137
+ }.compact
138
+ end
139
+
140
+ def agent(payload)
141
+ {
142
+ 'gen_ai.operation.name' => 'invoke_agent',
143
+ 'gen_ai.agent.name' => payload[:agent_name],
144
+ 'gen_ai.agent.description' => payload[:agent_description],
145
+ 'gen_ai.conversation.id' => payload[:chat_id]&.to_s
97
146
  }.compact
98
147
  end
99
148
 
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class OpenTelemetry
5
+ # The opt-in content attributes, in the GenAI semantic conventions'
6
+ # message format. Attachments are described, never sent as bytes.
7
+ module Content # :nodoc:
8
+ module_function
9
+
10
+ def request(operation, payload)
11
+ case operation
12
+ when 'chat' then chat_request(payload[:input_messages] || [])
13
+ when 'execute_tool' then { 'gen_ai.tool.call.arguments' => json(payload[:tool_arguments]) }
14
+ else {}
15
+ end.compact
16
+ end
17
+
18
+ def response(operation, payload)
19
+ case operation
20
+ when 'chat' then chat_response(payload[:response])
21
+ when 'execute_tool' then { 'gen_ai.tool.call.result' => tool_result(payload[:result]) }
22
+ else {}
23
+ end.compact
24
+ end
25
+
26
+ def chat_request(messages)
27
+ system, conversation = messages.partition { |message| message.role == :system }
28
+ {
29
+ 'gen_ai.system_instructions' => (json(system.flat_map { |message| parts(message) }) if system.any?),
30
+ 'gen_ai.input.messages' => (json(conversation.map { |message| message(message) }) if conversation.any?)
31
+ }
32
+ end
33
+
34
+ def chat_response(response)
35
+ return {} unless response.is_a?(Message)
36
+
37
+ output = message(response)
38
+ output[:finish_reason] = response.finish_reason.to_s if response.finish_reason
39
+ { 'gen_ai.output.messages' => json([output]) }
40
+ end
41
+
42
+ def message(message)
43
+ { role: message.role.to_s, parts: parts(message) }
44
+ end
45
+
46
+ def parts(message)
47
+ return tool_parts(message) if message.role == :tool
48
+
49
+ thinking = message.thinking&.text
50
+ [
51
+ ({ type: 'reasoning', content: thinking } if present?(thinking)),
52
+ ({ type: 'text', content: message.content } if present?(message.content)),
53
+ *message.attachments.map { |attachment| attachment(attachment) },
54
+ *message.tool_calls.map { |call| tool_call(call) }
55
+ ].compact
56
+ end
57
+
58
+ def tool_parts(message)
59
+ [{ type: 'tool_call_response', id: message.tool_call_id, response: message.content },
60
+ *message.attachments.map { |attachment| attachment(attachment) }]
61
+ end
62
+
63
+ def tool_call(call)
64
+ { type: 'tool_call', id: call.id, name: call.name, arguments: call.arguments }
65
+ end
66
+
67
+ def attachment(attachment)
68
+ described = { modality: attachment.type.to_s, mime_type: attachment.mime_type }
69
+ if attachment.provider_file?
70
+ { type: 'file', **described, file_id: attachment.provider_file_id }
71
+ elsif attachment.url?
72
+ { type: 'uri', **described, uri: uri_without_credentials(attachment.source) }
73
+ else
74
+ { type: 'attachment', **described, filename: attachment.filename }
75
+ end
76
+ end
77
+
78
+ # What the model receives from the tool, with attachments described.
79
+ def tool_result(result)
80
+ return if result.nil?
81
+
82
+ content, attachments = Tool.split_result(result)
83
+ return content if attachments.empty?
84
+
85
+ json([({ type: 'text', content: } if present?(content)), *attachments.map { |item| attachment(item) }].compact)
86
+ end
87
+
88
+ # Signed URLs carry access credentials in their query string.
89
+ def uri_without_credentials(source)
90
+ uri = URI(source.to_s)
91
+ uri.class.build(host: uri.host, port: uri.port, path: uri.path).to_s
92
+ end
93
+
94
+ def present?(text)
95
+ text.is_a?(String) && !text.empty?
96
+ end
97
+
98
+ def json(value)
99
+ return if value.nil?
100
+
101
+ value.is_a?(String) ? value : JSON.generate(value)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -10,9 +10,16 @@ module RubyLLM
10
10
  # Enables tracing without replacing the configured instrumenter.
11
11
  # Requires the optional opentelemetry-api gem. Repeated calls are harmless.
12
12
  # The application owns the tracer provider, SDK, and exporters.
13
- def enable
13
+ # With <tt>content: true</tt>, spans also carry instructions, messages,
14
+ # model output, and tool arguments and results.
15
+ #
16
+ # RubyLLM::OpenTelemetry.enable
17
+ # RubyLLM::OpenTelemetry.enable(content: true)
18
+ #
19
+ def enable(content: false)
14
20
  require 'opentelemetry-api'
15
21
  @mutex.synchronize do
22
+ @content = content
16
23
  @subscriber ||= new
17
24
  Support::Instrumentation.subscribe(@subscriber)
18
25
  end
@@ -27,16 +34,22 @@ module RubyLLM
27
34
  @mutex.synchronize { Support::Instrumentation.unsubscribe(@subscriber) if @subscriber }
28
35
  nil
29
36
  end
37
+
38
+ def content? # :nodoc:
39
+ @content == true
40
+ end
30
41
  end
31
42
 
32
43
  def instrument(name, payload) # :nodoc:
33
44
  operation = Attributes::OPERATIONS[name]
34
45
  return yield unless operation
35
46
 
47
+ content = self.class.content?
36
48
  span = safely do
37
- tracer.start_span(Attributes.span_name(operation, payload),
38
- kind: Attributes.span_kind(operation),
39
- attributes: Attributes.request(operation, payload, event: name))
49
+ attributes = Attributes.request(operation, payload, event: name, content:)
50
+ attributes.merge!(Content.request(operation, payload)) if content
51
+ span_name = Attributes.span_name(operation, payload, content:)
52
+ tracer.start_span(span_name, kind: Attributes.span_kind(operation), attributes:)
40
53
  end
41
54
  return yield unless span
42
55
 
@@ -47,6 +60,7 @@ module RubyLLM
47
60
  raise
48
61
  ensure
49
62
  safely { span.add_attributes(Attributes.response(payload)) }
63
+ safely { span.add_attributes(Content.response(operation, payload)) } if content
50
64
  safely { span.finish }
51
65
  end
52
66
  end
@@ -7,7 +7,7 @@
7
7
  # RubyLLM lexical scope: +Chat+ in a template is the application's model,
8
8
  # not RubyLLM::Chat.
9
9
  class RubyLLM::Prompt::Context # rubocop:disable Style/ClassAndModuleChildren
10
- LOCAL_VARIABLE_NAME = /\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/
10
+ LOCAL_VARIABLE_NAME = /\A(?![[:upper:]\p{Lt}0-9])[[:alnum:]_[:^ascii:]]+\z/
11
11
  private_constant :LOCAL_VARIABLE_NAME
12
12
 
13
13
  # Returns the locals passed to the current prompt or partial. Use it to
@@ -46,7 +46,7 @@ class RubyLLM::Prompt::Context # rubocop:disable Style/ClassAndModuleChildren
46
46
 
47
47
  def partial(name)
48
48
  name = name.to_s
49
- partial = name.sub(%r{([^/]+)\z}, '_\\1')
49
+ partial = name.sub(%r{(\A|/)([^/]+)\z}, '\\1_\\2')
50
50
  directory = File.dirname(@prompt.name)
51
51
  return RubyLLM::Prompt.new(partial) if name.include?('/') || directory == '.'
52
52
 
@@ -6,13 +6,15 @@ require 'pathname'
6
6
  module RubyLLM
7
7
  # Resolves prompt templates from the application and registered engine directories.
8
8
  class Prompt
9
- # The ordered directories searched for prompt files. The application's
10
- # prompt directory is always first, so an application can override any
11
- # prompt an engine ships by placing a file at the same relative path.
12
- # Engines append their own directory in an initializer:
9
+ # The ordered directories searched for prompt files, and for skills
10
+ # through Skill.roots. The application's directory is always first, so
11
+ # an application can override any prompt or skill an engine ships by
12
+ # placing its own at the same relative path. Engines append their own
13
+ # directories in an initializer:
13
14
  #
14
15
  # initializer "my_engine.prompts" do
15
16
  # RubyLLM::Prompt.roots << MyEngine::Engine.root.join("app/prompts")
17
+ # RubyLLM::Skill.roots << MyEngine::Engine.root.join("app/skills")
16
18
  # end
17
19
  #
18
20
  class Roots
@@ -50,7 +52,7 @@ module RubyLLM
50
52
  def render(**locals) # :nodoc:
51
53
  raise PromptNotFoundError, "Prompt file not found: #{path}" unless File.exist?(path)
52
54
 
53
- ERB.new(File.read(path)).result(Context.new(self, locals).scope)
55
+ ERB.new(File.read(path, encoding: Encoding::UTF_8), trim_mode: '-').result(Context.new(self, locals).scope)
54
56
  end
55
57
 
56
58
  def self.render(name, **locals) # :nodoc:
@@ -8,14 +8,8 @@ module RubyLLM
8
8
  fallback = Streaming::StreamState.new
9
9
  on_data = binary_on_data(fallback, progress, &)
10
10
 
11
- response = @connection.post(url, payload, usage: @usage_tracker, stream: true) do |request|
12
- (request.options.context ||= {})[Transport::Connection::STREAM_PROGRESS_KEY] = progress
13
- if faraday_1?
14
- request.options[:on_data] = on_data
15
- else
16
- request.options.on_data = on_data
17
- end
18
- end
11
+ response = post_stream(url, payload, on_data:, progress:, on_reset: fallback.method(:reset),
12
+ on_failure: stream_failure(fallback))
19
13
  state = response.env[:streaming_state] || fallback
20
14
  validate_binary_response(response, state.buffer)
21
15
  yield state.buffer.dup unless progress[:started] || state.buffer.empty?