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
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  # never set.
26
26
  class Configuration
27
27
  class << self
28
- def option(key, default = nil) # :nodoc:
28
+ def option(key, default = nil, &normalize) # :nodoc:
29
29
  key = key.to_sym
30
30
  return if options.include?(key)
31
31
 
@@ -33,6 +33,7 @@ module RubyLLM
33
33
 
34
34
  define_method("#{key}=") do |value|
35
35
  value = nil if value.is_a?(String) && value.strip.empty?
36
+ value = normalize.call(value) if normalize
36
37
  instance_variable_set(:"@#{key}", value)
37
38
  end
38
39
 
@@ -65,8 +66,8 @@ module RubyLLM
65
66
  # :attr_accessor: default_model
66
67
  #
67
68
  # The model id used by RubyLLM.chat when no model is given.
68
- # Default: <tt>'gpt-5.6'</tt>.
69
- option :default_model, 'gpt-5.6'
69
+ # Default: <tt>'gpt-6.1-sol'</tt>.
70
+ option :default_model, 'gpt-6.1-sol'
70
71
 
71
72
  ##
72
73
  # :attr_accessor: default_embedding_model
@@ -159,6 +160,15 @@ module RubyLLM
159
160
  # Optional persistence adapter installed by the Rails integration.
160
161
  option :batch_store, nil # :nodoc:
161
162
 
163
+ ##
164
+ # :attr_accessor: broadcast_messages
165
+ #
166
+ # Whether +acts_as_message+ records broadcast a chat's messages and
167
+ # streamed chunks with Turbo. Needs turbo-rails and Action Cable. An
168
+ # +acts_as_message+ model's +broadcasts:+ option overrides it. The
169
+ # chat UI generator turns it on. Default: +false+.
170
+ option :broadcast_messages, false
171
+
162
172
  ##
163
173
  # :attr_accessor: mcp_credential_store
164
174
  #
@@ -186,6 +196,59 @@ module RubyLLM
186
196
  # registering RubyLLM. Default: +nil+.
187
197
  option :mcp_client_id, nil
188
198
 
199
+ ##
200
+ # :attr_accessor: sandbox
201
+ #
202
+ # Where code from a model runs by default, such as the code
203
+ # Chat#with_code_execution executes: an MXC::Sandbox class from the mxc
204
+ # gem, its name as a String, or any sandbox Sandbox describes. Default:
205
+ # +nil+ (nothing runs locally).
206
+ option :sandbox, nil
207
+
208
+ ##
209
+ # :attr_accessor: sandbox_backend
210
+ #
211
+ # What isolates an MXC::Sandbox on Linux: +:bubblewrap+, which gives
212
+ # each run its own namespaces, or +:landlock+, which only restricts
213
+ # the files it can open and runs without network access. Other
214
+ # platforms ignore it, and so does a sandbox class that names its own
215
+ # backend. Default: +:bubblewrap+.
216
+ option :sandbox_backend, :bubblewrap
217
+
218
+ ##
219
+ # :attr_accessor: package_cache_dir
220
+ #
221
+ # Directory holding the gems and Python packages declared with
222
+ # Chat#with_code_execution, installed once and reused by every run.
223
+ # Defaults to a +packages+ directory in the operating system's user
224
+ # cache directory.
225
+ option :package_cache_dir, -> { (directory = Support::Utils.cache_directory) && File.join(directory, 'packages') }
226
+
227
+ ##
228
+ # :attr_accessor: code_execution_max_file_size
229
+ #
230
+ # Bytes a file that Chat#with_code_execution's code writes may have and
231
+ # still come back with the result. Larger files stay out, and the
232
+ # model is told which ones. Default: 10 MB.
233
+ #
234
+ # config.code_execution_max_file_size = 100 * 1024 * 1024
235
+ #
236
+ option :code_execution_max_file_size, 10 * 1024 * 1024
237
+
238
+ ##
239
+ # :attr_accessor: file_converters
240
+ #
241
+ # Converters that turn a chat's files into text for the model, as a
242
+ # Hash of MIME types to converters, tried before the ones RubyLLM
243
+ # ships. A type may end in <tt>/*</tt>, like <tt>"image/*"</tt>, and a
244
+ # converter is anything that answers +call+ with an Attachment and
245
+ # returns its text, or +nil+ to pass it on. See FileStore::Converters.
246
+ # Default: <tt>{}</tt>.
247
+ #
248
+ # config.file_converters["image/*"] = RubyLLM::FileStore::Converters::OCR.new
249
+ #
250
+ option :file_converters, -> { {} }
251
+
189
252
  # Keeps custom 1.x initializers bootable long enough to run the 2.0
