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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for evaluations and their datasets.
8
+ class EvaluationGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:evaluation'
12
+
13
+ desc 'Creates an evaluation class and its dataset in app/evals'
14
+
15
+ def check_evaluation_class_collision
16
+ class_collisions evaluation_class_name
17
+ end
18
+
19
+ def create_evaluation_file
20
+ template 'evaluation.rb.tt', File.join('app/evals', class_path, "#{evaluation_file_name}.rb")
21
+ end
22
+
23
+ def create_dataset_file
24
+ template 'evaluation.yml.tt', File.join('app/evals', class_path, "#{evaluation_file_name}.yml")
25
+ end
26
+
27
+ private
28
+
29
+ def evaluation_class_name
30
+ class_name.end_with?('Evaluation') ? class_name : "#{class_name}Evaluation"
31
+ end
32
+
33
+ def evaluation_file_name
34
+ evaluation_class_name.demodulize.underscore
35
+ end
36
+
37
+ def agent_class_name
38
+ name = "#{evaluation_class_name.delete_suffix('Evaluation')}Agent"
39
+ name if File.exist?(File.join(destination_root, 'app/agents', "#{name.underscore}.rb"))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ class <%= evaluation_class_name %> < RubyLLM::Evaluation
2
+ def perform(input)
3
+ <% if agent_class_name -%>
4
+ <%= agent_class_name %>.new.ask(input)
5
+ <% else -%>
6
+ RubyLLM.chat.ask(input)
7
+ <% end -%>
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ cases:
2
+ - name: first_case
3
+ inputs: ""
4
+ expected_output: ""
@@ -1,15 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ripper'
4
+
3
5
  module RubyLLM
4
6
  module Generators
5
7
  # Shared helpers for RubyLLM generators
6
8
  module GeneratorHelpers
7
9
  APPLICATION_MODEL_TYPES = %w[chat message].freeze
10
+ APPLICATION_AGENT_PATH = 'app/agents/application_agent.rb'
8
11
  DEFAULT_MODEL_NAMES = {
9
12
  chat: 'Chat',
10
13
  message: 'Message'
11
14
  }.freeze
12
15
 
