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
@@ -11,31 +11,31 @@ module RubyLLM
11
11
  attachment.provider_file? || %i[image pdf document text].include?(attachment.type)
12
12
  end
13
13
 
14
- def format_content(content, attachments = [])
14
+ def render_content(content, attachments = [])
15
15
  return content if attachments.empty?
16
16
 
17
17
  parts = []
18
18
  parts << { type: 'input_text', text: content } if content
19
- attachments.each { |attachment| parts << format_attachment(attachment) }
19
+ attachments.each { |attachment| parts << render_attachment(attachment) }
20
20
  parts
21
21
  end
22
22
 
23
- def format_attachment(attachment)
23
+ def render_attachment(attachment)
24
24
  raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
25
25
 
26
- return format_provider_file(attachment) if attachment.provider_file?
26
+ return render_provider_file(attachment) if attachment.provider_file?
27
27
 
28
28
  case attachment.type
29
29
  when :image
30
- format_image(attachment)
30
+ render_image(attachment)
31
31
  when :pdf, :document
32
- format_document(attachment)
32
+ render_document(attachment)
33
33
  when :text
34
34
  { type: 'input_text', text: attachment.for_llm }
35
35
  end
36
36
  end
37
37
 
38
- def format_image(image, image_url: image.url_or_data_uri, original_detail: @provider&.original_image_detail?)
38
+ def render_image(image, image_url: image.url_or_data_uri, original_detail: @provider&.original_image_detail?)
39
39
  part = { type: 'input_image', image_url: }
40
40
  return part unless image.resolution
41
41
 
@@ -45,7 +45,9 @@ module RubyLLM
45
45
  # The Responses API extracts text from documents, presentations, and
46
46
  # spreadsheets as well as PDFs, so every document attachment rides
47
47
  # along as a native input_file.
48
- def format_document(document)
48
+ def render_document(document)
49
+ return { type: 'input_file', file_url: document.source.to_s } if document.url? && document_urls?
50
+
49
51
  {
50
52
  type: 'input_file',
51
53
  filename: document.filename,
@@ -53,7 +55,13 @@ module RubyLLM
53
55
  }
54
56
  end
55
57
 