190
253
  # upgrade generator. The registry is always RubyLLM-owned in 2.0.
191
254
  def model_registry_class=(_value) # :nodoc:
@@ -206,6 +269,27 @@ module RubyLLM
206
269
  # Seconds to wait for a response before timing out. Default: 300.
207
270
  option :request_timeout, 300
208
271
 
272
+ ##
273
+ # :attr_accessor: connect_timeout
274
+ #
275
+ # Seconds to wait for a connection to open before timing out, or +nil+
276
+ # to wait as long as request_timeout. Default: +nil+.
277
+ option :connect_timeout, nil
278
+
279
+ ##
280
+ # :attr_accessor: stream_idle_timeout
281
+ #
282
+ # Seconds a streaming request may go without receiving a byte, from
283
+ # sending the request or since the last chunk, before it fails with a
284
+ # ConnectionError whose +timeout?+ is +true+, as a request_timeout
285
+ # does, so fallbacks take over. +request_timeout+ still bounds sending
286
+ # the request, and opening the connection unless connect_timeout is
287
+ # set. Requests that don't stream ignore it. Default: +nil+, which leaves streams to +request_timeout+.
288
+ #
289
+ # config.request_timeout = 1800
290
+ # config.stream_idle_timeout = 60
291
+ option :stream_idle_timeout
292
+
209
293
  ##
210
294
  # :attr_accessor: max_retries
211
295
  #
@@ -248,6 +332,17 @@ module RubyLLM
248
332
  # SOCKS5 proxies are supported. Default: +nil+.
249
333
  option :http_proxy, nil
250
334
 
335
+ ##
336
+ # :attr_accessor: ssl
337
+ #
338
+ # TLS options for all requests, as a Hash of Faraday's SSL option
339
+ # keys, such as <tt>{ ca_file: '/certs/internal-ca.pem' }</tt> or
340
+ # <tt>{ verify: false }</tt>. A context copies its strings, but shares
341
+ # objects such as a +cert_store+, so give each context its own and
342
+ # leave it unchanged once used. WebSocket connections raise
343
+ # ArgumentError when it is set. Default: +nil+.
344
+ option :ssl, nil
345
+
251
346
  ##
252
347
  # :attr_accessor: tool_concurrency
253
348
  #
@@ -256,6 +351,28 @@ module RubyLLM
256
351
  # for fibers via the async gem. Default: +false+.
257
352
  option :tool_concurrency, false
258
353
 
354
+ ##
355
+ # :attr_accessor: context_budget
356
+ #
357
+ # The shares of the context window every chat and agent keeps tool
358
+ # results within, as Chat#with_context_budget takes them. A chat's or
359
+ # agent's own budget wins, and +nil+ there turns it off. Default:
360
+ # +nil+ (no budget).
361
+ #
362
+ # config.context_budget = { per_result: 0.25, per_request: 0.7 }
363
+ #
364
+ option(:context_budget, nil) { |budget| Chat::ContextBudget.normalize(budget) }
365
+
366
+ ##
367
+ # :attr_accessor: max_run_time
368
+ #
369
+ # Seconds a persisted chat's run may go without progress, or a tool
370
+ # call it runs may take, before another process treats it as crashed
371
+ # and resumes it. A run makes progress as it streams and between
372
+ # steps. A process that dies frees its claims within a minute.
373
+ # Default: 600.
374
+ option :max_run_time, 600
375
+
259
376
  ##
260
377
  # :attr_accessor: auto_upload_large_files
261
378
  #
@@ -286,6 +403,27 @@ module RubyLLM
286
403
  # +:raise+. Default: +:warn+.
287
404
  option :deprecation_behavior, :warn
288
405
 
