ruby_llm 2.1.1 → 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 +160 -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 +515 -274
  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 +1593 -331
  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 +50 -24
  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 +127 -106
  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 +15 -15
  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 +1 -7
  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 +5 -14
  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 +213 -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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module RubyLLM
6
+ module Transport
7
+ # Sits outside everything on a connection without retries, so an error
8
+ # leaves it without the credentials of the request it answers.
9
+ class ReleaseRequest < Faraday::Middleware # :nodoc:
10
+ # The response outlives the call as Message#raw, Error#response, and
11
+ # other results, which applications log and report. The request holds
12
+ # the API key in its headers, the whole serialized conversation in its
13
+ # body, and the provider and signer in its context, and a streaming
14
+ # callback closes over the payload it was built from.
15
+ def self.release(response)
16
+ response.delete(:request) if response.is_a?(Hash)
17
+ env = response.respond_to?(:env) ? response.env : response
18
+ return response unless env.is_a?(Faraday::Env)
19
+
20
+ env.request_body = nil
21
+ env.request_headers = Faraday::Utils::Headers.new
22
+ env.request.on_data = nil
23
+ env.request.context = nil
24
+ response
25
+ end
26
+
27
+ def call(env)
28
+ @app.call(env)
29
+ rescue Error, Faraday::Error => e
30
+ self.class.release(e.response)
31
+ raise
32
+ end
33
+ end
34
+ end
35
+ end
@@ -3,6 +3,7 @@
3
3
  require 'socket'
4
4
  require 'openssl'
5
5
  require 'io/wait'
6
+ require 'timeout'
6
7
 
7
8
  module RubyLLM
8
9
  module Transport # :nodoc:
@@ -27,9 +28,11 @@ module RubyLLM
27
28
  raise ArgumentError, 'WebSocket URL must not contain credentials or a fragment'
28
29
  end
29
30
  raise ArgumentError, 'WebSocket connections do not support HTTP proxies' if config.http_proxy
31
+ raise ArgumentError, 'WebSocket connections do not support TLS options' if config.ssl
30
32
 
31
33
  @headers = headers
32
34
  @timeout = config.request_timeout
35
+ @connect_timeout = config.connect_timeout || @timeout
33
36
  @messages = []
34
37
  @write_lock = Mutex.new
35
38
  end
@@ -41,7 +44,7 @@ module RubyLLM
41
44
  configure_driver
42
45
  @driver.start
43
46
  read_frame(deadline) until @opened || @closed || @error
44
- raise @error if @error
47
+ raise @error if @error && !@opened
45
48
  raise Error, 'WebSocket closed before opening' unless @opened
46
49
 
47
50
  self
@@ -63,26 +66,13 @@ module RubyLLM
63
66
  def read(timeout: @timeout)
64
67
  deadline = monotonic_time + timeout
65
68
  read_frame(deadline) while @messages.empty? && !@closed && !@error
66
- raise @error if @error
69
+ raise_unread_error
67
70
 
68
71
  @messages.shift
69
72
  rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
70
- raise unless @closed
71
- end
72
-
73
- def read_available
74
- ensure_open
75
- until @messages.any? || @closed || @error
76
- break unless read_available_frame
77
- end
78
- ensure_open
73
+ raise unless @closed || @messages.any?
79
74
 
80
75
  @messages.shift
81
- rescue EOFError, Errno::ECONNRESET
82
- @closed = true
83
- raise Error, 'WebSocket connection ended without a close frame'
84
- rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
85
- raise unless @closed
86
76
  end
87
77
 
88
78
  def each_message(write:)
@@ -130,16 +120,6 @@ module RubyLLM
130
120
 
131
121
  private
132
122
 
133
- def read_available_frame
134
- data = @socket.read_nonblock(16_384, exception: false)
135
- return if %i[wait_readable wait_writable].include?(data)
136
-
137
- raise EOFError unless data
138
-
139
- @driver.parse(data)
140
- data
141
- end
142
-
143
123
  def finish_writer(writer)
144
124
  raise Error, 'WebSocket closed before outgoing messages finished' unless writer.join(1)
145
125
  end
@@ -158,8 +138,20 @@ module RubyLLM
158
138
 
159
139
  def open_socket(deadline)
160
140
  port = @uri.port || (@uri.scheme == 'wss' ? 443 : 80)