56
- def format_provider_file(file)
58
+ # The provider fetches the URL itself. Dialects opt in once a live
59
+ # request shows their endpoint downloads input_file URLs.
60
+ def document_urls?
61
+ false
62
+ end
63
+
64
+ def render_provider_file(file)
57
65
  {
58
66
  type: 'input_file',
59
67
  file_id: file.provider_file_id
@@ -6,17 +6,15 @@ module RubyLLM
6
6
  # Streaming methods of the OpenAI Responses API. Events are semantic:
7
7
  # each SSE data frame carries a `type` describing what changed.
8
8
  module Streaming
9
- ERROR_STATUSES = {
10
- 'server_error' => 500,
11
- 'rate_limit_exceeded' => 429,
12
- 'too_many_requests' => 429,
13
- 'insufficient_quota' => 429
14
- }.freeze
9
+ ERROR_STATUSES = ChatCompletions::Streaming::ERROR_STATUSES.merge('too_many_requests' => 429).freeze
15
10
 
16
11
  module_function
17
12
 
18
13
  def stream_response(...)
19
14
  @citation_content_lengths = Hash.new(0)
15
+ @citation_markers = {}
16
+ @reasoning_output_index = nil
17
+ @text_output_index = nil
20
18
  super
21
19
  end
22
20
 
@@ -28,12 +26,16 @@ module RubyLLM
28
26
  chunk thinking: Thinking.build(text: data['delta'])
29
27
  when 'response.reasoning_summary_part.added'
30
28
  build_reasoning_summary_part_chunk(data)
29
+ when 'response.output_text.done', 'response.refusal.done'
30
+ build_text_done_chunk(data)
31
31
  when 'response.output_text.annotation.added'
32
32
  build_annotation_chunk(data)
33
+ when 'response.content_part.added'
34
+ build_content_part_added_chunk(data)
33
35
  when 'response.output_item.added'
34
36
  build_item_added_chunk(data)
35
37
  when 'response.function_call_arguments.delta'
36
- chunk tool_calls: { data['output_index'] => ToolCall.new(id: nil, name: nil, arguments: data['delta']) }
38
+ chunk tool_calls: { data['output_index'] => ToolCall.new(name: nil, arguments: data['delta']) }
37
39
  when 'response.output_item.done'
38
40
  build_item_done_chunk(data)
39
41
  when 'response.completed', 'response.incomplete'
@@ -46,9 +48,17 @@ module RubyLLM
46
48
  end
47
49
 
48
50
  def build_text_chunk(data)
51
+ text_content_chunk(data, citation_markers(data).feed(data['delta']))
52
+ end
53
+
54
+ def build_text_done_chunk(data)
55
+ text_content_chunk(data, citation_markers(data).finish)
56
+ end
57
+
58
+ def text_content_chunk(data, content)
49
59
  @citation_content_lengths ||= Hash.new(0)
50
- @citation_content_lengths[citation_content_position(data)] += data['delta'].to_s.length
51
- chunk content: data['delta']
60
+ @citation_content_lengths[citation_content_position(data)] += content.to_s.length
61
+ chunk content:
52
62
  end
53
63
 
54
64
  def build_annotation_chunk(data)
@@ -57,7 +67,18 @@ module RubyLLM
57
67
  (key <=> position).negative? ? length : 0
58
68
  end
59
69
 
60
- chunk citations: offset_citations(parse_annotations([data['annotation']], nil), offset, nil)
70
+ chunk citations: offset_citations(parse_annotations([data['annotation']], nil), offset, nil,
71
+ citation_markers(data))
72
+ end
73
+
74
+ def citation_markers(data)
75
+ @citation_markers ||= {}
76
+ @citation_markers[citation_content_position(data)] ||= CitationMarkers.new
77
+ end
78
+
79
+ def stream_remainder
80
+ content = (@citation_markers || {}).values.filter_map(&:finish).join
81
+ chunk content: content unless content.empty?
61
82
  end
62
83
 
63
84
  def citation_content_position(data)
@@ -65,7 +86,9 @@ module RubyLLM
65
86
  end
66
87
 
67
88
  def build_reasoning_summary_part_chunk(data)
68
- return chunk unless data['summary_index'].positive?
89
+ earlier_item = @reasoning_output_index && @reasoning_output_index != data['output_index']
90
+ @reasoning_output_index = data['output_index']
91
+ return chunk unless earlier_item || data['summary_index'].positive?
69
92
 
70
93
  chunk thinking: Thinking.build(text: "\n\n")
71
94
  end
@@ -75,17 +98,51 @@ module RubyLLM
75
98
  return chunk unless item['type'] == 'function_call'
76
99
 
77
100
  chunk tool_calls: {
78
- data['output_index'] => ToolCall.new(id: item['call_id'], name: item['name'], arguments: +'')
101
+ data['output_index'] => ToolCall.new(provider_call_id: item['call_id'], name: item['name'],
102
+ namespace: item['namespace'], arguments: +'')
79
103
  }
80
104
  end
81
105
 
106
+ def build_content_part_added_chunk(data)
107
+ return chunk unless Chat::MESSAGE_TEXT_KEYS.key?(data.dig('part', 'type'))
108
+
109
+ output_index = data['output_index']
110
+ earlier_item = @text_output_index && @text_output_index != output_index
111
+ @text_output_index = output_index
112
+ return chunk unless earlier_item
113
+
114
+ @citation_content_lengths[[output_index, -1]] += Chat::MESSAGE_SEPARATOR.length
115
+ chunk content: Chat::MESSAGE_SEPARATOR
116
+ end
117
+
82
118
  def build_item_done_chunk(data)
83
119
  item = data['item']
120
+ return build_function_call_done_chunk(data) if item['type'] == 'function_call'
84
121
  return chunk unless item['type'] == 'reasoning' && item['encrypted_content']
85
122
 
86
123
  chunk thinking: Thinking.build(text: nil, signature: item['encrypted_content'])
87
124
  end
88
125
 
126
+ def build_function_call_done_chunk(data)
127
+ item = data['item']
128
+ arguments = completed_function_call_arguments(item['arguments'])
129
+ return chunk unless arguments
130
+
131
+ chunk tool_calls: {
132
+ data['output_index'] => ToolCall.new(provider_call_id: item['call_id'], name: item['name'],
133
+ namespace: item['namespace'], arguments:)
134
+ }
135
+ end
136
+
137
+ def completed_function_call_arguments(arguments)
138
+ return unless arguments.is_a?(String)
139
+
140
+ parsed = JSON.parse(arguments)
141
+ parsed if parsed.is_a?(Hash)
142
+ rescue JSON::ParserError
143
+ nil
144
+ end
145
+
89
146
  def build_final_chunk(data)
90
147
  response = data['response'] || {}
91
148
  output = response['output'] || []
@@ -94,9 +151,11 @@ module RubyLLM
94
151
  chunk model: response['model'],
95
152
  tool_calls: parse_tool_approvals(output, finish_reason: parse_finish_reason(response)),
96
153
  finish_reason: parse_finish_reason(response),
154
+ content_filters: ChatCompletions::ContentFilters.parse_content_filters(response),
97
155
  citations: parse_citations(response, output, nil),
98
156
  server_tool_calls: server_tool_calls,
99
157
  raw_content: keep_raw_output?(output, server_tool_calls) ? output : nil,
158
+ phase: parse_phase(output),
100
159
  **parse_usage(response['usage'] || {}),
101
160
  server_tool_use: parse_server_tool_use(response)
102
161
  end
@@ -14,7 +14,7 @@ module RubyLLM
14
14
  end
15
15
 
16
16
  def render_count_tokens_payload(messages, model:, **)
17
- render_payload(messages, model: model, temperature: nil, **).slice(*COUNT_TOKENS_KEYS)
17
+ apply_tool_search(render_payload(messages, model: model, temperature: nil, **)).slice(*COUNT_TOKENS_KEYS)
18
18
  end
19
19
 
20
20
  def parse_count_tokens_response(response)
@@ -9,6 +9,7 @@ module RubyLLM
9
9
  module_function
10
10
 
11
11
  NATIVE_TOOL_SEARCH = { type: 'tool_search' }.freeze
12
+ RESERVED_NAMESPACES = %w[computer browser].freeze
12
13
 
13
14
  def tool_for(tool)
14
15
  definition = {
@@ -25,10 +26,36 @@ module RubyLLM
25
26
  RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
26
27
  end
27
28
 
29
+ def render_tools(tools)
30
+ ordinary, namespaced = tools.values.partition { |tool| tool.namespace.nil? }
31
+ groups = namespaced.group_by(&:namespace).map do |namespace, members|
32
+ if RESERVED_NAMESPACES.include?(namespace)
33
+ raise Error, "The namespace \"#{namespace}\" is reserved by the Responses API, which drops its " \
34
+ 'functions; choose another namespace'
35
+ end
36
+
37
+ { type: 'namespace', name: namespace, description: members.filter_map(&:namespace_description).first,
38
+ tools: members.map { |tool| tool_for(tool) } }.compact
39
+ end
40
+ ordinary.map { |tool| tool_for(tool) } + groups
41
+ end
42
+
28
43
  def deferred?(tool)
29
44
  tool.is_a?(RubyLLM::Tool::Deferred)
30
45
  end
31
46
 
47
+ # Tool search loads a deferred function declared outside any
48
+ # namespace under a namespace of its own name, and the API refuses a
49
+ # call to it that lacks the namespace.
50
+ def namespace_searched_calls(input, tools)
51
+ searched = tools.each_value.filter_map { |tool| tool.name.to_s if deferred?(tool) && tool.namespace.nil? }
52
+ input.map { |item| searched_call?(item, searched) ? item.merge(namespace: item[:name]) : item }
53
+ end
54
+
55
+ def searched_call?(item, searched)
56
+ item.is_a?(Hash) && item[:type] == 'function_call' && item[:namespace].nil? && searched.include?(item[:name])
57
+ end
58
+
32
59
  # The API requires a search tool next to deferred tools, and rejects
33
60
  # search items in the input of a request that declares none.
34
61
  def apply_tool_search(payload)
@@ -41,8 +68,28 @@ module RubyLLM
41
68
  end)
42
69
  end
43
70
 
71
+ # A web search reports the pages it found only when the request asks
72
+ # for them, and services on this wire format that lack this include
73
+ # value reject the whole request, so each one opts in.
74
+ WEB_SEARCH_TOOL_TYPES = %w[web_search web_search_preview].freeze
75
+ WEB_SEARCH_SOURCES = 'web_search_call.action.sources'
76
+
77
+ def apply_provider_tools(*)
78
+ payload = super
79
+ return payload unless web_search_sources?
80
+ return payload unless Array(payload[:tools]).any? { |tool| web_search_tool?(tool) }
81
+
82
+ payload.merge(include: Array(payload[:include]) | [WEB_SEARCH_SOURCES])
83
+ end
84
+
85
+ def web_search_tool?(tool)
86
+ WEB_SEARCH_TOOL_TYPES.include?(tool[:type] || tool['type'])
87
+ end
88
+
44
89
  def defers_loading?(tools)
45
- tools.any? { |tool| tool[:defer_loading] || tool['defer_loading'] }
90
+ tools.any? do |tool|
91
+ tool[:defer_loading] || tool['defer_loading'] || defers_loading?(Array(tool[:tools] || tool['tools']))
92
+ end
46
93
  end
47
94
 
48
95
  def search_tool?(tool)
@@ -28,9 +28,30 @@ module RubyLLM
28
28
  end
29
29
  }.freeze