16
+ def create_application_agent
17
+ return if application_agent?
18
+
19
+ create_file APPLICATION_AGENT_PATH, <<~RUBY
20
+ class ApplicationAgent < RubyLLM::Agent
21
+ chat_class #{chat_model_name}
22
+ end
23
+ RUBY
24
+ end
25
+
26
+ def application_agent?
27
+ File.exist?(File.join(destination_root, APPLICATION_AGENT_PATH))
28
+ end
29
+
30
+ # Removes the broadcasting code and attachment declaration earlier
31
+ # versions wrote into the message model, which acts_as_message now
32
+ # provides, and explains what to do with broadcasting code RubyLLM
33
+ # did not write.
34
+ def remove_generated_message_code
35
+ return unless behavior == :invoke
36
+
37
+ path = "app/models/#{message_model_name.underscore}.rb"
38
+ model = File.join(destination_root, path)
39
+ return unless File.exist?(model)
40
+
41
+ source = File.read(model)
42
+ (generated_broadcasting_blocks + [" has_many_attached :attachments\n"]).each do |code|
43
+ gsub_file(path, code, '') if source.include?(code)
44
+ end
45
+ explain_own_broadcasting(File.read(model))
46
+ generated_broadcasting_blocks.any? { |block| source.include?(block) }
47
+ end
48
+
49
+ # Turns on Configuration#broadcast_messages in the RubyLLM initializer.
50
+ def enable_message_broadcasting
51
+ configure_initializer(:broadcast_messages, 'true')
52
+ end
53
+
54
+ # Sets +setting+ to +value+ at the end of the initializer's
55
+ # RubyLLM.configure block, unless the initializer already sets it.
56
+ def configure_initializer(setting, value)
57
+ path = 'config/initializers/ruby_llm.rb'
58
+ initializer = File.join(destination_root, path)
59
+ source = File.exist?(initializer) ? File.read(initializer) : ''
60
+ block = configure_block(source)
61
+ return say("Add config.#{setting} = #{value} to your RubyLLM configuration", :yellow) unless block
62
+
63
+ indent, name = block.captures
64
+ return if block[0].match?(/^\s*#{name}\.#{setting}\s*=/)
65
+ return if source.match?(/^\s*RubyLLM\.config\.#{setting}\s*=/)
66
+
67
+ inject_into_file path, "#{indent} #{name}.#{setting} = #{value}\n", after: block[0]
68
+ end
69
+
70
+ # The RubyLLM.configure block up to its closing +end+, or +nil+ unless
71
+ # Ruby reads both lines as code and that +end+ as the one closing the
72
+ # block. Lines inside a heredoc can look like either.
73
+ def configure_block(source)
74
+ block = source.match(/^( *)RubyLLM\.configure do \|(\w+)\|\n.*?(?=^\1end\b)/m)
75
+ return unless block
76
+
77
+ tokens = Ripper.lex(source).to_h { |at, type| [at, type] }
78
+ column = block[1].length
79
+ opening = [source[0...block.begin(0)].count("\n") + 1, column]
80
+ closing = [source[0...block.end(0)].count("\n") + 1, column]
81
+ return unless tokens[opening] == :on_const && tokens[closing] == :on_kw
82
+
83
+ block if Ripper.sexp("#{block[0]}#{block[1]}end\n")
84
+ end
85
+
86
+ def explain_own_broadcasting(source)
87
+ return unless source.match?(/\b(?:broadcast_\w+_to|broadcasts_to|broadcasts_refreshes|broadcast_append_chunk)/)
88
+ return if source.include?('broadcasts: false')
89
+
90
+ say "#{message_model_name} broadcasts with Turbo itself: remove that code, or write acts_as_message " \
91
+ 'broadcasts: false', :yellow
92
+ end
93
+
94
+ # The broadcasting blocks earlier versions of ruby_llm:chat_ui wrote:
95
+ # releases 1.14 through 2.1, and the numbered-chunk block before
96
+ # acts_as_message broadcast.
97
+ def generated_broadcasting_blocks
98
+ message = message_variable_name
99
+ chat = chat_variable_name
100
+ association = chat_table_name.singularize
101
+ stream = %("#{chat}_\#{#{association}_id}")
102
+ [<<-RELEASED, <<-NUMBERED]
103
+
104
+ broadcasts_to ->(#{message}) { "#{chat}_\#{#{message}.#{association}_id}" }, inserts_by: :append
105
+
106
+ def broadcast_append_chunk(content)
107
+ broadcast_append_to #{stream},
108
+ target: "#{message}_\#{id}_content",
109
+ content: ERB::Util.html_escape(content.to_s)
110
+ end
111
+ RELEASED
112
+
113
+ after_create_commit -> { broadcast_append_to #{stream} if role.to_s == "assistant" && content.blank? }
114
+ after_update_commit -> { broadcast_replace_to #{stream} }
115
+ after_destroy_commit -> { broadcast_remove_to #{stream} }
116
+ after_commit -> { broadcast_refresh_to #{stream} }
117
+
118
+ def broadcast_append_chunk(content, sequence)
119
+ broadcast_append_to #{stream},
120
+ target: "#{message}_\#{id}_stream",
121
+ content: %(<span data-seq="\#{sequence}">\#{ERB::Util.html_escape(content)}</span>)
122
+ end
123
+ NUMBERED
124
+ end
125
+
13
126
  def parse_model_mappings(allowed_types: APPLICATION_MODEL_TYPES, defaults: DEFAULT_MODEL_NAMES)
14
127
  @model_names = defaults.dup
15
128
  @explicit_model_mappings = []
@@ -104,20 +217,20 @@ module RubyLLM
104
217
 
105
218
  [chat_model_name, message_model_name].each do |model_name|
106
219
  if model_name.include?('::')
107
- namespace = model_name.split('::').first
220
+ namespace = model_name.deconstantize
108
221
  namespaces << namespace unless namespaces.include?(namespace)
109
222
  end
110
223
  end
111
224
 
112
225
  namespaces.each do |namespace|
113
226
  module_path = "app/models/#{namespace.underscore}.rb"
114
- next if File.exist?(Rails.root.join(module_path))
227
+ next if File.exist?(File.join(destination_root, module_path))
115
228
 
116
229
  create_file module_path do
117
230
  <<~RUBY
118
231
  module #{namespace}
119
232
  def self.table_name_prefix
120
- "#{namespace.underscore}_"
233
+ "#{namespace.underscore.tr('/', '_')}_"
121
234
  end
122
235
  end
123
236
  RUBY
@@ -17,10 +17,7 @@ module RubyLLM
17
17
 
18
18
  argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
19
19
 
20
- class_option :skip_active_storage, type: :boolean, default: false,
21
- desc: 'Skip ActiveStorage installation and attachment setup'
22
-
23
- desc 'Creates models and migrations for RubyLLM Rails integration\n' \
20
+ desc "Creates models and migrations for RubyLLM Rails integration\n" \
24
21
  'Usage: bin/rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
25
22
 
26
23
  def self.next_migration_number(dirname)
@@ -47,55 +44,16 @@ module RubyLLM
47
44
  template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
48
45
  end
49
46
 
50
- def create_convention_directories
51
- %w[agents tools schemas prompts].each do |name|
52
- empty_directory "app/#{name}"
53
- create_file "app/#{name}/.gitkeep" unless File.exist?(Rails.root.join("app/#{name}/.gitkeep"))
54
- end
47
+ def create_application_agent_file
48
+ create_application_agent
55
49
  end
56
50
 
57
51
  def install_active_storage
58
- return if options[:skip_active_storage]
59
-
60
- say ' Installing ActiveStorage for file attachments...', :cyan
61
52
  rails_command 'active_storage:install'
62
53
  end
63
54
 
64
- def show_install_info
65
- say "\n ✅ RubyLLM installed!", :green
66
-
67
- say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
68
-
69
- say "\n Next steps:", :yellow
70
- say ' 1. Run: bin/rails db:migrate'
71
- say ' 2. Run: bin/rails ruby_llm:load_models'
72
- say ' 3. Set your API keys in config/initializers/ruby_llm.rb'
73
-
74
- say " 4. Start chatting: #{chat_model_name}.create!(model: 'gpt-5.6-luna').ask('Hello!')"
75
- say " 5. Optional UI: #{chat_ui_generator_command}"
76
-
77
- if options[:skip_active_storage]
78
- say "\n 📎 Note: ActiveStorage was skipped", :yellow
79
- say ' File attachments won\'t work without ActiveStorage.'
80
- say ' To enable later:'
81
- say ' 1. Run: bin/rails active_storage:install && bin/rails db:migrate'
82
- say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
83
- end
84
-
85
- say "\n 📚 Documentation: https://rubyllm.com", :cyan
86
-
87
- say "\n ❤️ Love RubyLLM?", :magenta
88
- say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
89
- say ' • 🐦 Follow for updates: https://x.com/paolino'
90
- say "\n"
91
- end
92
-
93
- private
94
-
95
- def chat_ui_generator_command
96
- mappings = model_mappings.join(' ')
97
- mappings = " #{mappings}" unless mappings.empty?
98
- "bin/rails generate ruby_llm:chat_ui#{mappings}"
55
+ def show_next_step
56
+ say "\nRun bin/rails db:migrate" if behavior == :invoke
99
57
  end
100
58
  end
101
59
  end
@@ -3,7 +3,11 @@ class <%= create_migration_class_name(chat_table_name) %> < ActiveRecord::Migrat
3
3
  create_table :<%= chat_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :ruby_llm_model, null: false, foreign_key: { to_table: :ruby_llm_models }, type: :<%= reference_type %>
5
5
  t.boolean :cancelled, null: false, default: false
6
+ t.string :claim_token
7
+ t.datetime :claimed_until
6
8
  t.timestamps
9
+
10
+ t.index :claimed_until<%= ', where: "claim_token IS NOT NULL"' unless mysql? %>
7
11
  end
8
12
  end
9
13
  end
@@ -3,11 +3,11 @@ class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Mig
3
3
  create_table :<%= message_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: { to_table: :<%= chat_table_name %> }, type: :<%= reference_type %>
5
5
  t.string :role, null: false
6
- t.text :content
6
+ t.text :content<%= ', size: :long' if mysql? %>
7
7
  t.boolean :cache_until_here, null: false, default: false
8
8
  t.string :cache_ttl
9
- t.text :thinking_text
10
- t.text :thinking_signature
9
+ t.text :thinking_text<%= ', size: :long' if mysql? %>
10
+ t.text :thinking_signature<%= ', size: :long' if mysql? %>
11
11
  <% if postgresql? %>
12
12
  t.jsonb :citations
13
13
  t.jsonb :server_tool_calls
@@ -19,6 +19,8 @@ class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Mig
19
19
  t.json :raw_content
20
20
  t.json :raw_reasoning
21
21
  <% end %>
22
+ t.string :phase
23
+ t.text :continuation<%= ', size: :long' if mysql? %>
22
24
  t.string :finish_reason
23
25
  t.timestamps
24
26
  end
@@ -41,10 +41,13 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
41
41
  t.references :message, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
42
42
  t.references :result, polymorphic: true, type: :<%= reference_type %>, index: false
43
43
  t.string :tool_call_id, null: false
44
+ t.string :provider_call_id
45
+ t.string :namespace
44
46
  t.string :name, null: false
45
- t.text :thought_signature
47
+ t.text :thought_signature<%= ', size: :long' if mysql? %>
46
48
  t.string :approval
47
49
  t.boolean :remote, default: false, null: false
50
+ t.datetime :claimed_until
48
51
  <% if postgresql? %>
49
52
  t.jsonb :arguments, default: {}
50
53
  t.jsonb :mcp_state
@@ -88,6 +91,9 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
88
91
  t.integer :cache_read_tokens
89
92
  t.integer :cache_write_tokens
90
93
  t.integer :thinking_tokens
94
+ t.integer :characters
95
+ t.decimal :audio_seconds, precision: 16, scale: 6
96
+ t.integer :search_units
91
97
  <% if postgresql? %>
92
98
  t.jsonb :server_tool_use
93
99
  <% else %>
@@ -98,17 +104,51 @@ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Mig
98
104
  t.decimal :cache_read_cost, precision: 16, scale: 10
99
105
  t.decimal :cache_write_cost, precision: 16, scale: 10
100
106
  t.decimal :thinking_cost, precision: 16, scale: 10
107
+ t.decimal :characters_cost, precision: 16, scale: 10
108
+ t.decimal :audio_seconds_cost, precision: 16, scale: 10
109
+ t.decimal :search_units_cost, precision: 16, scale: 10
101
110
  t.decimal :total_cost, precision: 16, scale: 10
111
+ t.string :workflow_id
112
+ t.string :workflow_name
113
+ t.string :workflow_step_id
114
+ t.string :workflow_step_name
102
115
  t.timestamps
103
116
 
104
117
  t.index [ :chat_type, :chat_id ]
105
118
  t.index [ :message_type, :message_id ]
106
119
  t.index [ :owner_type, :owner_id ]
107
120
  t.index :status
121
+ t.index :workflow_id
108
122
  t.check_constraint "operation<%= '::text' if postgresql? %> IN (<%= usage_operations_sql %>)"
109
123
  t.check_constraint "status<%= '::text' if postgresql? %> IN (<%= usage_statuses_sql %>)"
110
124
  end
111
125
 
126
+ create_table :ruby_llm_queued_messages, id: :<%= reference_type %> do |t|
127
+ t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
128
+ t.references :sender, polymorphic: true, type: :<%= reference_type %>, index: false
129
+ t.text :content<%= ', size: :long' if mysql? %>
130
+ t.boolean :steer, default: true, null: false
131
+ t.timestamps
132
+
133
+ t.index [ :chat_type, :chat_id ]
134
+ t.index [ :sender_type, :sender_id ]
135
+ end
136
+
137
+ create_table :ruby_llm_chat_files, id: :<%= reference_type %> do |t|
138
+ t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
139
+ t.references :owner, polymorphic: true, type: :<%= reference_type %>, index: false
140
+ t.string :path, null: false
141
+ t.string :content_type
142
+ t.bigint :byte_size
143
+ t.string :digest
144
+ t.text :text<%= ', size: :long' if mysql? %>
145
+ t.string :read_digest
146
+ t.timestamps
147
+
148
+ t.index [ :chat_type, :chat_id, :path ]
149
+ t.index [ :owner_type, :owner_id ]
150
+ end
151
+
112
152
  create_table :ruby_llm_batches, id: :<%= reference_type %> do |t|
113
153
  t.string :provider_batch_id, null: false
114
154
  t.string :provider, null: false
@@ -1,4 +1,5 @@
1
1
  RubyLLM.configure do |config|
2
2
  config.openai_api_key = ENV.fetch("OPENAI_API_KEY", Rails.application.credentials.dig(:openai_api_key))
3
- # config.default_model = "gpt-5-nano"
3
+ # config.default_model = "gpt-6-luna"
4
+ config.context_budget = { per_result: 0.25, per_request: 0.7 }
4
5
  end
@@ -1,4 +1,3 @@
1
1
  class <%= message_model_name %> < ApplicationRecord
2
- <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
- has_many_attached :attachments<% end %>
2
+ <%= acts_as_message_declaration %>
4
3
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for judge classes that ask judgment models for probabilities,
8
+ # choices, and scores.
9
+ class JudgeGenerator < Rails::Generators::NamedBase
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ namespace 'ruby_llm:judge'
13
+
14
+ desc 'Creates a judge class in app/judges'
15
+
16
+ def check_judge_class_collision
17
+ class_collisions judge_class_name
18
+ end
19
+
20
+ def create_judge_file
21
+ template 'judge.rb.tt', File.join('app/judges', class_path, "#{judge_file_name}.rb")
22
+ end
23
+
24
+ private
25
+
26
+ def judge_class_name
27
+ class_name.end_with?('Judge') ? class_name : "#{class_name}Judge"
28
+ end
29
+
30
+ def judge_file_name
31
+ judge_class_name.demodulize.underscore
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,2 @@
1
+ class <%= judge_class_name %> < RubyLLM::Judge
2
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'shellwords'
5
+
6
+ module RubyLLM
7
+ module Generators
8
+ # Generator for MCP server classes.
9
+ class MCPGenerator < Rails::Generators::NamedBase
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ namespace 'ruby_llm:mcp'
13
+
14
+ def self.exit_on_failure?
15
+ true
16
+ end
17
+
18
+ argument :url, type: :string, required: false, banner: 'URL'
19
+
20
+ class_option :command, type: :string, desc: 'The command that starts a local server, for stdio'
21
+
22
+ desc "Creates an MCP server class in app/mcp\n" \
23
+ 'Usage: bin/rails g ruby_llm:mcp Linear https://mcp.linear.app/mcp ' \
24
+ 'or bin/rails g ruby_llm:mcp Files --command "npx -y @modelcontextprotocol/server-filesystem ."'
25
+
26
+ def check_server
27
+ return if url.present? ^ options[:command].present?
28
+
29
+ raise Thor::Error, "Pass the server's URL or --command, not both"
30
+ end
31
+
32
+ def check_server_class_collision
33
+ class_collisions mcp_class_name
34
+ end
35
+
36
+ def create_server_file
37
+ template 'mcp.rb.tt', File.join('app/mcp', class_path, "#{mcp_file_name}.rb")
38
+ end
39
+
40
+ private
41
+
42
+ # Follows the application's inflections, so it is LinearMcp, or
43
+ # LinearMCP where MCP is an acronym, as Zeitwerk expects either way.
44
+ def mcp_class_name
45
+ suffix = 'mcp'.camelize
46
+ class_name.end_with?(suffix) ? class_name : "#{class_name}#{suffix}"
47
+ end
48
+
49
+ def mcp_file_name
50
+ mcp_class_name.demodulize.underscore
51
+ end
52
+
53
+ def command_arguments
54
+ Shellwords.split(options[:command]).map(&:inspect).join(', ')
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,7 @@
1
+ class <%= mcp_class_name %> < RubyLLM::MCP
2
+ <% if url.present? -%>
3
+ url <%= url.inspect %>
4
+ <% else -%>
5
+ command <%= command_arguments %>
6
+ <% end -%>
7
+ end
@@ -112,10 +112,9 @@ module RubyLLM
112
112
  write_template 'core/provider_spec.rb.erb', "spec/ruby_llm/providers/#{slug}_spec.rb"
113
113
 
114
114
  update_core_registration
115
- update_core_env_example
115
+ update_core_env
116
116
  update_core_spec_configuration
117
117
  update_core_vcr_configuration
118
- update_core_models_dev_map if models_dev_provider
119
118
  end
120
119
 
121
120
  def generate_provider_gem
@@ -168,11 +167,12 @@ module RubyLLM
168
167
  )
169
168
  end
170
169
 
171
- def update_core_env_example
172
- path = file_path('.env.example')
170
+ def update_core_env
171
+ path = file_path('.env')
173
172
  return unless File.exist?(path)
174
173
 
175
- insert_sorted_line(path, "#{api_key_env}=$(op read \"op://RubyLLM/#{class_name}/credential\")", ENV_LINE)
174
+ insert_sorted_line(path, "#{api_key_env}=$(op-cache read \"op://RubyLLM/#{class_name}/credential\")",
175
+ ENV_LINE)
176
176
  insert_sorted_line(path, "#{api_base_env}=#{api_base}", ENV_LINE)
177
177
  end
178
178
 
@@ -195,13 +195,6 @@ module RubyLLM
195
195
  insert_sorted_line(path, line, /^\s+config\.filter_sensitive_data\('<[A-Z0-9_]+>'\)/)
196
196
  end
197
197
 
198
- def update_core_models_dev_map
199
- path = file_path('lib/ruby_llm/models.rb')
200
- return unless File.exist?(path)
201
-
202
- insert_sorted_entry(path, " '#{models_dev_provider}' => '#{slug}'")
203
- end
204
-
205
198
  def write_template(template, relative_path, executable: false)
206
199
  content = ERB.new(File.read(template_path(template)), trim_mode: '-').result(binding)
207
200
  write_file(relative_path, content, executable:)
@@ -25,6 +25,12 @@ module RubyLLM
25
25
  def configuration_requirements
26
26
  %i[<%= api_key_config %>]
27
27
  end
28
+ <% if models_dev_provider -%>
29
+
30
+ def models_dev_provider_ids # :nodoc:
31
+ %w[<%= models_dev_provider %>]
32
+ end
33
+ <% end -%>
28
34
  <% if dynamic_models? -%>
29
35
 
30
36
  def assume_models_exist?
@@ -22,7 +22,6 @@ RSpec.describe RubyLLM::Chat, :live do
22
22
  expect(response.raw.status).to eq(200)
23
23
  expect(response.raw.headers).not_to be_empty
24
24
  expect(response.raw.body).not_to be_empty
25
- expect(response.raw.env.request_body).not_to be_empty
26
25
  end
27
26
 
28
27
  it "#{provider}/#{model} can handle a multi-turn conversation" do
@@ -16,7 +16,6 @@ RSpec.describe RubyLLM::Chat, :live do
16
16
  expect(chunks.first).to be_a(RubyLLM::Chunk)
17
17
  expect(response.raw.status).to eq(200)
18
18
  expect(response.raw.headers).not_to be_empty
19
- expect(response.raw.env.request_body).not_to be_empty
20
19
  end
21
20
  end
22
21
  end
@@ -16,7 +16,7 @@ RSpec.describe RubyLLM::Moderation, :live do
16
16
 
17
17
  expect(moderation).to be_a(described_class)
18
18
  expect(moderation.results).to all(be_a(RubyLLM::Moderation::Result))
19
- expect(moderation.flagged?).to be_in([true, false])
19
+ expect(moderation.flagged?).to be(true).or be(false)
20
20
  end
21
21
  end
22
22
  end