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
@@ -4,9 +4,8 @@ module RubyLLM
4
4
  # Error is the base class for provider-operation errors raised by
5
5
  # RubyLLM, including API, network, capability, and provider response-shape
6
6
  # failures. When an HTTP response is available, it wraps that response and
7
- # normalizes the message across providers. When the provider refuses a
8
- # conversation, #request_shape describes what it was sent. Subclasses map
9
- # common HTTP status codes: BadRequestError (400), UnauthorizedError (401),
7
+ # normalizes the message across providers. Subclasses map common HTTP
8
+ # status codes: BadRequestError (400), UnauthorizedError (401),
10
9
  # PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
11
10
  # ServerError (500), ServiceUnavailableError (502 to 504), and
12
11
  # OverloadedError (529).
@@ -20,6 +19,11 @@ module RubyLLM
20
19
  # puts e.response&.status
21
20
  # end
22
21
  #
22
+ # Providers often quote the request back in their error, so with
23
+ # Configuration#redact_error_messages the message carries only the HTTP
24
+ # status, the provider, and the provider's error code. What the provider
25
+ # wrote stays on #provider_message and its raw body on #response_body.
26
+ #
23
27
  # Local setup and programming errors, such as ConfigurationError and
24
28
  # ModelNotFoundError, inherit from StandardError directly and are not
25
29
  # caught by rescuing Error.
@@ -28,6 +32,20 @@ module RubyLLM
28
32
  # available. Its +status+ and +body+ carry the provider's reply.
29
33
  attr_reader :response
30
34
 
35
+ # The slug of the provider that answered, such as <tt>"openai"</tt>, or
36
+ # +nil+ when it is not known.
37
+ attr_reader :provider
38
+
39
+ # The provider's code for the error, such as
40
+ # <tt>"invalid_request_error"</tt> or <tt>"INVALID_ARGUMENT"</tt>, or
41
+ # +nil+ when it gave none that reads like a code.
42
+ attr_reader :error_code
43
+
44
+ # What the provider wrote about the error, or +nil+ when it wrote
45
+ # nothing RubyLLM could read. It can quote the request, so
46
+ # Configuration#redact_error_messages keeps it out of #message.
47
+ attr_reader :provider_message
48
+
31
49
  # The Protocol that rendered the request the error answered, which
32
50
  # reads its payload back for #request_shape.
33
51
  attr_accessor :request_protocol # :nodoc:
@@ -39,11 +57,28 @@ module RubyLLM
39
57
  nil
40
58
  end
41
59
 
60
+ ERROR_CODE = /\A[A-Za-z][\w.:-]{0,63}\z/ # :nodoc:
61
+
42
62
  # Creates an error with +message+. Pass +response:+ to attach the HTTP
43
- # response; its body supplies the message when +message+ is +nil+.
44
- def initialize(message = nil, response: nil)
63
+ # response, +provider:+ to name the provider's slug, +error_code:+ for
64
+ # the provider's code, kept only when it reads like one, and
65
+ # +provider_message:+ for what the provider wrote, which follows
66
+ # +message+, or stands in for it when +message+ is +nil+. Without
67
+ # either, the response body supplies the message.
68
+ def initialize(message = nil, response: nil, provider: nil, provider_message: nil, error_code: nil, redact: nil)
45
69
  @response = response
46
- super(message || response&.body || self.class.default_message)
70
+ @provider = provider
71
+ @provider_message = provider_message
72
+ @error_code = error_code if error_code.is_a?(Integer) || error_code.to_s.match?(ERROR_CODE)
73
+ redact = RubyLLM.config.redact_error_messages if redact.nil?
74
+ super(full_message_for(message, redact:) || self.class.default_message)
75
+ end
76
+
77
+ # Returns the body of #response, or +nil+ without one. It can quote the
78
+ # request, so Configuration#redact_error_messages keeps it out of
79
+ # #message.
80
+ def response_body
81
+ response&.body
47
82
  end
48
83
 
49
84
  # Returns a RequestShape describing the conversation request the error