30
30
 
31
+ # Returns the text of +message+ that holds its answer when its turn
32
+ # marks other items as intermediate, or +nil+ when all of its content
33
+ # is the answer. Reads only what the message keeps, so a restored
34
+ # message answers the same.
35
+ def self.parse_answer_text(message)
36
+ Chat.parse_answer_text(message)
37
+ end
38
+
39
+ # Returns the pages the message's web searches returned, as WebSource objects.
40
+ def self.parse_web_sources(message)
41
+ Chat.parse_web_sources(message)
42
+ end
43
+
31
44
  def server_tool_aliases
32
45
  SERVER_TOOL_ALIASES
33
46
  end
47
+
48
+ def tool_search?
49
+ true
50
+ end
51
+
52
+ def web_search_sources? # :nodoc:
53
+ false
54
+ end
34
55
  end
35
56
  end
36
57
  end
@@ -8,15 +8,22 @@ module RubyLLM
8
8
 
9
9
  def parse_judgment_response(response, questions:)
10
10
  body = response.body
11
+ usage = body['usage'] || {}
11
12
  answers = questions.values.to_h do |question|
12
13
  [question.name, parse_answer(body.fetch('answers').fetch(question.name.to_s), question)]
13
14
  end
14
15
  RubyLLM::Judgment.new(
15
16
  answers:, model: body['model'], raw: response, model_info: @model,
16
- tokens: Tokens.new(input: body.dig('usage', 'input_tokens'), output: body.dig('usage', 'output_tokens'))
17
+ tokens: Tokens.new(input: usage['input_tokens'], output: usage['output_tokens'],
18
+ reported_cost: reported_cost(usage))
17
19
  )
18
20
  rescue KeyError, NoMethodError, TypeError => e
19
- raise Error.new("System One returned an invalid judgment: #{e.message}", response:)
21
+ raise Error.new('System One returned an invalid judgment', provider_message: e.message, response:,
22
+ redact: @config&.redact_error_messages)
23
+ end
24
+
25
+ def reported_cost(_usage)
26
+ nil
20
27
  end
21
28
 
22
29
  def parse_answer(answer, question)
@@ -10,8 +10,8 @@ module RubyLLM
10
10
 
11
11
  STORAGE_CLIENTS = Support::ProcessCache.new
12
12
 
13
- def self.storage_clients
14
- STORAGE_CLIENTS
13
+ def rejected_options(operation)
14
+ operation == :upload ? %i[purpose expires_in] : super
15
15
  end
16
16
 
17
17
  # rubocop:disable-next Lint/UnusedMethodArgument
@@ -22,7 +22,8 @@ module RubyLLM
22
22
  bucket_name, key = parse_gcs_uri(target_uri)
23
23
 
24
24
  with_file_body(attachment) do |body|
25
- bucket(bucket_name).create_file(body, key, content_type: content_type || file_content_type(attachment))
25
+ bucket(bucket_name).create_file(body, key, **provider_options.transform_keys(&:to_sym),
26
+ content_type: content_type || file_content_type(attachment))
26
27
  end