406
+ ##
407
+ # :attr_accessor: redact_error_messages
408
+ #
409
+ # Whether Error#message leaves out what the provider wrote. When
410
+ # +true+, a provider's error reads as its HTTP status, provider, and
411
+ # error code only, such as <tt>openai returned HTTP 400
412
+ # (invalid_request_error)</tt>, because providers often quote the
413
+ # request back. The text stays on Error#provider_message and the raw
414
+ # body on Error#response_body. Default: +false+.
415
+ option :redact_error_messages, false
416
+
417
+ ##
418
+ # :attr_accessor: split_think_tags
419
+ #
420
+ # Whether Chat Completions responses that open with a
421
+ # <tt><think>...</think></tt> block move that block into the
422
+ # message's thinking. Turn it on for servers that leave reasoning
423
+ # inline, such as llama.cpp with <tt>--reasoning-format none</tt>.
424
+ # A +reasoning_content+ field still wins. Default: +false+.
425
+ option :split_think_tags, false
426
+
289
427
  ##
290
428
  # :attr_accessor: faraday_adapter
291
429
  #
@@ -332,8 +470,17 @@ module RubyLLM
332
470
  end
333
471
  end
334
472
 
473
+ # Copies file_converters too, so registering one on a context's copy,
474
+ # <tt>config.file_converters["image/*"] = ...</tt>, leaves the
475
+ # original alone.
476
+ def initialize_copy(source) # :nodoc:
477
+ super
478
+ @file_converters = @file_converters.dup
479
+ @ssl = @ssl.to_h.transform_values { |value| value.is_a?(String) ? value.dup : value } if @ssl
480
+ end
481
+
335
482
  def instance_variables # :nodoc:
336
- super.reject { |ivar| ivar.to_s.match?(/(_id|_key|_secret|_token|_credential_provider)$/) }
483
+ super.reject { |ivar| ivar.to_s.match?(/(_id|_key|_secret|_token|_credential_provider|_proxy|\A@ssl)$/) }
337
484
  end
338
485
 
339
486
  # Object#inspect reads the instance variable table directly, so
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A ContentFilter is one verdict of the provider's content filtering on a
5
+ # request or its response, such as Azure's hate or protected material
6
+ # filters. They appear on Message#content_filters for providers that
7
+ # report them, whether or not they stopped the response, so you can see
8
+ # why a reply ended with Message#content_filtered? or what came close.
9
+ #
10
+ # response = chat.ask "Summarize this paper"
11
+ # response.content_filtered? # => true
12
+ # response.content_filters.select(&:filtered?).map(&:category) # => ["hate"]
13
+ #
14
+ # A verdict names a category and a severity, never the text it judged.
15
+ class ContentFilter
16
+ include Support::Inspectable
17
+
18
+ SOURCES = %i[prompt completion].freeze # :nodoc:
19
+
20
+ # Which side was judged: +:prompt+ for the request, +:completion+ for
21
+ # the response.
22
+ attr_reader :source
23
+
24
+ # The provider's name for the category, such as <tt>"hate"</tt>,
25
+ # <tt>"jailbreak"</tt>, or <tt>"protected_material_text"</tt>.
26
+ attr_reader :category
27
+
28
+ # The severity the provider gave, such as <tt>"safe"</tt>,
29
+ # <tt>"low"</tt>, or <tt>"medium"</tt>, or +nil+ for a category
30
+ # judged only as detected or not.
31
+ attr_reader :severity
32
+
33
+ def self.from_h(data) # :nodoc:
34
+ data = data.transform_keys(&:to_sym)
35
+ new(source: data[:source], category: data[:category], severity: data[:severity],
36
+ filtered: data[:filtered], detected: data[:detected])
37
+ end
38
+
39
+ def initialize(source:, category:, severity: nil, filtered: false, detected: nil) # :nodoc:
40
+ @source = source&.to_sym
41
+ @category = category.to_s
42
+ @severity = severity
43
+ @filtered = filtered == true
44
+ @detected = detected
45
+ end
46
+
47
+ # Returns whether the filter blocked or cut the text it judged.
48
+ def filtered?
49
+ @filtered
50
+ end
51
+
52
+ # Returns whether the provider detected what the category looks for,
53
+ # such as a jailbreak attempt or protected material, or +nil+ for a
54
+ # category judged by severity.
55
+ def detected?
56
+ @detected
57
+ end
58
+
59
+ # Returns the verdict as a Hash, omitting +nil+ values.
60
+ def to_h
61
+ { source: source, category: category, severity: severity, filtered: filtered?, detected: @detected }.compact
62
+ end
63
+
64
+ def inspect_attributes # :nodoc:
65
+ to_h
66
+ end
67
+ end
68
+ end
data/lib/ruby_llm/cost.rb CHANGED
@@ -13,9 +13,14 @@ module RubyLLM
13
13
  # cost.output