@@ -57,12 +92,40 @@ module RubyLLM
57
92
  # Rails.logger.error("#{e.message}\n#{e.request_shape}")
58
93
  # Rails.error.report(e, context: { request_shape: e.request_shape&.to_h })
59
94
  #
60
- def request_shape
95
+ def request_shape # :nodoc:
61
96
  return unless request_protocol
62
97
  return @request_shape if defined?(@request_shape)
63
98
 
64
99
  @request_shape = request_protocol.request_shape(request_payload)
65
100
  end
101
+
102
+ private
103
+
104
+ def full_message_for(message, redact:)
105
+ return message || (redacted_message if remote?) if redact
106
+ return "#{message}: #{provider_message}" if message && provider_message
107
+
108
+ message || provider_message || response_body_message
109
+ end
110
+
111
+ def remote?
112
+ !response.nil? || !provider_message.nil?
113
+ end
114
+
115
+ def redacted_message
116
+ status = response.status if response.respond_to?(:status)
117
+ text = "#{provider || redacted_source} returned #{status ? "HTTP #{status}" : 'an error'}"
118
+ error_code ? "#{text} (#{error_code})" : text
119
+ end
120
+
121
+ def redacted_source
122
+ 'The provider'
123
+ end
124
+
125
+ def response_body_message
126
+ body = response_body
127
+ body unless body.respond_to?(:empty?) && body.empty?
128
+ end
66
129
  end
67
130
 
68
131
  # Raised when a deprecated API is used and
@@ -75,12 +138,28 @@ module RubyLLM
75
138
  class DeprecationError < StandardError; end
76
139
 
77
140
  # Raised when required configuration, such as a provider API key, is
78
- # missing.
141
+ # missing, or when a setup can't do what a chat asks of it, such as a
142
+ # subagent added with +tools+ that stops to wait (see AgentTool).
79
143
  class ConfigurationError < StandardError; end
80
144
 
81
145
  # Raised when RubyLLM.render_prompt cannot find the named prompt file.
82
146
  class PromptNotFoundError < StandardError; end
83
147
 
148
+ # Raised when a skill named by Chat#with_skills or Skill.find is missing.
149
+ class SkillNotFoundError < StandardError; end
150
+
151
+ # Raised when a skill's +SKILL.md+ breaks the Agent Skills specification:
152
+ # missing or malformed front matter, or a name or description it rejects.
153
+ class InvalidSkillError < StandardError; end
154
+
155
+ # Raised when a FileStore has no file at the path asked for.
156
+ class FileNotFoundError < StandardError; end
157
+
158
+ # Raised by a FileStore when a write would take it past its limit. Its
159
+ # message is what the model reads, so say what to do. The file tools and
160
+ # +run_code+ answer the model with it instead of failing the reply.
161
+ class FileStoreFullError < StandardError; end
162
+
84
163
  # Raised when a message role outside +:system+, +:user+, +:assistant+, or
85
164
  # +:tool+ is used.
86
165
  class InvalidRoleError < StandardError; end
@@ -92,9 +171,20 @@ module RubyLLM
92
171
  # Raised when a user message is staged while the last response still has
93
172
  # unanswered tool calls. Providers reject such a transcript, so the chat
94
173
  # refuses it up front: finish the round with #complete, recording
95
- # #approve or #deny decisions for calls that require approval.
174
+ # #approve or #decline decisions for calls that require approval.
96
175
  class PendingToolCallsError < StandardError; end
97
176
 
177
+ # Raised by Chat#complete when the limit set with
178
+ # <tt>Chat#with_tool_options(rounds:)</tt> is reached. Every tool call of
179
+ # the rounds that ran has its result, so the conversation stays valid and
180
+ # another #complete continues with a fresh count. Not a RubyLLM::Error,
181
+ # which a record takes for a failed request and a job may retry.
182
+ class ToolRoundLimitError < StandardError; end
183
+
184
+ # Raised when a persisted tool call is declined after it started running.
185
+ # The call keeps running and the model receives its result.
186
+ class ToolCallRunningError < StandardError; end
187
+
98
188
  # Raised when a requested model id is not in the model registry.