161
- @socket = Socket.tcp(@uri.host, port, connect_timeout: @timeout)
162
- secure_socket(deadline) if @uri.scheme == 'wss'
141
+ opened_by = [monotonic_time + @connect_timeout, deadline].min
142
+ @socket = connect_socket(port, opened_by)
143
+ secure_socket(opened_by) if @uri.scheme == 'wss'
144
+ end
145
+
146
+ # Socket.tcp times the lookup and each address on their own, so only
147
+ # the outer timeout holds the whole connect to one deadline.
148
+ def connect_socket(port, opened_by)
149
+ remaining = opened_by - monotonic_time
150
+ raise Error, 'WebSocket operation timed out' unless remaining.positive?
151
+
152
+ Timeout.timeout(remaining, Error, 'WebSocket operation timed out') do
153
+ Socket.tcp(@uri.hostname, port, connect_timeout: remaining, resolv_timeout: remaining)
154
+ end
163
155
  end
164
156
 
165
157
  def close_socket
@@ -176,11 +168,15 @@ module RubyLLM
176
168
  context = OpenSSL::SSL::SSLContext.new
177
169
  context.set_params
178
170
  socket = OpenSSL::SSL::SSLSocket.new(@socket, context)
179
- socket.hostname = @uri.host
171
+ socket.hostname = @uri.hostname
180
172
  socket.sync_close = true
181
173
  @socket = socket
182
174
  socket_operation(deadline) { @socket.connect_nonblock }
183
- @socket.post_connection_check(@uri.host)
175
+ @socket.post_connection_check(@uri.hostname)
176
+ end
177
+
178
+ def raise_unread_error
179
+ raise @error if @error && @messages.empty?
184
180
  end
185
181
 
186
182
  def ensure_open
@@ -41,7 +41,9 @@ module RubyLLM
41
41
  # The provider-reported processing status of the file.
42
42
  attr_reader :status
43
43
 
44
- # The MIME type of the stored file.
44
+ # The MIME type of the stored file. OpenAI, Azure, DeepSeek, Meta, and
45
+ # xAI do not report one: ::upload returns the content type it sent, and
46
+ # ::find returns +nil+.
45
47
  attr_reader :mime_type
46
48
 
47
49
  # The purpose the file was uploaded for, such as <tt>"batch"</tt>, when
@@ -91,16 +93,23 @@ module RubyLLM
91
93
  # RubyLLM::UploadedFile.upload(io, provider: :openai, purpose: "batch",
92
94
  # filename: "batch.jsonl")
93
95
  #
94
- # OpenAI and Azure require +purpose:+. Cohere uses +purpose:+ for its
96
+ # OpenAI, Meta, and Azure require +purpose:+. Cohere uses +purpose:+ for its
95
97
  # dataset type, such as <tt>"embed-input"</tt>.
96
98
  # Pass +expires_in:+ as a number of
97
99
  # seconds to have the provider delete the file automatically; OpenAI,
98
- # xAI, and Mistral support it, and Mistral rounds up to whole hours.
100
+ # Meta, Anthropic, xAI, and Mistral support it, and Mistral rounds up to
101
+ # whole hours.
99
102
  # Storage-backed providers (Vertex AI and Bedrock) store the file at
100
103
  # +uri:+ when given, otherwise at a generated object name under the
101
- # configured bucket. +content_type:+ overrides the MIME type RubyLLM
102
- # detects. +provider_options:+ carries the rest in the provider's own
103
- # vocabulary: +visibility:+ on Mistral, +display_name:+ on Gemini.
104
+ # configured bucket. Providers that cannot carry +purpose:+,
105
+ # +expires_in:+, or +uri:+ raise ArgumentError. +content_type:+
106
+ # overrides the MIME type RubyLLM detects. +provider_options:+
107
+ # carries the rest in the provider's own vocabulary: multipart form
108
+ # fields on most providers, such as +visibility:+ on Mistral, File
109
+ # resource fields on Gemini, such as +display_name:+, PutObject keywords
110
+ # on Bedrock, and +create_file+ keywords on Vertex AI. RubyLLM sends them
111
+ # as given, so the provider rejects what it does not recognize. They
112
+ # cannot replace the file itself.
104
113
  def self.upload(file, provider: nil, context: nil, filename: nil, purpose: nil, expires_in: nil,
105
114
  uri: nil, content_type: nil, provider_options: {})
106
115
  options = { filename:, purpose:, expires_in:, uri:, content_type: }.compact
@@ -2,5 +2,5 @@
2
2
 
3
3
  module RubyLLM
4
4
  # The version of the ruby_llm gem, as a string.