14
14
  #
15
15
  # The components are RubyLLM's normalized token buckets: #input, #output,
16
- # #cache_read, #cache_write, and #thinking. When the registry lacks
17
- # pricing for tokens that were used, the affected component and #total
18
- # return +nil+ instead of a false zero.
16
+ # #cache_read, #cache_write, and #thinking, plus #characters,
17
+ # #audio_seconds, and #search_units for operations billed in those units.
18
+ # When the registry lacks pricing for usage that was reported, the
19
+ # affected component and #total return +nil+ instead of a false zero.
20
+ #
21
+ # speech = RubyLLM.speak "Hello"
22
+ # speech.cost.characters # => 0.00015, or nil when no price is known
23
+ #
19
24
  #
20
25
  # When the provider reports the exact cost of a call, #total returns the
21
26
  # reported amount instead of a registry-price estimate, even when
@@ -28,7 +33,8 @@ module RubyLLM
28
33
  class Cost
29
34
  include Support::Inspectable
30
35
 
31
- COMPONENTS = %i[input output cache_read cache_write thinking].freeze # :nodoc:
36
+ UNITS = %i[characters audio_seconds search_units].freeze # :nodoc:
37
+ COMPONENTS = (%i[input output cache_read cache_write thinking] + UNITS).freeze # :nodoc:
32
38
  PER_MILLION = 1_000_000.0 # :nodoc:
33
39
 
34
40
  class << self
@@ -58,33 +64,43 @@ module RubyLLM
58
64
 
59
65
  # Rebuilds a cost from a stored breakdown Hash, as produced by #to_h
60
66
  # and persisted alongside a usage entry. Keys may be Strings or
61
- # Symbols. The component readers return the recorded amounts and
62
- # #total equals the recorded +:total+.
67
+ # Symbols, and amounts may be numeric Strings, as JSON encodes
68
+ # decimals. The component readers return the recorded amounts and
69
+ # #total equals the recorded +:total+. A breakdown with a total omits
70
+ # the components that had no usage, so ::aggregate still sums them
71
+ # across costs. Pass +tokens+ to mark used components without a
72
+ # recorded amount as missing pricing.
63
73
  #
64
74
  # RubyLLM::Cost.from_h({ input: 0.0001, total: 0.0003 }).total
65
75
  #
66
76
  def from_h(hash, tokens: nil)
67
- amounts = COMPONENTS.to_h { |component| [component, hash[component] || hash[component.to_s]] }
77
+ amounts = COMPONENTS.to_h { |component| [component, recorded_amount(hash, component)] }
68
78
  total_recorded = hash.key?(:total) || hash.key?('total')
69
- total = hash[:total] || hash['total'] if total_recorded
70
- missing = missing_recorded_components(amounts, tokens, total_recorded)
79
+ total = recorded_amount(hash, :total) if total_recorded
80
+ missing = missing_recorded_components(amounts, tokens, total)
71
81
 
72
82
  new(amounts:, missing:, reported: recorded_tokens?(amounts, tokens, total_recorded), total:)
73
83
  end
74
84
 
75
85
  private
76
86
 
87
+ def recorded_amount(hash, key)
88
+ amount = hash[key] || hash[key.to_s]
89
+ amount.is_a?(String) ? Float(amount) : amount
90
+ end
91
+
77
92
  def aggregate_total(costs)
78
93
  totals = costs.map(&:total)
79
94
  totals.sum if totals.any? && totals.none?(&:nil?)
80
95
  end
81
96
 
82
- def missing_recorded_components(amounts, tokens, total_recorded)
83
- return [] if total_recorded
84
- return COMPONENTS.reject { |component| amounts[component] } unless tokens
85
-
86
- COMPONENTS.select do |component|
87
- tokens.public_send(component).to_i.positive? && amounts[component].nil?
97
+ def missing_recorded_components(amounts, tokens, total)
98
+ if tokens
99
+ COMPONENTS.select { |component| tokens.public_send(component).to_f.positive? && amounts[component].nil? }
100
+ elsif total && amounts.values.any?
101
+ []
102
+ else
103
+ COMPONENTS.reject { |component| amounts[component] }
88
104
  end
89
105
  end
90
106
 
@@ -145,10 +161,29 @@ module RubyLLM
145
161
  @amounts[:thinking]
146
162
  end
147
163
 
164
+ # Returns the cost of billed characters in US dollars, or +nil+ when
165
+ # the count or its pricing is unavailable.
166
+ def characters
167
+ @amounts[:characters]
168
+ end
169
+
170
+ # Returns the cost of billed audio seconds in US dollars, or +nil+ when
171
+ # the duration or its pricing is unavailable.
172
+ def audio_seconds
173
+ @amounts[:audio_seconds]
174
+ end
175
+
176
+ # Returns the cost of billed search units in US dollars, or +nil+ when
177
+ # the count or its pricing is unavailable.
178
+ def search_units
179
+ @amounts[:search_units]
180
+ end
181
+
148
182
  # Returns the sum of all components in US dollars, or the exact amount
149
183
  # the provider reported when it reported one. Returns +nil+ when there
150
184
  # is no token usage, or when pricing is missing for tokens that were
151
- # used and the provider reported no cost.
185
+ # used and the provider reported no cost. A calculated total covers only
186
+ # the counts the provider reported, so partial usage can leave out charges.
152
187
  def total
153
188
  return nil unless @complete
154
189
  return nil unless tokens?
@@ -164,14 +199,7 @@ module RubyLLM
164
199
  # Returns a hash of component costs in US dollars, plus +:total+,
165
200
  # omitting +nil+ values.
166
201
  def to_h
167
- {
168
- input: input,
169
- output: output,
170
- cache_read: cache_read,
171
- cache_write: cache_write,
172
- thinking: thinking,
173
- total: total
174
- }.compact
202
+ COMPONENTS.to_h { |component| [component, public_send(component)] }.merge(total: total).compact
175
203
  end
176
204
 
177
205
  def tokens? # :nodoc:
@@ -202,9 +230,8 @@ module RubyLLM
202
230
  def missing_component?(component)
203
231
  return image_input_missing? if component == :input && detailed_image_input?
204
232
  return cache_write_missing? if component == :cache_write && repriced_cache_writes?
205
- return false if component == :thinking && !thinking_priced_separately?
206
233
 
207
- tokens_for(component).to_i.positive? && price_for(component).nil?
234
+ tokens_for(component).to_f.positive? && price_for(component).nil?
208
235
  end
209
236
 
210
237
  def amount_for(component)
@@ -213,8 +240,6 @@ module RubyLLM
213
240
 
214
241
  token_count = tokens_for(component)
215
242
  return nil if token_count.nil?
216
-
217
- token_count = token_count.to_i
218
243
  return 0.0 if token_count.zero?
219
244
 
220
245
  price = price_for(component)
@@ -237,6 +262,8 @@ module RubyLLM
237
262
  @tokens.cache_write
238
263
  when :thinking
239
264
  @tokens.thinking if thinking_priced_separately?
265
+ when *UNITS
266
+ @tokens.public_send(component) if @tokens.respond_to?(component)
240
267
  end
241
268
  end
242
269
 
@@ -247,9 +274,18 @@ module RubyLLM
247
274
  when :cache_read then text_price(:cache_read_input_per_million, text_pricing.cache_read_input)
248
275
  when :cache_write then text_price(:cache_write_input_per_million, text_pricing.cache_write_input)
249
276
  when :thinking then text_price(:reasoning_output_per_million, text_pricing.reasoning_output)
277
+ when *UNITS then unit_price(component)
250
278
  end
251
279
  end
252
280
 
281
+ def unit_price(unit)
282
+ pricing = @model&.pricing
283
+ return unless pricing
284
+
285
+ category = pricing.public_send(unit)
286
+ @tier == :batch ? category.batch&.input_per_million : category.input
287
+ end
288
+
253
289
  def input_price
254
290
  return text_input_price if @category == :text_tokens || image_cost?
255
291
 
@@ -257,7 +293,6 @@ module RubyLLM
257
293
  end
258
294
 
259
295
  def output_price
260
- return image_pricing.output || text_output_price if image_cost?
261
296
  return text_output_price if @category == :text_tokens
262
297
 
263
298
  category_pricing.output || text_output_price
@@ -272,13 +307,9 @@ module RubyLLM
272
307
  end
273
308
 
274
309
  def text_price(attribute, standard_price)
275
- applicable_text_tier&.public_send(attribute) || standard_price
276
- end
277
-
278
- def applicable_text_tier
279
- return text_pricing.batch if @tier == :batch
310
+ return text_pricing.batch&.public_send(attribute) if @tier == :batch
280
311
 