99
189
  class ModelNotFoundError < StandardError; end
100
190
 
@@ -133,6 +223,24 @@ module RubyLLM
133
223
  end
134
224
  end
135
225
 
226
+ # Raised when the provider rejects a request because the model does not
227
+ # take one of its parameters, such as a temperature on a reasoning model.
228
+ # Chat#with_fallbacks can retry without it with <tt>drop_unsupported: true</tt>.
229
+ class UnsupportedParameterError < BadRequestError
230
+ # The parameter the provider rejected: a Symbol naming the RubyLLM
231
+ # setting, such as +:temperature+, +:thinking+, or +:max_output_tokens+,
232
+ # or the provider's own name as a String for anything else, such as a
233
+ # key of Chat#with_provider_options.
234
+ attr_reader :parameter
235
+
236
+ # Creates the error for +parameter+. Takes the other arguments of
237
+ # Error.new.
238
+ def initialize(message = nil, parameter: nil, **)
239
+ @parameter = parameter
240
+ super(message, **)
241
+ end
242
+ end
243
+
136
244
  # Raised for HTTP 403 responses when the API key lacks permission for the
137
245
  # requested resource.
138
246
  class ForbiddenError < Error
@@ -149,17 +257,39 @@ module RubyLLM
149
257
  end
150
258
  end
151
259
 
152
- # Raised when a provider returns tool-call arguments that are not valid
153
- # JSON, often because the response was truncated mid-tool-call.
260
+ # The parent of ToolCallTruncatedError, the error RubyLLM raises for
261
+ # tool-call arguments it cannot read. Other arguments that are not a JSON
262
+ # object do not raise: the chat answers the call with an error result
263
+ # and the model calls the tool again.
154
264
  class ToolCallParseError < Error
155
265
  # Returns the normalized reason generation stopped, or +nil+.
156
266
  attr_reader :finish_reason
157
267
 
158
268
  def initialize(message = nil, response: nil, finish_reason: nil) # :nodoc:
159
269
  @finish_reason = finish_reason
160
- message ||= 'Provider returned malformed tool call arguments'
161
- message = "#{message} (finish_reason: #{finish_reason})" if finish_reason
162
- super(message, response: response)
270
+ super(message || default_message, response: response)
271
+ end
272
+
273
+ private
274
+
275
+ def default_message
276
+ message = 'Provider returned malformed tool call arguments'
277
+ finish_reason ? "#{message} (finish_reason: #{finish_reason})" : message
278
+ end
279
+ end
280
+
281
+ # Raised when the response reached its output token limit while the model
282
+ # was writing a tool call, so the call's arguments are incomplete. Raise
283
+ # the limit with Chat#with_max_output_tokens, or ask for less at once.
284
+ class ToolCallTruncatedError < ToolCallParseError
285
+ def initialize(message = nil, response: nil) # :nodoc:
286
+ super(message, response:, finish_reason: :max_tokens)
287
+ end
288
+
289
+ private
290
+
291
+ def default_message
292
+ 'The response reached its output token limit while writing a tool call (finish_reason: max_tokens)'
163
293
  end
164
294
  end
165
295
 
@@ -207,6 +337,29 @@ module RubyLLM
207
337
  end
208
338
  end
209
339
 
340
+ # Raised when a request never gets an answer from the provider or an
341
+ # MCP server: the connection is refused or drops, TLS fails, or the
342
+ # request times out. RubyLLM retries provider requests first, following
343
+ # Configuration#max_retries. The original Faraday error is its +cause+.
344
+ #
345
+ # rescue RubyLLM::ConnectionError => e
346
+ # retry_later if e.timeout?
347
+ class ConnectionError < Error
348
+ # Returns whether the request timed out, including while opening the
349
+ # connection or waiting for the next bytes of a stream, rather than
350
+ # failing to connect.
351
+ def timeout?
352
+ return true if cause.is_a?(Faraday::TimeoutError)
353
+
354
+ cause.respond_to?(:wrapped_exception) && TIMEOUTS.any? { |timeout| cause.wrapped_exception.is_a?(timeout) }
355
+ end
356
+
357
+ # Some adapters, such as async-http-faraday, report a read that waited
358
+ # too long as a failed connection wrapping IO::TimeoutError.
359
+ TIMEOUTS = [Timeout::Error, IO::TimeoutError].freeze # :nodoc:
360
+ private_constant :TIMEOUTS
361
+ end
362
+
210
363
  # Raised for HTTP 401 responses when the API key is missing or invalid.
211
364
  class UnauthorizedError < Error
212
365
  def self.default_message # :nodoc:
@@ -42,8 +42,7 @@ module RubyLLM
42
42
  def discover(name)
43
43
  raise ArgumentError, 'An anonymous evaluation needs an explicit dataset' unless name
44
44
 
45
- filename = name.gsub('::', '/').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
46
- .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
45
+ filename = Support::Utils.file_path(name)
47
46
  root = Prompt.root.parent.join('evals')
48
47
  paths = %w[yml yaml json jsonl].map { |extension| root.join("#{filename}.#{extension}") }.select(&:file?)
49
48
  raise ArgumentError, "Dataset not found: #{root.join(filename)}.{yml,yaml,json,jsonl}" if paths.empty?
@@ -55,8 +54,10 @@ module RubyLLM
55
54
  def read(path)
56
55
  case File.extname(path)
57
56
  when '.yml', '.yaml' then YAML.safe_load_file(path, aliases: false)
58
- when '.json' then JSON.parse(File.read(path))
59
- when '.jsonl' then File.foreach(path).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) }
57
+ when '.json' then JSON.parse(File.read(path, encoding: Encoding::UTF_8))
58
+ when '.jsonl'
59
+ File.foreach(path, encoding: Encoding::UTF_8).reject { |line| line.strip.empty? }
60
+ .map { |line| JSON.parse(line) }
60
61
  else raise ArgumentError, "Unsupported dataset format: #{path}"
61
62
  end
62
63
  end
@@ -26,7 +26,7 @@ module RubyLLM
26
26
  when Message then [conversation]
27
27
  else []
28
28
  end.freeze
29
- @tool_calls = messages.flat_map { |message| Array(message.tool_calls&.values) }
29
+ @tool_calls = messages.flat_map { |message| message.tool_calls.to_a }
30
30
  @tool_calls << result if result.is_a?(ToolCall)
31
31
  @tool_calls.freeze
32
32
  @output = primary_output(conversation, result)
@@ -57,12 +57,13 @@ module RubyLLM
57
57
  when Attachment then attachment_data(value)
58
58
  when Hash then serialize_hash(value, parents)
59
59
  when Array then value.map { |item| serialize(item, parents) }
60
+ when Numeric then value.to_f
60
61
  else serialize_object(value, parents)
61
62
  end
62
63
  end
63
64
 
64
65
  def scalar?(value)
65
- [String, Symbol, Numeric, TrueClass, FalseClass, NilClass].any? { |type| value.is_a?(type) }
66
+ [String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass].any? { |type| value.is_a?(type) }
66
67
  end
67
68
 
68
69
  def serialize_hash(value, parents)
@@ -95,7 +96,8 @@ module RubyLLM
95
96
  def conversation_data(chat, parents)
96
97
  answer = chat.messages.reverse.find { |message| message.role == :assistant }&.content
97
98
  { output: answer, messages: serialize(chat.messages, parents), complete: chat.complete?, waiting: chat.waiting?,
98
- pending_approvals: chat.pending_approvals.map(&:id), cancelled: chat.cancelled?, model: chat.model.id }
99
+ pending_questions: chat.pending_questions.map(&:id), cancelled: chat.cancelled?,
100
+ model: chat.model.id }
99
101
  end
100
102
 
101
103
  def message_data(message, parents)
@@ -68,14 +68,18 @@ module RubyLLM
68
68
 
69
69
  # Writes a JSON report and returns the supplied path.
70
70
  def save(path)
71
- File.write(path, JSON.pretty_generate(to_h))
71
+ File.write(File.expand_path(path), JSON.pretty_generate(to_h))
72
72
  path
73
73
  end
74
74
 
75
- # Returns a readable per-case report followed by counts.
75
+ # Returns a readable per-case report followed by counts, and the total
76
+ # cost when every amount is known.
76
77
  def to_s
77
78
  rows = trials.map { |trial| trial_description(trial) }
78
- [name, *rows, counts.map { |status, count| "#{count} #{status}" }.join(', ')].join("\n")
79
+ total = cost.total
80
+ summary = counts.map { |status, count| "#{count} #{status}" }.join(', ')
81
+ summary += format(', cost $%.4f', total) if total
82
+ [name, *rows, summary].join("\n")
79
83
  end
80
84
 
81
85
  def inspect_attributes # :nodoc:
@@ -62,7 +62,7 @@ module RubyLLM
62
62
 
63
63
  # Returns the measurement, policy, and outcome as a Hash.
64
64
  def to_h
65
- { name:, status:, value: value.respond_to?(:to_h) ? value.to_h : value,
65
+ { name:, status:, value: value.nil? || !value.respond_to?(:to_h) ? value : value.to_h,
66
66
  reason:, minimum: @minimum, model:, evidence:,
67
67
  error: error && { class: error.class.name, message: error.message } }
68
68
  end
@@ -63,7 +63,8 @@ module RubyLLM
63
63
 
64
64
  @declared_names << name
65
65
  backend = Evaluator.new(evaluator) if evaluator
66
- definitions[name] = { name:, instructions: instructions&.dup&.freeze, minimum:, evaluator: backend }.freeze
66
+ definitions[name] = { name:, instructions: instructions&.dup&.freeze, minimum: minimum&.to_f,
67
+ evaluator: backend }.freeze
67
68
  end
68
69
 
69
70
  # Converts a custom result into evaluation evidence. The original stays available as result.
@@ -5,7 +5,10 @@ module RubyLLM
5
5
  # When the active model fails with a matching error, the chat retries the
6
6
  # request with each fallback in order. Instances are yielded to
7
7
  # Chat#before_fallback and Chat#after_fallback callbacks, enriched with the
8
- # details of that attempt.
8
+ # details of that attempt. With <tt>drop_unsupported: true</tt>, a retry
9
+ # of the same model without a parameter it rejected is an attempt too:
10
+ # its #from and #to are that model, and #dropped_parameter names what
11
+ # it left out.
9
12
  #
10
13
  # chat = RubyLLM.chat(model: "gpt-4.1")
11
14
  # .with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5")
@@ -27,11 +30,11 @@ module RubyLLM
27
30
  ServerError,
28
31
  ServiceUnavailableError,
29
32
  OverloadedError,
30
- Faraday::TimeoutError,
31
- Faraday::ConnectionFailed
33
+ ConnectionError
32
34
  ].freeze
33
35
 
34
- # The model id of the fallback target.
36
+ # The model id of the fallback target, or of the model a retry without a
37
+ # rejected parameter runs on.
35
38
  attr_reader :id
36
39
 
37
40
  # The provider slug of the fallback target as a String, or +nil+.
@@ -56,6 +59,11 @@ module RubyLLM
56
59
  # The fallback attempt number, starting at 1.
57
60
  attr_reader :attempt
58
61
 
62
+ # The parameter a retry on the same model left out, as
63
+ # UnsupportedParameterError#parameter names it, or +nil+ for a fallback
64
+ # to another model. See Chat#with_fallbacks.
65
+ attr_reader :dropped_parameter
66
+
59
67
  # The response Message from a successful fallback attempt, or +nil+.
60
68
  attr_reader :response
61
69
 
@@ -78,6 +86,25 @@ module RubyLLM
78
86
  end
79
87
  end
80
88
 
89
+ # Returns whether +error+ is one of +errors+ and may move on to a
90
+ # fallback. A cancellation never does.
91
+ def self.error?(error, errors) # :nodoc:
92
+ !error.is_a?(CancelledError) && errors.any? { |error_class| error.is_a?(error_class) }
93
+ end
94
+
95
+ # Takes fallbacks off +queue+ until the block accepts one, skipping
96
+ # those whose model can't be resolved. Returns the fallback and the
97
+ # block's value, or +nil+ when none is left.
98
+ def self.next_usable(queue) # :nodoc:
99
+ while (fallback = queue.shift)
100
+ begin
101
+ return [fallback, yield(fallback)]
102
+ rescue ModelNotFoundError, ConfigurationError => e
103
+ RubyLLM.logger.warn("RubyLLM skipped fallback #{fallback.id}: #{e.message}")
104
+ end
105
+ end
106
+ end
107
+
81
108
  def inspect_attributes # :nodoc:
82
109
  { id: id, provider: provider, attempt: attempt }
83
110
  end
@@ -95,6 +122,7 @@ module RubyLLM
95
122
  @chunks_yielded = attributes.fetch(:chunks_yielded, false)
96
123
  @response = attributes[:response]
97
124
  @fallback_error = attributes[:fallback_error]
125
+ @dropped_parameter = attributes[:dropped_parameter]
98
126
  end
99
127
 
100
128
  def with_attempt(**attributes) # :nodoc:
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class FileStore
5
+ module Converters
6
+ # Reads scans, photos, and PDFs with RubyLLM.ocr. Each file costs one
7
+ # OCR request the first time the model reads or searches it, and is
8
+ # kept after that. Register it in Configuration#file_converters for
9
+ # the types it should read:
10
+ #
11
+ # config.file_converters["image/*"] = RubyLLM::FileStore::Converters::OCR.new
12
+ # config.file_converters["application/pdf"] = OCR.new(model: "mistral-ocr-latest")
13
+ #
14
+ class OCR
15
+ # Creates a converter that calls RubyLLM.ocr with +options+, such as
16
+ # +model:+, +provider:+, or +context:+.
17
+ def initialize(**options)
18
+ @options = options
19
+ end
20
+
21
+ # Returns the Markdown RubyLLM.ocr reads from +attachment+.
22
+ def call(attachment)
23
+ RubyLLM.ocr(attachment, **@options).markdown
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class FileStore
5
+ module Converters
6
+ module Office
7
+ # Walks the tags of an Office XML part from left to right, so its
8
+ # time grows with the part's size whatever the part holds: nothing
9
+ # backtracks over unclosed tags. Names lose their prefix, since
10
+ # generators pick their own. Parts are binary Strings.
11
+ class Markup # :nodoc:
12
+ ENTITIES = { 'amp' => '&', 'lt' => '<', 'gt' => '>', 'quot' => '"', 'apos' => "'" }.freeze
13
+ ENTITY = /&(?:#(\d+)|#x(\h+)|(amp|lt|gt|quot|apos));/
14
+ NAME = %r{\A(?:[^\s:/]+:)?([^\s/]+)}
15
+ ATTRIBUTES = Hash.new do |patterns, name|
16
+ patterns[name] = /\s#{Regexp.escape(name)}=(?:"([^"]*)"|'([^']*)')/
17
+ end
18
+ PREFIXED = Hash.new do |patterns, name|
19
+ patterns[name] = /\s(?:[\w.-]+:)?#{Regexp.escape(name)}=(?:"([^"]*)"|'([^']*)')/
20
+ end
21
+ RELATIONSHIP_ID = /\s[\w.-]+:id=(?:"([^"]*)"|'([^']*)')/
22
+ SLASH = 47
23
+ QUESTION = 63
24
+ BANG = 33
25
+ private_constant :ENTITIES, :ENTITY, :NAME, :ATTRIBUTES, :PREFIXED, :RELATIONSHIP_ID, :SLASH, :QUESTION, :BANG
26
+
27
+ class << self
28
+ # Yields <tt>:open</tt>, <tt>:empty</tt>, or <tt>:close</tt> with
29
+ # each tag's name and its source, and <tt>:text</tt> with nil and
30
+ # the text between tags, leaving out the elements named in +skip+.
31
+ def scan(xml, skip: [], &)
32
+ new(xml.to_s.b, skip, &).run
33
+ end
34
+
35
+ # Returns the source of each tag named +name+.
36
+ def tags(xml, name)
37
+ found = []
38
+ scan(xml) { |kind, tag_name, source| found << source if tag_name == name && kind != :close }
39
+ found
40
+ end
41
+
42
+ # Returns the value of an attribute in a tag's source, unescaped.
43
+ def attribute(tag, name)
44
+ match = ATTRIBUTES[name].match(tag.to_s)
45
+ unescape(match[1] || match[2]) if match
46
+ end
47
+
48
+ # Returns the value of an attribute under any prefix, or none.
49
+ def prefixed(tag, name)
50
+ match = PREFIXED[name].match(tag.to_s)
51
+ unescape(match[1] || match[2]) if match
52
+ end
53
+
54
+ # Returns the prefixed +id+ attribute of a tag's source, whatever
55
+ # its prefix, which is how a part refers to a relationship.
56
+ def relationship_id(tag)
57
+ match = RELATIONSHIP_ID.match(tag.to_s)
58
+ unescape(match[1] || match[2]) if match
59
+ end
60
+
61
+ # Returns +text+ as UTF-8, with the entities XML defines replaced.
62
+ def unescape(text)
63
+ text = text.to_s.dup.force_encoding(Encoding::UTF_8).scrub
64
+ return text unless text.include?('&')
65
+
66
+ text.gsub(ENTITY) do
67
+ named = Regexp.last_match(3)
68
+ next ENTITIES.fetch(named) if named
69
+
70
+ character(Regexp.last_match(1)&.to_i || Regexp.last_match(2).to_i(16))
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def character(code)
77
+ [code].pack('U').then { |text| text.valid_encoding? ? text : '' }
78
+ rescue RangeError
79
+ ''
80
+ end
81
+ end
82
+
83
+ def initialize(xml, skip, &block)
84
+ @xml = xml
85
+ @skip = skip
86
+ @block = block
87
+ @position = 0
88
+ @skipping = nil
89
+ @depth = 0
90
+ end
91
+
92
+ def run
93
+ while (start = @xml.index('<', @position))
94
+ finish = @xml.index('>', start)
95
+ break unless finish
96
+
97
+ @block.call(:text, nil, @xml.byteslice(@position, start - @position)) if start > @position && !@skipping
98
+ @position = finish + 1
99
+ tag(start, finish)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def tag(start, finish)
106
+ first = @xml.getbyte(start + 1)
107
+ return if [QUESTION, BANG].include?(first)
108
+
109
+ if first == SLASH
110
+ closing(name_of(@xml.byteslice(start + 2, finish - start - 2)))
111
+ else
112
+ opening(@xml.byteslice(start + 1, finish - start - 1))
113
+ end
114
+ end
115
+
116
+ def opening(source)
117
+ name = name_of(source)
118
+ empty = source.end_with?('/')
119
+ return count(name, empty) if @skipping
120
+ return start_skipping(name) if !empty && @skip.include?(name)
121
+
122
+ @block.call(empty ? :empty : :open, name, source)
123
+ end
124
+
125
+ def closing(name)
126
+ return @block.call(:close, name, nil) unless @skipping
127
+
128
+ @depth -= 1 if name == @skipping
129
+ @skipping = nil if @depth.zero?
130
+ end
131
+
132
+ def count(name, empty)
133
+ @depth += 1 if name == @skipping && !empty
134
+ end
135
+
136
+ def start_skipping(name)
137
+ @skipping = name
138
+ @depth = 1
139
+ end
140
+
141
+ def name_of(source)
142
+ source[NAME, 1].to_s
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end