5
- VERSION = '2.1.1'
5
+ VERSION = '2.2.0'
6
6
  end
@@ -9,6 +9,8 @@ module RubyLLM
9
9
  #
10
10
  class Video
11
11
  include Support::Inspectable
12
+ include Files::Saving
13
+ include Accounting::Usage::Result
12
14
 
13
15
  # The URL of the hosted video, for providers that return one, or +nil+.
14
16
  attr_reader :url
@@ -50,9 +52,9 @@ module RubyLLM
50
52
  # the request as-is. +context:+ supplies a Context whose configuration
51
53
  # replaces the global one. +metadata:+ is included in the
52
54
  # instrumentation payload. +owner:+ attributes the usage to a record,
53
- # such as a user, and wins over RubyLLM.with_usage_owner.
55
+ # such as a user, and wins over RubyLLM.with_owner.
54
56
  #
55
- # video = RubyLLM.animate("a hummingbird in slow motion", model: "veo-3.1-fast-generate-preview")
57
+ # video = RubyLLM.animate("a hummingbird in slow motion", model: "gemini-omni-1.2-flash")
56
58
  #
57
59
  # RubyLLM.animate(
58
60
  # "Make the waterfall crash down",
@@ -72,6 +74,7 @@ module RubyLLM
72
74
  metadata: nil,
73
75
  owner: nil)
74
76
  config = context&.config || RubyLLM.config
77
+ model ||= config.default_video_model
75
78
  payload = { model:, prompt:, provider_options:, metadata: }
76
79
 
77
80
  RubyLLM.instrument('video.ruby_llm', payload, config: config) do |event|
@@ -104,23 +107,44 @@ module RubyLLM
104
107
  @raw = raw
105
108
  end
106
109
 
107
- # Returns the raw binary video bytes, downloading from #url when the
108
- # provider returned a hosted video.
110
+ # Returns the usage the provider reported for the job that rendered
111
+ # the video.
112
+ def tokens
113
+ return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
114
+
115
+ Tokens.new
116
+ end
117
+
118
+ # Returns the Cost of the job that rendered the video: the amount the
119
+ # provider reported, or its reported tokens at registry prices. Its total
120
+ # is +nil+ when the provider reports neither a price nor priced tokens.
109
121
  #
110
- # video_bytes = video.to_blob
122
+ # video = RubyLLM.animate("a paper boat")
123
+ # video.cost.total # => 0.05
111
124
  #
112
- def to_blob
113
- data || Transport::Connection.basic(config).get(url).body
125
+ def cost
126
+ return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
127
+
128
+ Cost.new(tokens:)
114
129
  end
115
130
 
131
+ ##
132
+ # :method: save
133
+ # :call-seq:
134
+ # save(path)
135
+ #
116
136
  # Writes the binary video to +path+, expanding it first. Returns
117
137
  # +path+ as given.
118
138
  #
119
139
  # video.save("clip.mp4")
140
+
141
+ # Returns the raw binary video bytes, downloading from #url when the
142
+ # provider returned a hosted video.
120
143
  #
121
- def save(path)
122
- File.binwrite(File.expand_path(path), to_blob)
123
- path
144
+ # video_bytes = video.to_blob
145
+ #
146
+ def to_blob
147
+ data || Transport::Connection.download(url, config)
124
148
  end
125
149
 
126
150
  private
@@ -11,6 +11,7 @@ module RubyLLM
11
11
  #
12
12
  class VideoJob
13
13
  include Support::Inspectable
14
+ include Support::DeprecatedJobPredicates
14
15
 
15
16
  # The provider's id for the job.
16
17
  attr_reader :id
@@ -28,23 +29,23 @@ module RubyLLM
28
29
  attr_reader :raw
29
30
 
30
31
  # The clip length in seconds the request asked for, or +nil+ when it
31
- # left the length to the provider's default.
32
+ # left the length to the provider's default or the job came from ::find.
32
33
  attr_reader :duration
33
34
 
34
35
  # The resolution the request asked for, as the provider spells it,
35
36
  # such as <tt>"720p"</tt> or <tt>"1280x720"</tt>, or +nil+ when it left
36
- # the resolution to the provider's default.
37
+ # the resolution to the provider's default or the job came from ::find.
37
38
  attr_reader :resolution
38
39
 
39
40
  # Submits a video generation job and returns a VideoJob without
40
41
  # waiting for the result. Most code calls this through
41
42
  # RubyLLM.animate_later. Takes the same arguments as Video.animate.