27
28
 
28
29
  uploaded_file(
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ module VertexAI
8
+ # Google Cloud Text-to-Speech, served from the endpoint of the
9
+ # configured location. A voice name is locale, model, and voice:
10
+ # de-DE-Chirp3-HD-Leda speaks German, and a bare Leda on Chirp3-HD is
11
+ # en-US-Chirp3-HD-Leda.
12
+ class TextToSpeech < Protocol
13
+ MODELS = %w[Chirp3-HD].freeze
14
+
15
+ DEFAULT_VOICE = 'Charon'
16
+ DEFAULT_LOCALE = 'en-US'
17
+ DEFAULT_FORMAT = 'mp3'
18
+
19
+ # RubyLLM format names in Cloud Text-to-Speech's audioEncoding
20
+ # vocabulary. LINEAR16, MULAW, and ALAW come back in a WAV container.
21
+ ENCODINGS = {
22
+ 'mp3' => 'MP3',
23
+ 'wav' => 'LINEAR16',
24
+ 'opus' => 'OGG_OPUS',
25
+ 'mulaw' => 'MULAW',
26
+ 'alaw' => 'ALAW'
27
+ }.freeze
28
+
29
+ # Request fields that choose the text, voice, or encoding RubyLLM
30
+ # reports, which provider_options may not replace in either JSON
31
+ # spelling (languageCode or language_code).
32
+ RESERVED_OPTIONS = {
33
+ 'input' => %w[text ssml markup multispeakermarkup],
34
+ 'voice' => %w[name languagecode customvoice voiceclone],
35
+ 'audioconfig' => %w[audioencoding]
36
+ }.freeze
37
+
38
+ MIME_TYPES = {
39
+ 'opus' => 'audio/ogg',
40
+ 'mulaw' => 'audio/wav',
41
+ 'alaw' => 'audio/wav'
42
+ }.freeze
43
+
44
+ def initialize(provider, model = nil)
45
+ super
46
+ @connection = provider.text_to_speech_connection
47
+ end
48
+
49
+ def speech_url(**)
50
+ 'text:synthesize'
51
+ end
52
+
53
+ def speak(input, **, &)
54
+ @speech_characters = input.to_s.length
55
+ super
56
+ end
57
+
58
+ def render_speech_payload(input, model:, voice:, format:, provider_options: {})
59
+ options = Support::Utils.deep_stringify_keys(provider_options || {})
60
+ reject_reserved_options(options)
61
+
62
+ name = voice_name(voice, model)
63
+ payload = {
64
+ 'input' => { 'text' => input },
65
+ 'voice' => { 'languageCode' => name.split('-').first(2).join('-'), 'name' => name },
66
+ 'audioConfig' => { 'audioEncoding' => encoding_for(format) }
67
+ }
68
+ Support::Utils.deep_merge(payload, options)
69
+ end
70
+
71
+ def parse_speech_response(response, model:, voice:, format:)
72
+ audio = response.body['audioContent'] if response.body.is_a?(Hash)
73
+ raise Error.new('Cloud Text-to-Speech returned no audio', response:) unless audio
74
+
75
+ format = format_for(format)
76
+ RubyLLM::Speech.new(data: Base64.decode64(audio), model:, voice: voice_name(voice, model), format:,
77
+ mime_type: MIME_TYPES[format], characters: @speech_characters)
78
+ end
79
+
80
+ private
81
+
82
+ def reject_reserved_options(options)
83
+ reserved = options.flat_map do |section, fields|
84
+ keys = RESERVED_OPTIONS[field_key(section)]
85
+ next [] unless keys && fields.is_a?(Hash)
86
+
87
+ fields.keys.select { |key| keys.include?(field_key(key)) }.map { |key| "#{section}.#{key}" }
88
+ end
89
+ return if reserved.empty?
90
+
91
+ raise ArgumentError, 'Use the input, voice:, and format: arguments instead of provider_options for ' \
92
+ "#{reserved.join(', ')}"
93
+ end
94
+
95
+ def field_key(key)
96
+ key.delete('_').downcase
97
+ end
98
+
99
+ def voice_name(voice, model)
100
+ voice = (voice || DEFAULT_VOICE).to_s
101
+ voice.include?('-') ? voice : "#{DEFAULT_LOCALE}-#{model}-#{voice}"
102
+ end
103
+
104
+ def encoding_for(format)
105
+ format = format_for(format)
106
+ ENCODINGS.fetch(format, format.upcase)
107
+ end
108
+
109
+ def format_for(format)
110
+ format = (format || DEFAULT_FORMAT).to_s
111
+ ENCODINGS.key(format.upcase) || format.downcase
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -5,15 +5,21 @@ module RubyLLM
5
5
  module XAI
6
6
  # xAI Files API.
7
7
  class Files < Protocols::Files
8
+ def rejected_options(operation)
9
+ operation == :upload ? %i[uri] : super
10
+ end
11
+
8
12
  private
9
13
 
10
- # rubocop:disable-next Lint/UnusedMethodArgument
11
- def render_upload_payload(attachment, purpose: nil, expires_in: nil, visibility: nil,
12
- display_name: nil, uri: nil, content_type: nil)
13
- multipart_payload(attachment, expires_after: expires_in, purpose:)
14
+ def render_upload_fields(_attachment, purpose: nil, expires_in: nil)
15
+ { expires_after: expires_in, purpose: }
16
+ end
17
+
18
+ def parse_upload_response(data, attachment)
19
+ parse_file_response(data, mime_type: file_content_type(attachment))
14
20
  end
15
21
 
16
- def parse_file_response(data)
22
+ def parse_file_response(data, mime_type: nil)
17
23
  uploaded_file(
18
24
  data,
19
25
  id: data['id'],
@@ -21,7 +27,8 @@ module RubyLLM
21
27
  byte_size: data['bytes'],
22
28
  created_at: timestamp(data['created_at']),
23
29
  expires_at: timestamp(data['expires_at']),
24
- purpose: data['purpose']
30
+ purpose: data['purpose'],
31
+ mime_type:
25
32
  )
26
33
  end
27
34
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module ZAI
6
+ # Z.ai file storage. Files go up and can be referenced by id, but Z.ai
7
+ # has no route that reads one back, and no expiry option.
8
+ class Files < Protocols::OpenAI::Files
9
+ def rejected_options(operation)
10
+ operation == :upload ? %i[expires_in uri] : super
11
+ end
12
+
13
+ def find(_file_id)
14
+ raise Error, 'Z.ai does not support retrieving uploaded files'
15
+ end
16
+
17
+ def download(_file_id)
18
+ raise Error, 'Z.ai does not support downloading uploaded files'
19
+ end
20
+
21
+ private
22
+
23
+ def render_upload_fields(_attachment, purpose: nil, **)
24
+ { purpose: purpose || 'user_data' }
25
+ end
26
+
27
+ def timestamp(value)
28
+ Time.at(value.fdiv(1000)) if value
29
+ end
30
+
31
+ def uploaded_file(data, **attributes)
32
+ super(data, **attributes, downloadable: false)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end