ruby_llm 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +160 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +515 -274
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1593 -331
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +50 -24
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +127 -106
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +15 -15
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +1 -7
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +5 -14
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +213 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for sandbox classes that code execution runs model code in.
8
+ class SandboxGenerator < Rails::Generators::NamedBase
9
+ INITIALIZER = 'config/initializers/ruby_llm.rb'
10
+ CONFIGURE_BLOCK = /^RubyLLM\.configure do \|config\|\n/
11
+ MXC_REQUIREMENT = '>= 0.4.1'
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ namespace 'ruby_llm:sandbox'
16
+
17
+ desc 'Creates a sandbox class for code execution in app/sandboxes'
18
+
19
+ def check_sandbox_class_collision
20
+ class_collisions sandbox_class_name
21
+ end
22
+
23
+ def create_sandbox_file
24
+ template 'sandbox.rb.tt', File.join('app/sandboxes', class_path, "#{sandbox_file_name}.rb")
25
+ end
26
+
27
+ def add_mxc_gem
28
+ gemfile = File.join(destination_root, 'Gemfile')
29
+ return unless File.exist?(gemfile)
30
+
31
+ existing = File.read(gemfile)[/^\s*gem\s+["']mxc["'].*$/]
32
+ return warn_about_mxc_constraint(existing) if existing
33
+
34
+ gem 'mxc', MXC_REQUIREMENT
35
+ say "\nRun bundle install" if behavior == :invoke
36
+ end
37
+
38
+ def default_to_new_sandbox
39
+ initializer = File.join(destination_root, INITIALIZER)
40
+ return unless File.exist?(initializer)
41
+ return if behavior == :invoke && File.read(initializer).match?(/^\s*config\.sandbox\s*(\|\|)?=/)
42
+
43
+ unless File.read(initializer).match?(CONFIGURE_BLOCK)
44
+ return say %(Set config.sandbox = "#{sandbox_class_name}" where you configure RubyLLM), :yellow
45
+ end
46
+
47
+ inject_into_file INITIALIZER, %( config.sandbox = "#{sandbox_class_name}"\n), after: CONFIGURE_BLOCK
48
+ end
49
+
50
+ private
51
+
52
+ def warn_about_mxc_constraint(line)
53
+ requirement = line[/["']mxc["']\s*,\s*["']([^"']+)["']/, 1]
54
+ return if requirement.nil? || Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new('0.4.1'))
55
+
56
+ say %(Change the mxc requirement in your Gemfile to "#{MXC_REQUIREMENT}"), :yellow
57
+ end
58
+
59
+ def sandbox_class_name
60
+ class_name.end_with?('Sandbox') ? class_name : "#{class_name}Sandbox"
61
+ end
62
+
63
+ def sandbox_file_name
64
+ sandbox_class_name.demodulize.underscore
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ class <%= sandbox_class_name %> < MXC::Sandbox
2
+ timeout 120
3
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rails/generators'
5
+ require 'tmpdir'
6
+
7
+ module RubyLLM
8
+ module Generators
9
+ # Generator for skills in the open Agent Skills format.
10
+ class SkillGenerator < Rails::Generators::Base
11
+ namespace 'ruby_llm:skill'
12
+
13
+ def self.exit_on_failure?
14
+ true
15
+ end
16
+
17
+ argument :skill_name, type: :string, banner: 'name'
18
+ argument :description, type: :string, banner: '"What it does and when to use it"'
19
+
20
+ desc 'Creates a skill folder with its SKILL.md in app/skills'
21
+
22
+ def check_skill
23
+ Dir.mktmpdir do |root|
24
+ folder = File.join(root, skill_name)
25
+ FileUtils.mkdir_p(folder)
26
+ File.write(File.join(folder, 'SKILL.md'), skill_markdown)
27
+ RubyLLM::Skill.new(folder)
28
+ end
29
+ rescue RubyLLM::InvalidSkillError => e
30
+ raise Thor::Error, e.message.sub(%r{\A\S*/SKILL\.md }, '')
31
+ end
32
+
33
+ def create_skill_file
34
+ create_file File.join(skill_path, 'SKILL.md'), skill_markdown
35
+ end
36
+
37
+ private
38
+
39
+ def skill_path
40
+ File.join('app/skills', skill_name)
41
+ end
42
+
43
+ def skill_markdown
44
+ "---\nname: #{skill_name}\ndescription: #{description.to_json}\n---\n"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,7 +1,7 @@
1
1
  <div id="<%= message_variable_name %>_tool_call_<%%= tool_call.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
2
2
  <div>
3
3
  <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
4
- <%= tool_display_name %> Call
4
+ <%= tool_partial_name.humanize %> call
5
5
  </span>
6
6
  </div>
7
7
 
@@ -8,7 +8,7 @@
8
8
  <div id="<%= message_variable_name %>_<%%= tool.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
9
9
  <div>
10
10
  <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
11
- <%= tool_display_name %> Result
11
+ <%= tool_partial_name.humanize %> result
12
12
  </span>
13
13
  </div>
14
14
 
@@ -2,8 +2,5 @@ class <%= tool_class_name %> < RubyLLM::Tool
2
2
  description "TODO: describe what this tool does"
3
3
 
4
4
  def execute
5
- # TODO: return something to the LLM.
6
- # Anything you return here gets converted to a string.
7
- "TODO: implement <%= tool_class_name %>"
8
5
  end
9
6
  end
@@ -1,6 +1,6 @@
1
1
  <div id="<%= message_variable_name %>_tool_call_<%%= tool_call.id %>" class="<%= message_variable_name %>"
2
2
  style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
3
- <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Call</div>
3
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_partial_name.humanize %> call</div>
4
4
  <pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
5
5
  <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
6
6
  <%%= message.created_at&.strftime("%I:%M %p") %>
@@ -7,7 +7,7 @@
7
7
  <%% else %>
8
8
  <div id="<%= message_variable_name %>_<%%= tool.id %>" class="<%= message_variable_name %>"
9
9
  style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
10
- <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Result</div>
10
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_partial_name.humanize %> result</div>
11
11
  <pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
12
12
  <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
13
13
  <%%= tool.created_at&.strftime("%I:%M %p") %>
@@ -49,13 +49,9 @@ module RubyLLM
49
49
  tool_class_name.demodulize.underscore
50
50
  end
51
51
 
52
- def tool_display_name
53
- tool_class_name.demodulize.delete_suffix('Tool')
54
- end
55
-
56
52
  # Mirrors RubyLLM::Tool.tool_name, the key the runtime partial lookup uses.
57
53
  def tool_partial_name
58
- tool_file_name.delete_suffix('_tool')
54
+ RubyLLM::Support::Utils.underscore(tool_class_name).delete_suffix('_tool').gsub('::', '__')
59
55
  end
60
56
  end
61
57
  end
@@ -0,0 +1,164 @@
1
+ class <%= 'upgrade_ruby_llm_to_2_2'.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ unless column_exists?(:ruby_llm_tool_calls, :provider_call_id)
4
+ if column_exists?(:ruby_llm_tool_calls, :provider_id)
5
+ rename_column :ruby_llm_tool_calls, :provider_id, :provider_call_id
6
+ else
7
+ add_column :ruby_llm_tool_calls, :provider_call_id, :string
8
+ end
9
+ end
10
+
11
+ add_column :ruby_llm_tool_calls, :namespace, :string unless column_exists?(:ruby_llm_tool_calls, :namespace)
12
+
13
+ unless column_exists?(:<%= chat_table_name %>, :claim_token)
14
+ add_column :<%= chat_table_name %>, :claim_token, :string
15
+ end
16
+
17
+ unless column_exists?(:<%= chat_table_name %>, :claimed_until)
18
+ add_column :<%= chat_table_name %>, :claimed_until, :datetime
19
+ end
20
+
21
+ unless index_exists?(:<%= chat_table_name %>, :claimed_until)
22
+ add_index :<%= chat_table_name %>, :claimed_until<%= ', where: "claim_token IS NOT NULL"' unless mysql? %>
23
+ end
24
+
25
+ unless column_exists?(:ruby_llm_tool_calls, :claimed_until)
26
+ add_column :ruby_llm_tool_calls, :claimed_until, :datetime
27
+ end
28
+
29
+ unless column_exists?(:<%= message_table_name %>, :phase)
30
+ add_column :<%= message_table_name %>, :phase, :string
31
+ end
32
+
33
+ unless column_exists?(:<%= message_table_name %>, :continuation)
34
+ add_column :<%= message_table_name %>, :continuation, :text<%= ', size: :long' if mysql? %>
35
+ end
36
+ <% if mysql? -%>
37
+
38
+ message_columns = %i[content thinking_text thinking_signature].select do |column|
39
+ column_exists?(:<%= message_table_name %>, column)
40
+ end
41
+ if message_columns.any?
42
+ change_table :<%= message_table_name %>, bulk: true do |t|
43
+ message_columns.each { |column| t.change column, :text, size: :long }
44
+ end
45
+ end
46
+ change_column :ruby_llm_tool_calls, :thought_signature, :text, size: :long
47
+ <% end -%>
48
+
49
+ %i[workflow_id workflow_name workflow_step_id workflow_step_name].each do |column|
50
+ add_column :ruby_llm_usages, column, :string unless column_exists?(:ruby_llm_usages, column)
51
+ end
52
+ add_index :ruby_llm_usages, :workflow_id unless index_exists?(:ruby_llm_usages, :workflow_id)
53
+
54
+ add_column :ruby_llm_usages, :characters, :integer unless column_exists?(:ruby_llm_usages, :characters)
55
+ unless column_exists?(:ruby_llm_usages, :audio_seconds)
56
+ add_column :ruby_llm_usages, :audio_seconds, :decimal, precision: 16, scale: 6
57
+ end
58
+ add_column :ruby_llm_usages, :search_units, :integer unless column_exists?(:ruby_llm_usages, :search_units)
59
+ %i[characters_cost audio_seconds_cost search_units_cost].each do |column|
60
+ unless column_exists?(:ruby_llm_usages, column)
61
+ add_column :ruby_llm_usages, column, :decimal, precision: 16, scale: 10
62
+ end
63
+ end
64
+
65
+ unless table_exists?(:ruby_llm_queued_messages)
66
+ create_table :ruby_llm_queued_messages, id: :<%= reference_type %> do |t|
67
+ t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
68
+ t.references :sender, polymorphic: true, type: :<%= reference_type %>, index: false
69
+ t.text :content<%= ', size: :long' if mysql? %>
70
+ t.boolean :steer, default: true, null: false
71
+ t.timestamps
72
+
73
+ t.index [ :chat_type, :chat_id ]
74
+ t.index [ :sender_type, :sender_id ]
75
+ end
76
+ end
77
+
78
+ unless table_exists?(:ruby_llm_chat_files)
79
+ create_table :ruby_llm_chat_files, id: :<%= reference_type %> do |t|
80
+ t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
81
+ t.references :owner, polymorphic: true, type: :<%= reference_type %>, index: false
82
+ t.string :path, null: false
83
+ t.string :content_type
84
+ t.bigint :byte_size
85
+ t.string :digest
86
+ t.text :text<%= ', size: :long' if mysql? %>
87
+ t.string :read_digest
88
+ t.timestamps
89
+
90
+ t.index [ :chat_type, :chat_id, :path ]
91
+ t.index [ :owner_type, :owner_id ]
92
+ end
93
+ end
94
+ unless column_exists?(:ruby_llm_chat_files, :read_digest)
95
+ add_column :ruby_llm_chat_files, :read_digest, :string
96
+ end
97
+
98
+ allow_code_execution_usage unless usage_accepts_code_execution?
99
+ # Asks again: a check of the application's may still refuse the row.
100
+ warn_about_code_execution_usage unless usage_accepts_code_execution?
101
+ end
102
+
103
+ def down
104
+ drop_table :ruby_llm_chat_files, if_exists: true
105
+ drop_table :ruby_llm_queued_messages, if_exists: true
106
+ remove_column :<%= message_table_name %>, :continuation
107
+ %i[search_units_cost audio_seconds_cost characters_cost search_units audio_seconds characters].each do |column|
108
+ remove_column :ruby_llm_usages, column
109
+ end
110
+ remove_index :ruby_llm_usages, :workflow_id, if_exists: true
111
+ remove_column :ruby_llm_usages, :workflow_step_name
112
+ remove_column :ruby_llm_usages, :workflow_step_id
113
+ remove_column :ruby_llm_usages, :workflow_name
114
+ remove_column :ruby_llm_usages, :workflow_id
115
+ remove_column :<%= message_table_name %>, :phase
116
+ remove_column :ruby_llm_tool_calls, :namespace
117
+ remove_column :ruby_llm_tool_calls, :claimed_until
118
+ remove_index :<%= chat_table_name %>, :claimed_until, if_exists: true
119
+ remove_column :<%= chat_table_name %>, :claimed_until
120
+ remove_column :<%= chat_table_name %>, :claim_token
121
+ remove_column :ruby_llm_tool_calls, :provider_call_id
122
+ end
123
+
124
+ private
125
+
126
+ # Asks the database, since each one prints a check differently: a check
127
+ # refusing the row leaves code runs unmetered, with only a warning at run time.
128
+ def usage_accepts_code_execution?
129
+ connection.transaction(requires_new: true) do
130
+ execute <<~SQL.squish
131
+ INSERT INTO ruby_llm_usages (operation, provider, model, status, created_at, updated_at)
132
+ VALUES ('code_execution', 'sandbox', 'upgrade check', 'succeeded', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
133
+ SQL
134
+ raise ActiveRecord::Rollback
135
+ end
136
+ true
137
+ rescue ActiveRecord::StatementInvalid
138
+ false
139
+ end
140
+
141
+ def allow_code_execution_usage
142
+ lists = check_constraints(:ruby_llm_usages).select { |check| operation_list?(check.expression) }
143
+ lists.each { |check| remove_check_constraint :ruby_llm_usages, name: check.name }
144
+ return if lists.empty?
145
+
146
+ add_check_constraint :ruby_llm_usages, "operation<%= '::text' if postgresql? %> IN (<%= usage_operations_sql %>)"
147
+ end
148
+
149
+ # The list the install generator wrote, as each database prints it. A check
150
+ # with other conditions is the application's, and stays.
151
+ def operation_list?(expression)
152
+ names = expression.scan(/'(\w+)'/).flatten
153
+ expression.match?(/\A\(*[`"]?operation[`"]?\)*(?:::\w+)?\s*(?:in\b|=\s*any\b)/i) &&
154
+ !expression.match?(/\b(?:and|or|not)\b|<>|!=/i) && names.size > 1 && (names - %w[<%= ::RubyLLM::Accounting::Usage::Entry::OPERATIONS.join(' ') %>]).empty?
155
+ end
156
+
157
+ def warn_about_code_execution_usage
158
+ checks = check_constraints(:ruby_llm_usages).select { |check| check.expression.match?(/\boperation\b/) }
159
+ .map(&:name).join(', ')
160
+ say "RubyLLM: ruby_llm_usages refuses 'code_execution' rows, so code runs will go unmetered with only a " \
161
+ "warning in the log. Check these constraints, or any NOT NULL column you added: #{checks}. Allow the " \
162
+ 'value in a new migration: see "Code Runs Write Usage Rows" in the upgrading guide.'
163
+ end
164
+ end
@@ -6,7 +6,7 @@ require_relative '../generator_helpers'
6
6
 
7
7
  module RubyLLM
8
8
  module Generators
9
- # Upgrades the schema of an application on RubyLLM 2.0 to 2.1.
9
+ # Upgrades the schema of an application on RubyLLM 2.1 to 2.2.
10
10
  class UpgradeGenerator < Rails::Generators::Base
11
11
  include Rails::Generators::Migration
12
12
  include RubyLLM::Generators::GeneratorHelpers
@@ -16,7 +16,7 @@ module RubyLLM
16
16
 
17
17
  argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName'
18
18
 
19
- desc 'Upgrades a RubyLLM 2.0 Rails schema to 2.1\n' \
19
+ desc "Upgrades a RubyLLM 2.1 Rails schema to 2.2\n" \
20
20
  'Usage: bin/rails g ruby_llm:upgrade [chat:ChatName] [message:MessageName]'
21
21
 
22
22
  def self.next_migration_number(dirname)
@@ -24,7 +24,45 @@ module RubyLLM
24
24
  end
25
25
 
26
26
  def create_migration_file
27
- migration_template 'upgrade_ruby_llm_to_2_1.rb.tt', 'db/migrate/upgrade_ruby_llm_to_2_1.rb'
27
+ migration_template 'upgrade_ruby_llm_to_2_2.rb.tt', 'db/migrate/upgrade_ruby_llm_to_2_2.rb'
28
+ end
29
+
30
+ def create_application_agent_file
31
+ create_application_agent
32
+ end
33
+
34
+ def remove_generated_message_code_from_message_model
35
+ return if chat_ui_uses_generated_broadcasting?
36
+
37
+ enable_message_broadcasting if remove_generated_message_code
38
+ end
39
+
40
+ def install_active_storage
41
+ return if table_exists?('active_storage_blobs')
42
+
43
+ rails_command 'active_storage:install'
44
+ end
45
+
46
+ def show_next_step
47
+ say "\nRun bin/rails db:migrate" if behavior == :invoke
48
+ end
49
+
50
+ private
51
+
52
+ def chat_ui_uses_generated_broadcasting?
53
+ return false unless behavior == :invoke
54
+
55
+ model = File.join(destination_root, "app/models/#{message_model_name.underscore}.rb")
56
+ return false unless File.exist?(model)
57
+
58
+ source = File.read(model)
59
+ return false unless generated_broadcasting_blocks.any? { |block| source.include?(block) }
60
+
61
+ stream = %("#{chat_variable_name}_\#{)
62
+ Dir.glob(File.join(destination_root, 'app/**/*.{rb,erb}')).without(model).any? do |path|
63
+ source = File.read(path)
64
+ source.include?('broadcast_append_chunk') || source.include?(stream)
65
+ end
28
66
  end
29
67
  end
30
68
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Accounting
5
+ # The body the standalone operations share: resolve the model, open the
6
+ # instrumentation event, attribute the provider call to its owner, and
7
+ # report the result on the event. +run+ takes the event fields particular
8
+ # to the operation, in event order, and yields the provider and the
9
+ # resolved model.
10
+ #
11
+ # +details:+ adds fields after the response model. +report:+ replaces the
12
+ # reporting of an operation whose result does not read like the others.
13
+ # +usage: false+ leaves tokens and cost off the event. +resolved:+ takes
14
+ # a Model already resolved, such as a fallback given as one, and skips
15
+ # the lookup.
16
+ class Operation # :nodoc: all
17
+ def initialize(name, model, owner:, context: nil, provider: nil, assume_model_exists: false,
18
+ default: nil, operation: nil, category: nil, usage: true, details: nil, report: nil,
19
+ resolved: nil)
20
+ @name = name
21
+ @owner = owner
22
+ @category = category
23
+ @usage = usage
24
+ @details = details
25
+ @report = report
26
+ @config = context&.config || RubyLLM.config
27
+ @model, @provider = if resolved
28
+ [resolved, Provider.resolve!(resolved.provider).new(@config)]
29
+ else
30
+ resolve(model, default, operation, provider:, assume_model_exists:)
31
+ end
32
+ end
33
+
34
+ def run(**fields)
35
+ RubyLLM.instrument("#{@name}.ruby_llm", payload(fields), config: @config) do |event|
36
+ result = Usage.owned_by(@owner) { yield @provider, @model }
37
+ report(event, result)
38
+ result
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def resolve(model, default, operation, **options)
45
+ default_model = default && @config.public_send(default)
46
+ options[:config] = @config
47
+ return Models.resolve(model, **options, operation:, default_model:) if operation
48
+
49
+ Models.resolve(model || default_model, **options)
50
+ end
51
+
52
+ def payload(fields)
53
+ common = { provider: @provider.slug, provider_class: @provider.name, model: @model&.id, model_info: @model }
54
+ return common.merge(fields) unless @usage
55
+
56
+ tokens = Tokens.new
57
+ common.merge(fields, tokens:, cost: Cost.new(tokens:, model: @model, **{ category: @category }.compact))
58
+ end
59
+
60
+ def report(event, result)
61
+ return @report.call(event, result) if @report
62
+
63
+ event[:result] = result
64
+ event[:response_model] = result.model
65
+ @details&.call(event, result)
66
+ return unless @usage
67
+
68
+ event[:tokens] = result.tokens
69
+ event[:cost] = result.cost
70
+ end
71
+ end
72
+ end
73
+ end
@@ -31,6 +31,10 @@ module RubyLLM
31
31
  owner.nil? ? yield : with_owner(owner, &)
32
32
  end
33
33
 
34
+ def workflow
35
+ Support::Instrumentation.current_workflow
36
+ end
37
+
34
38
  def record(entry)
35
39
  ledger&.record(entry)
36
40
  end
@@ -40,20 +44,24 @@ module RubyLLM
40
44
  instrument(entry, config:)
41
45
  end
42
46
 
47
+ # An attempt reports the workflow it started in, even when it
48
+ # finishes outside that block, such as a polled video job.
43
49
  def instrument(entry, config:)
44
- RubyLLM.instrument(
45
- 'usage.ruby_llm',
46
- {
47
- operation: entry.operation,
48
- provider: entry.provider,
49
- model: entry.model,
50
- status: entry.status,
51
- tokens: entry.tokens,
52
- cost: entry.cost,
53
- owner: entry.owner
54
- },
55
- config:
56
- )
50
+ Support::Instrumentation.with_workflow(entry.workflow || workflow) do
51
+ RubyLLM.instrument(
52
+ 'usage.ruby_llm',
53
+ {
54
+ operation: entry.operation,
55
+ provider: entry.provider,
56
+ model: entry.model,
57
+ status: entry.status,
58
+ tokens: entry.tokens,
59
+ cost: entry.cost,
60
+ owner: entry.owner
61
+ },
62
+ config:
63
+ )
64
+ end
57
65
  end
58
66
  end
59
67
 
@@ -82,12 +90,26 @@ module RubyLLM
82
90
  end
83
91
  end
84
92
 
93
+ # The registry Model of a result that names one. The usage tracker
94
+ # assigns it; a result built by hand looks up #model.
95
+ module ModelInfo
96
+ attr_writer :model_info # :nodoc:
97
+
98
+ def model_info # :nodoc:
99
+ @model_info ||= RubyLLM.models.find(model)
100
+ rescue ModelNotFoundError
101
+ nil
102
+ end
103
+ end
104
+
85
105
  # One physical provider request attempt and its accounting facts.
86
106
  class Entry
87
- OPERATIONS = %i[chat embedding moderation image speech transcription ocr rerank judgment video research].freeze
107
+ OPERATIONS = %i[
108
+ chat embedding moderation image speech transcription ocr rerank judgment video research code_execution
109
+ ].freeze
88
110
  STATUSES = %i[pending succeeded failed cancelled].freeze
89
111
 
90
- attr_reader :operation, :provider, :model, :status, :tokens, :owner
112
+ attr_reader :operation, :provider, :model, :status, :tokens, :owner, :workflow
91
113
  attr_accessor :message
92
114
 
93
115
  include Support::Inspectable
@@ -97,7 +119,7 @@ module RubyLLM
97
119
  end
98
120
 
99
121
  def initialize(operation:, provider:, model:, status: :pending, tokens: nil, cost: nil, message: nil,
100
- owner: nil)
122
+ owner: nil, workflow: nil)
101
123
  @operation = operation.to_sym
102
124
  @provider = provider.to_s
103
125
  @model = model&.to_s
@@ -106,6 +128,7 @@ module RubyLLM
106
128
  @cost = cost
107
129
  @message = message
108
130
  @owner = owner
131
+ @workflow = workflow
109
132
  validate!
110
133
  end
111
134
 
@@ -161,6 +184,11 @@ module RubyLLM
161
184
  judgment: :text_tokens
162
185
  }.freeze
163
186
 
187
+ CONNECT_FAILURES = [
188
+ Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError, Net::OpenTimeout
189
+ ].freeze
190
+ TLS_HANDSHAKE_FAILURE = /\ASSL_connect|does not match the server certificate/
191
+
164
192
  attr_reader :entries
165
193
 
166
194
  def initialize(operation:, provider:, model:, config:, on_finish: nil, model_id: nil)
@@ -176,13 +204,14 @@ module RubyLLM
176
204
 
177
205
  def start
178
206
  entry = Entry.new(operation: @operation, provider: @provider.slug, model: @model_id || @model_info&.id,
179
- owner: Usage.owner)
207
+ owner: Usage.owner, workflow: Usage.workflow)
180
208
  @entries << entry
181
209
  @pending << entry
182
210
  entry
183
211
  end
184
212
 
185
213
  def observe(chunk)
214
+ @answered = @pending.last
186
215
  observe_tokens(chunk.tokens) if chunk.respond_to?(:tokens)
187
216
  end
188
217
 
@@ -207,6 +236,12 @@ module RubyLLM
207
236
  @pending.dup.each { |entry| fail_attempt(entry, error) }
208
237
  end
209
238
 
239
+ # Finishes the attempts an exception outside StandardError
240
+ # interrupted, such as a stopped task or a shutdown.
241
+ def cancel_pending
242
+ @pending.dup.each { |entry| finish(entry, status: :cancelled, tokens: entry.tokens) }
243
+ end
244
+
210
245
  def succeed(result)
211
246
  # A request that produced several results, like a multi-image
212
247
  # generation, is billed once: the first result carries the call.
@@ -246,23 +281,38 @@ module RubyLLM
246
281
  end
247
282
 
248
283
  # A request that never reached the provider, or that the provider
249
- # refused before running it, cannot have been billed; possibly-billed
250
- # failures keep their usage unknown.
284
+ # answered with an error before streaming anything, cannot have been
285
+ # billed; possibly-billed failures keep their usage unknown.
251
286
  def failure_tokens(entry, error)
252
287
  return entry.tokens unless @model_info
253
- return entry.tokens if entry.usage_available? || (!never_sent?(error) && !refused?(error))
288
+ return entry.tokens if entry.usage_available? || (!never_sent?(entry, error) && !refused?(entry, error))
254
289
 
255
290
  Tokens.new(input: 0, output: 0)
256
291
  end
257
292
 
258
- def never_sent?(error)
259
- error.is_a?(Faraday::ConnectionFailed) || error.is_a?(Faraday::SSLError)
293
+ # Faraday also raises its connection errors for a connection that
294
+ # drops after the request was sent, so only the wrapped cause tells
295
+ # whether the connection was ever established.
296
+ def never_sent?(entry, error)
297
+ return false if entry.equal?(@answered)
298
+
299
+ error = transport_error(error)
300
+ return false unless error.is_a?(Faraday::ConnectionFailed) || error.is_a?(Faraday::SSLError)
301
+
302
+ cause = error.wrapped_exception || error.cause
303
+ CONNECT_FAILURES.any? { |failure| cause.is_a?(failure) } ||
304
+ (cause.is_a?(OpenSSL::SSL::SSLError) && cause.message.match?(TLS_HANDSHAKE_FAILURE))
305
+ end
306
+
307
+ def transport_error(error)
308
+ error.is_a?(ConnectionError) ? error.cause : error
260
309
  end
261
310
 
262
- def refused?(error)
311
+ def refused?(entry, error)
263
312
  return false unless error.is_a?(Error) && error.response.respond_to?(:status)
264
313
 
265
- (400..499).cover?(error.response.status)
314
+ status = error.response.status
315
+ (400..499).cover?(status) || ((500..599).cover?(status) && !entry.equal?(@answered))
266
316
  end
267
317
 
268
318
  def finish(entry, status:, tokens:, **details)