42
43
  # The job records its usage, attributed to +owner:+ or the owner of
43
- # RubyLLM.with_usage_owner, once it finishes.
44
+ # RubyLLM.with_owner, once it finishes.
44
45
  #
45
46
  # job = RubyLLM.animate_later("a hummingbird in slow motion")
46
- # job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
47
- # job.done? # => false
47
+ # job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
48
+ # job.complete? # => false
48
49
  #
49
50
  def self.animate_later(prompt = nil,
50
51
  model: nil,
@@ -56,31 +57,32 @@ module RubyLLM
56
57
  provider_options: {},
57
58
  metadata: nil,
58
59
  owner: nil)
59
- config = context&.config || RubyLLM.config
60
- model ||= config.default_video_model
61
- model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
62
- config: config)
63
- payload = {
64
- provider: provider_instance.slug,
65
- provider_class: provider_instance.name,
66
- model: model.id,
67
- model_info: model,
68
- prompt: prompt,
69
- provider_options: provider_options,
70
- metadata: metadata
71
- }
72
-
73
- RubyLLM.instrument('video_job.ruby_llm', payload, config: config) do |event|
74
- job = Accounting::Usage.owned_by(owner) do
75
- provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
76
- end
77
- event[:job_id] = job.id
78
- job
60
+ Accounting::Operation.new(
61
+ 'video_job', model, default: :default_video_model, usage: false, provider:, assume_model_exists:, context:,
62
+ owner:, report: ->(event, job) { event[:job_id] = job.id }
63
+ ).run(prompt:, provider_options:, metadata:) do |provider_instance, model|
64
+ provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
79
65
  end
80
66
  end
81
67
 
68
+ # Returns a VideoJob reflecting the provider's current state for +id+.
69
+ # Use it to pick a job back up from any process. Pass the #model that
70
+ # rendered it: it selects the provider and protocol, and names the
71
+ # usage entry. A job already finished reports its usage here, once per
72
+ # VideoJob object, attributed to the owner of RubyLLM.with_owner.
73
+ #
74
+ # job = RubyLLM::VideoJob.find(clip.video_job_id, model: clip.video_model, provider: :xai)
75
+ # job.video&.save("clip.mp4")
76
+ #
77
+ # Pass +context:+ to use a Context in place of the global configuration.
78
+ def self.find(id, model:, provider: nil, assume_model_exists: false, context: nil)
79
+ config = context&.config || RubyLLM.config
80
+ model, provider_instance = Models.resolve(model, provider:, assume_model_exists:, config:)
81
+ provider_instance.find_video_job(id, model:)
82
+ end
83
+
82
84
  def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil, # :nodoc:
83
- reported_cost: nil, duration: nil, resolution: nil)
85
+ reported_cost: nil, tokens: nil, duration: nil, resolution: nil)
84
86
  @id = id
85
87
  @protocol = protocol
86
88
  @model = model
@@ -88,9 +90,11 @@ module RubyLLM
88
90
  @raw = raw
89
91
  @error = error
90
92
  @reported_cost = reported_cost
93
+ @tokens = tokens
91
94
  @duration = duration
92
95
  @resolution = resolution
93
96
  @usage_owner = Accounting::Usage.owner
97
+ @usage_workflow = Accounting::Usage.workflow
94
98
  record_usage
95
99
  end
96
100
 
@@ -100,12 +104,12 @@ module RubyLLM
100
104
  end
101
105
 
102
106
  # Returns +true+ once the job finished, successfully or not.
103
- def done?
107
+ def complete?
104
108
  !pending?
105
109
  end
106
110
 
107
111
  # Returns +true+ when the job finished with a video.
108
- def completed?
112
+ def succeeded?
109
113
  status == :completed
110
114
  end
111
115
 
@@ -116,28 +120,29 @@ module RubyLLM
116
120
  end
117
121
 
118
122
  # Re-fetches the job from the provider, updating #status, #error, and
119
- # #raw. Does nothing once the job is #done?. Returns self.
123
+ # #raw. Does nothing once the job is #complete?. Returns self.
120
124
  def refresh
121
- return self if done?
125
+ return self if complete?
122
126
 
123
127
  state = @protocol.refresh_video_job(self)
124
128
  @status = state.fetch(:status)
125
129
  @raw = state[:raw]
126
130
  @error = state[:error]
127
131
  @reported_cost = state[:reported_cost]
132
+ @tokens = state[:tokens]
128
133
  record_usage
129
134
  self