281
- text_pricing.tier_for(prompt_tokens)
312
+ text_pricing.tier_for(prompt_tokens)&.public_send(attribute) || standard_price
282
313
  end
283
314
 
284
315
  def prompt_tokens
@@ -294,7 +325,6 @@ module RubyLLM
294
325
  end
295
326
 
296
327
  def category_pricing
297
- return text_pricing if @category == :text_tokens
298
328
  return image_pricing if image_cost?
299
329
 
300
330
  pricing = @model&.pricing
@@ -365,7 +395,6 @@ module RubyLLM
365
395
  end
366
396
 
367
397
  def image_input_parts
368
- text_input_price = applicable_text_tier&.input_per_million || text_pricing.input
369
398
  [
370
399
  [:text, input_detail('text_tokens'), text_input_price],
371
400
  [:image, input_detail('image_tokens'), image_pricing.input || text_input_price]
@@ -377,12 +406,10 @@ module RubyLLM
377
406
  end
378
407
 
379
408
  def thinking_priced_separately?
380
- tier = applicable_text_tier
381
- reasoning_price = tier&.reasoning_output_per_million || text_pricing.reasoning_output
409
+ reasoning_price = price_for(:thinking)
382
410
  return false unless reasoning_price
383
411
 
384
- output_price = tier&.output_per_million || text_pricing.output
385
- output_price.nil? || reasoning_price != output_price
412
+ text_output_price.nil? || reasoning_price != text_output_price
386
413
  end
387
414
 
388
415
  def normalize_model(model)
@@ -9,19 +9,20 @@ module RubyLLM
9
9
  #
10
10
  class DownloadedFile < String
11
11
  include Support::Inspectable
12
+ include Files::Saving
12
13
 
13
- # Returns the raw file bytes as a String.
14
- def to_blob
15
- to_s
16
- end
17
-
14
+ ##
15
+ # :method: save
16
+ # :call-seq:
17
+ # save(path)
18
+ #
18
19
  # Writes the file bytes to +path+, expanding it first. Returns +path+.
19
20
  #
20
21
  # file.save("report.pdf")
21
- #
22
- def save(path)
23
- File.binwrite(File.expand_path(path), to_blob)
24
- path
22
+
23
+ # Returns the raw file bytes as a String.
24
+ def to_blob
25
+ to_s
25
26
  end
26
27
 
27
28
  private
@@ -5,7 +5,7 @@ module RubyLLM
5
5
  # RubyLLM.embed returns one:
6
6
  #
7
7
  # embedding = RubyLLM.embed("Ruby is a programmer's best friend")
8
- # embedding.vectors # => [0.018, -0.027, ...]
8
+ # embedding.vector # => [0.018, -0.027, ...]
9
9
  #
10
10
  # Pass an array to embed several texts in one call on supported models:
11
11
  #
@@ -17,6 +17,7 @@ module RubyLLM
17
17
  class Embedding
18
18
  include Support::Inspectable
19
19
  include Accounting::Usage::Result
20
+ include Accounting::Usage::ModelInfo
20
21
 
21
22
  # The embedding vectors. A flat array of floats when a single text
22
23
  # was embedded, an array of such arrays when an array of texts was
@@ -32,6 +33,18 @@ module RubyLLM
32
33
  # The id of the model that produced the vectors, as a String.
33
34
  attr_reader :model
34
35
 
36
+ # Returns the only vector of this result. Raises Error when it holds
37
+ # several, one for each text of an array, so read #vectors instead.
38
+ #
39
+ # RubyLLM.embed("Ruby").vector # => [0.018, -0.027, ...]
40
+ #
41
+ def vector
42
+ return vectors if !vectors.is_a?(Array) || vectors.all?(Numeric)
43
+ return vectors.first if vectors.size == 1
44
+
45
+ raise Error, "This embedding holds #{vectors.size} vectors; read them with #vectors"
46
+ end
47
+
35
48
  def initialize(vectors:, model:, sparse_vectors: nil, input_tokens: nil, reported_cost: nil) # :nodoc:
36
49
  @vectors = vectors
37
50
  @sparse_vectors = sparse_vectors
@@ -54,14 +67,6 @@ module RubyLLM
54
67
  Cost.new(tokens:, model: model_info, category: :embeddings)
55
68
  end
56
69
 
57
- attr_writer :model_info # :nodoc:
58
-
59
- def model_info # :nodoc:
60
- @model_info ||= RubyLLM.models.find(model)
61
- rescue ModelNotFoundError
62
- nil
63
- end
64
-
65
70
  # Generates embeddings for +text+ and returns an Embedding. +text+
66
71
  # may be a single string or, on supported models, an array of strings.
67
72
  # An array produces one vector per string in a single API call.
@@ -91,7 +96,7 @@ module RubyLLM
91
96
  # <tt>"RETRIEVAL_DOCUMENT"</tt>, while Bedrock Cohere takes an input
92
97
  # type such as <tt>"search_document"</tt>. +title:+ labels the
93
98
  # document on Vertex AI and Gemini retrieval tasks. Providers that
94
- # have no task concept ignore both. +with:+ passes one or more media
99
+ # cannot carry either raise ArgumentError. +with:+ passes one or more media
95
100
  # attachments (images, audio, video, PDFs) to embed alongside the
96
101
  # text on multimodal embedding models such as Gemini's
97
102
  # gemini-embedding-2; providers without multimodal embeddings raise
@@ -100,7 +105,7 @@ module RubyLLM
100
105
  # request as-is. +metadata:+ is not sent to the provider; it is
101
106
  # attached to the emitted +embedding.ruby_llm+ instrumentation event.
102
107
  # +owner:+ attributes the usage to a record, such as a user, and wins
103
- # over RubyLLM.with_usage_owner.
108
+ # over RubyLLM.with_owner.
104
109
  def self.embed(text,
105
110
  model: nil,
106
111
  provider: nil,
@@ -113,40 +118,20 @@ module RubyLLM
113
118
  provider_options: {},
114
119
  metadata: nil,
115
120
  owner: nil)
116
- config = context&.config || RubyLLM.config
117
- model ||= config.default_embedding_model
118
- model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
119
- config: config)
120
- model_id = model.id
121
- empty_tokens = Tokens.new
122
-
123
- payload = {
124
- provider: provider_instance.slug,
125
- provider_class: provider_instance.name,
126
- model: model_id,
127
- model_info: model,
128
- input: text,
129
- dimensions: dimensions,
130
- task_type: task_type,
131
- title: title,
132
- attachment_count: Attachment.wrap(with).size,
133
- provider_options: provider_options,
134
- metadata: metadata,
135
- tokens: empty_tokens,
136
- cost: Cost.new(tokens: empty_tokens, model:, category: :embeddings)
137
- }
138
-
139
- RubyLLM.instrument('embedding.ruby_llm', payload, config: config) do |event|
140
- result = Accounting::Usage.owned_by(owner) do
141
- provider_instance.embed(text, model:, dimensions:, task_type:, title:, with:, provider_options:)
142
- end
143
- event[:result] = result
144
- event[:response_model] = result.model
145
- event[:tokens] = result.tokens
146
- event[:cost] = result.cost
147
- event[:embedding_dimensions] = vector_dimensions(result.vectors)
148
- event[:embedding_count] = embedding_count(result.vectors)
149
- result
121
+ Accounting::Operation.new(
122
+ 'embedding', model, default: :default_embedding_model, category: :embeddings, provider:, assume_model_exists:,
123
+ context:, owner:,
124
+ report: lambda { |event, result|
125
+ event[:result] = result
126
+ event[:response_model] = result.model
127
+ event[:tokens] = result.tokens
128
+ event[:cost] = result.cost
129
+ event[:embedding_dimensions] = vector_dimensions(result.vectors)
130
+ event[:embedding_count] = embedding_count(result.vectors)
131
+ }
132
+ ).run(input: text, dimensions:, task_type:, title:, attachment_count: Attachment.wrap(with).size,
133
+ provider_options:, metadata:) do |provider_instance, model|
134
+ provider_instance.embed(text, model:, dimensions:, task_type:, title:, with:, provider_options:)
150
135
  end
151
136
  end
152
137
 
@@ -10,7 +10,7 @@ module RubyLLM
10
10
  # batch = RubyLLM.batch(requests)
11
11
  # # later, once batch.refresh.complete?
12
12
  # batch.results
13
- # requests.first.result.vectors
13
+ # requests.first.result.vector
14
14
  #
15
15
  class EmbeddingRequest
16
16
  include Support::Inspectable