130
135
  end
131
136
 
132
- # Returns the Cost the provider reported for the job. Its total is
133
- # +nil+ while the job is pending or when the provider reports no
134
- # price.
137
+ # Returns the Cost of the job: the amount the provider reported, or its
138
+ # reported tokens at registry prices. Its total is +nil+ while the job is
139
+ # pending, or when the provider reports neither a price nor priced tokens.
135
140
  #
136
141
  # job.wait
137
142
  # job.cost.total # => 0.05
138
143
  #
139
144
  def cost
140
- Cost.new(tokens: Tokens.new(reported_cost: @reported_cost))
145
+ Cost.new(tokens: usage_tokens, model: (@protocol.model if @tokens))
141
146
  end
142
147
 
143
148
  # Polls the job until it finishes, then returns self. Raises Error
@@ -149,7 +154,7 @@ module RubyLLM
149
154
  interval ||= @protocol.config.video_generation_poll_interval
150
155
  deadline = monotonic_time + timeout
151
156
 
152
- until done?
157
+ until complete?
153
158
  remaining = deadline - monotonic_time
154
159
  raise Error, "Video generation timed out after #{timeout} seconds" unless remaining.positive?
155
160
 
@@ -166,20 +171,28 @@ module RubyLLM
166
171
  # Error when #failed?.
167
172
  def video
168
173
  raise Error, "Video generation failed: #{error}" if failed?
169
- return unless completed?
174
+ return unless succeeded?
170
175
 
171
- @video ||= @protocol.download_video(self).tap { |video| video.config = @protocol.config }
176
+ @video ||= @protocol.download_video(self).tap do |video|
177
+ video.config = @protocol.config
178
+ video.ruby_llm_usage_entries = @usage_entry
179
+ end
172
180
  end
173
181
 
174
182
  private
175
183
 
176
184
  def record_usage
177
- return if pending? || @usage_recorded
185
+ return if pending? || @usage_entry
186
+
187
+ @usage_entry = Accounting::Usage::Entry.new(
188
+ operation: :video, provider: @protocol.provider.slug, model:, status: succeeded? ? :succeeded : :failed,
189
+ tokens: usage_tokens, cost:, owner: @usage_owner, workflow: @usage_workflow
190
+ )
191
+ Accounting::Usage.report(@usage_entry, config: @protocol.config)
192
+ end
178
193
 
179
- @usage_recorded = true
180
- entry = Accounting::Usage::Entry.new(operation: :video, provider: @protocol.provider.slug, model:,
181
- status: completed? ? :succeeded : :failed, cost:, owner: @usage_owner)
182
- Accounting::Usage.report(entry, config: @protocol.config)
194
+ def usage_tokens
195
+ @tokens || Tokens.new(reported_cost: @reported_cost)
183
196
  end
184
197
 
185
198
  def monotonic_time
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A WebSource is a page a hosted web search returned. They appear on
5
+ # Message#web_sources, normalized across providers. A Citation is different:
6
+ # it links a span of the answer to what supports it, so a search usually
7
+ # has more sources than citations.
8
+ #
9
+ # response = chat.with_provider_tools(:web_search).ask "What changed in Ruby 4.0?"
10
+ # response.web_sources.map(&:url) # => ["https://www.ruby-lang.org/en/news/", ...]
11
+ #
12
+ # An address here comes from the outside world. See Message#web_sources
13
+ # before treating one as safe to open.
14
+ #
15
+ class WebSource
16
+ include Support::Inspectable
17
+
18
+ # The address the provider reports for the page.
19
+ attr_reader :url
20
+
21
+ # The title the provider reports for the page, or +nil+.
22
+ attr_reader :title
23
+
24
+ def initialize(url:, title: nil) # :nodoc:
25
+ @url = url
26
+ @title = title
27
+ end
28
+
29
+ # Returns the source as a Hash with symbol keys, omitting a +nil+ title.
30
+ def to_h
31
+ { url: url, title: title }.compact
32
+ end
33
+
34
+ def inspect_attributes # :nodoc:
35
+ { url: url, title: title }
36
+ end
37
+ end
38
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Adds workflow and step context to RubyLLM instrumentation while leaving
5
- # orchestration in ordinary Ruby code.
4
+ # Adds workflow and step context to RubyLLM instrumentation and usage
5
+ # records while leaving orchestration in ordinary Ruby code.
6
6
  #
7
7
  # Create workflows through RubyLLM.workflow rather than constructing this
8
8
  # class directly: