ruby_llm 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1598 -332
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +67 -28
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +134 -109
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +219 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -9,17 +9,16 @@ module RubyLLM
9
9
 
10
10
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
11
11
  schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
12
- if thinking&.budget
13
- RubyLLM.logger.debug { "Ollama has no thinking budgets; ignoring budget #{thinking.budget}" }
14
- end
12
+ raise ArgumentError, 'Ollama has no thinking budget; use effort:' if thinking&.budget
13
+
15
14
  super
16
15
  end
17
16
 
18
- def format_content(content, attachments = [])
19
- Ollama::Media.format_content(content, attachments)
17
+ def render_content(content, attachments = [])
18
+ Ollama::Media.render_content(content, attachments)
20
19
  end
21
20
 
22
- def format_role(role)
21
+ def render_role(role)
23
22
  role.to_s
24
23
  end
25
24
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Ollama embeds text, an image, and audio together when the input is
7
+ # one object holding each as a field.
8
+ module Embeddings
9
+ EMBEDDING_MEDIA_FIELDS = %i[image audio].freeze
10
+
11
+ module_function
12
+
13
+ def embedding_media?
14
+ true
15
+ end
16
+
17
+ def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, with: [],
18
+ provider_options: {})
19
+ input = with.any? ? media_embedding_input(text, with) : text
20
+ super(input, model:, dimensions:, task_type:, title:, provider_options:)
21
+ end
22
+
23
+ def media_embedding_input(text, attachments)
24
+ raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
25
+
26
+ input = text.to_s.empty? ? {} : { text: text }
27
+ attachments.each do |attachment|
28
+ unless EMBEDDING_MEDIA_FIELDS.include?(attachment.type)
29
+ raise UnsupportedAttachmentError,
30
+ attachment.mime_type
31
+ end
32
+ raise ArgumentError, "Ollama embeds one #{attachment.type} per input" if input.key?(attachment.type)
33
+
34
+ input[attachment.type] = attachment.encoded
35
+ end
36
+ input
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -11,22 +11,22 @@ module RubyLLM
11
11
  %i[image audio text].include?(attachment.type)
12
12
  end
13
13
 
14
- def format_content(content, attachments = [])
15
- Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
14
+ def render_content(content, attachments = [])
15
+ Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
16
16
  raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
17
17
 
18
18
  case attachment.type
19
19
  when :image
20
- format_image(attachment)
20
+ render_image(attachment)
21
21
  when :audio
22
- Protocols::ChatCompletions::Media.format_audio(attachment)
22
+ Protocols::ChatCompletions::Media.render_audio(attachment)
23
23
  when :text
24
- Protocols::ChatCompletions::Media.format_text_file(attachment)
24
+ Protocols::ChatCompletions::Media.render_text_file(attachment)
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
- def format_image(image)
29
+ def render_image(image)
30
30
  {
31
31
  type: 'image_url',
32
32
  image_url: {
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Ollama's dialect of speech generation. Its voices come from each model,
7
+ # so a call with no voice leaves the choice to the model, and it answers
8
+ # with WAV unless asked for PCM.
9
+ module Speech
10
+ DEFAULT_VOICE = 'default'
11
+ DEFAULT_FORMAT = 'wav'
12
+
13
+ def render_speech_payload(input, model:, voice:, format:, provider_options: {})
14
+ { model:, input:, voice:, response_format: format }.compact.merge(provider_options)
15
+ end
16
+
17
+ def parse_speech_response(response, model:, voice:, format:)
18
+ RubyLLM::Speech.new(data: response.body, model:, voice: voice || DEFAULT_VOICE,
19
+ format: (format || DEFAULT_FORMAT).to_s, characters: @speech_characters)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,8 +7,10 @@ module RubyLLM
7
7
  # Ollama's dialect of the Chat Completions API.
8
8
  class ChatCompletions < Protocols::ChatCompletions
9
9
  include Ollama::Chat
10
+ include Ollama::Embeddings
10
11
  include Ollama::Media
11
12
  include Ollama::Models
13
+ include Ollama::Speech
12
14
  end
13
15
 
14
16
  # Ollama's dialect of the System One API for decision models such as Clef.
@@ -16,11 +18,21 @@ module RubyLLM
16
18
  include Ollama::Judgments
17
19
  end
18
20
 
21
+ # Ollama's stateless Responses API, which serves manual compaction.
22
+ class Responses < Protocols::Responses
23
+ include Protocols::Responses::Compaction
24
+
25
+ def render_image(image, image_url: image.for_llm, **)
26
+ super
27
+ end
28
+ end
29
+
19
30
  protocol :chat_completions, ChatCompletions
20
31
  protocol :system_one, SystemOne
32
+ protocol :responses, Responses
21
33
 
22
34
  def api_base
23
- @config.ollama_api_base
35
+ configured_api_base(:ollama_api_base, example: 'http://localhost:11434/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
24
36
  end
25
37
 
26
38
  def resolve_protocol(name, model, operation: nil, **options)
@@ -7,12 +7,25 @@ module RubyLLM
7
7
  # Ollama Cloud's dialect of the Chat Completions API.
8
8
  class ChatCompletions < Ollama::ChatCompletions
9
9
  include OllamaCloud::Models
10
+
11
+ abstract :embedding_url, :embedding_media?, :speech_url, :build_audio_file_part, :render_image_payload,
12
+ :render_moderation_payload
13
+ end
14
+
15
+ # Ollama Cloud's dialect of the System One API for decision models such as Clef.
16
+ class SystemOne < Ollama::SystemOne
10
17
  end
11
18
 
12
19
  protocol :chat_completions, ChatCompletions
20
+ protocol :system_one, SystemOne
13
21
 
14
22
  def api_base
15
- @config.ollama_cloud_api_base || 'https://ollama.com/v1'
23
+ configured_api_base(:ollama_cloud_api_base, 'https://ollama.com/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
24
+ end
25
+
26
+ def resolve_protocol(name, model, operation: nil, **options)
27
+ name ||= :system_one if operation == :judge
28
+ super
16
29
  end
17
30
 
18
31
  def headers
@@ -20,6 +33,14 @@ module RubyLLM
20
33
  end
21
34
 
22
35
  class << self
36
+ def open_telemetry_name # :nodoc:
37
+ 'ollama'
38
+ end
39
+
40
+ def models_dev_provider_ids # :nodoc:
41
+ %w[ollama-cloud]
42
+ end
43
+
23
44
  def configuration_options
24
45
  %i[ollama_cloud_api_key ollama_cloud_api_base]
25
46
  end
@@ -7,6 +7,14 @@ module RubyLLM
7
7
  class Responses < Protocols::Responses
8
8
  include Protocols::Responses::TokenCounting
9
9
  include Protocols::Responses::Compaction
10
+
11
+ def document_urls?
12
+ true
13
+ end
14
+
15
+ def web_search_sources?
16
+ true
17
+ end
10
18
  end
11
19
  end
12
20
  end
@@ -19,7 +19,7 @@ module RubyLLM
19
19
  end
20
20
 
21
21
  def api_base
22
- @config.openai_api_base || 'https://api.openai.com/v1'
22
+ configured_api_base(:openai_api_base, 'https://api.openai.com/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
23
23
  end
24
24
 
25
25
  def resolve_protocol(name, model, operation: nil, **options)
@@ -29,9 +29,9 @@ module RubyLLM
29
29
 
30
30
  # Audio, realtime, and dedicated search models only exist on Chat Completions.
31
31
  def protocol_for(model, **)
32
- return protocols[:chat_completions] if Capabilities::SEARCH_MODELS.include?(model.id)
32
+ return fetch_protocol(:chat_completions) if Capabilities::SEARCH_MODELS.include?(model.id)
33
33
 
34
- model.id.match?(/audio|realtime/) ? protocols[:chat_completions] : super
34
+ model.id.match?(/audio|realtime/) ? fetch_protocol(:chat_completions) : super
35
35
  end
36
36
 
37
37
  def headers
@@ -69,6 +69,10 @@ module RubyLLM
69
69
  def batch_cost_multiplier(**) = 0.5
70
70
 
71
71
  class << self
72
+ def models_dev_provider_ids # :nodoc:
73
+ %w[openai]
74
+ end
75
+
72
76
  def capabilities
73
77
  OpenAI::Capabilities
74
78
  end
@@ -38,8 +38,8 @@ module RubyLLM
38
38
  OpenRouter::Media.supported_attachment?(attachment)
39
39
  end
40
40
 
41
- def format_content(content, attachments = [])
42
- OpenRouter::Media.format_content(content, attachments)
41
+ def render_content(content, attachments = [])
42
+ OpenRouter::Media.render_content(content, attachments)
43
43
  end
44
44
 
45
45
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil,
@@ -85,7 +85,7 @@ module RubyLLM
85
85
  reasoning[:enabled] = thinking.enabled
86
86
  end
87
87
 
88
- def format_thinking(msg)
88
+ def render_thinking(msg)
89
89
  return {} unless msg.role == :assistant
90
90
  return { reasoning_details: msg.raw_reasoning } if msg.raw_reasoning.is_a?(Array)
91
91
 
@@ -103,7 +103,7 @@ module RubyLLM
103
103
  end
104
104
  end
105
105
 
106
- def format_message_content(msg, caching: nil)
106
+ def render_message_content(msg, caching: nil)
107
107
  content = super
108
108
  return content unless caching != false && msg.cache_until_here?
109
109
 
@@ -127,7 +127,7 @@ module RubyLLM
127
127
  ttl ||= options[:ttl]
128
128
 
129
129
  { type: CACHE_CONTROL_TYPE }.tap do |control|
130
- control[:ttl] = ttl if ttl
130
+ control[:ttl] = render_cache_lifetime(ttl) if ttl
131
131
  end
132
132
  end
133
133
 
@@ -142,6 +142,8 @@ module RubyLLM
142
142
  "OpenRouter prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
143
143
  end
144
144
 
145
+ def cache_option_names = PROMPT_CACHE_OPTIONS
146
+
145
147
  def format_cache_option_keys(keys)
146
148
  keys.map { |key| ":#{key}" }.join(', ')
147
149
  end
@@ -158,10 +160,14 @@ module RubyLLM
158
160
  cost
159
161
  end
160
162
 
161
- def supports_provider_file_references?
163
+ def provider_file_references?
162
164
  true
163
165
  end
164
166
 
167
+ def provider_file_upload_options(_attachment)
168
+ {}
169
+ end
170
+
165
171
  def default_large_file_upload_threshold
166
172
  OPENROUTER_INLINE_FILE_THRESHOLD
167
173
  end
@@ -170,7 +176,7 @@ module RubyLLM
170
176
  attachment.pdf?
171
177
  end
172
178
 
173
- def extract_thinking_text(message_data)
179
+ def parse_thinking_text(message_data)
174
180
  candidate = message_data['reasoning']
175
181
  return candidate if candidate.is_a?(String)
176
182
 
@@ -189,7 +195,7 @@ module RubyLLM
189
195
  text.empty? ? nil : text
190
196
  end
191
197
 
192
- def extract_thinking_signature(message_data)
198
+ def parse_thinking_signature(message_data)
193
199
  details = message_data['reasoning_details']
194
200
  return nil unless details.is_a?(Array)
195
201
 
@@ -204,7 +210,7 @@ module RubyLLM
204
210
 
205
211
  # OpenRouter requires the reasoning_details array back untouched for
206
212
  # signed reasoning to survive multi-turn tool calls.
207
- def extract_raw_reasoning(message_data)
213
+ def parse_raw_reasoning(message_data)
208
214
  details = message_data['reasoning_details']
209
215
  details if details.is_a?(Array) && !details.empty?
210
216
  end
@@ -7,9 +7,13 @@ module RubyLLM
7
7
  module Embeddings
8
8
  EMBEDDING_MEDIA_TYPES = { audio: :input_audio, video: :input_video, pdf: :input_file }.freeze
9
9
 
10
+ def rejected_options(operation)
11
+ operation == :embed ? %i[title] : super
12
+ end
13
+
10
14
  module_function
11
15
 
12
- def supports_embedding_media?
16
+ def embedding_media?
13
17
  true
14
18
  end
15
19
 
@@ -18,7 +22,7 @@ module RubyLLM
18
22
  input = if with.any?
19
23
  raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
20
24
 
21
- [{ content: format_embedding_content(text, with) }]
25
+ [{ content: render_embedding_content(text, with) }]
22
26
  else
23
27
  text
24
28
  end
@@ -28,21 +32,21 @@ module RubyLLM
28
32
  payload.merge(provider_options)
29
33
  end
30
34
 
31
- def format_embedding_content(text, attachments)
32
- Protocols::ChatCompletions::Media.format_parts(text, attachments) do |attachment|
33
- format_embedding_attachment(attachment)
35
+ def render_embedding_content(text, attachments)
36
+ Protocols::ChatCompletions::Media.render_parts(text, attachments) do |attachment|
37
+ render_embedding_attachment(attachment)
34
38
  end
35
39
  end
36
40
 
37
- def format_embedding_attachment(attachment)
41
+ def render_embedding_attachment(attachment)
38
42
  raise UnsupportedAttachmentError, attachment.mime_type if attachment.provider_file?
39
43
 
40
44
  if (type = EMBEDDING_MEDIA_TYPES[attachment.type])
41
45
  { type: type.to_s, type => { data: attachment.for_llm, format: attachment.format } }
42
46
  elsif attachment.type == :image
43
- Protocols::ChatCompletions::Media.format_image(attachment)
47
+ Protocols::ChatCompletions::Media.render_image(attachment)
44
48
  else
45
- Protocols::ChatCompletions::Media.format_attachment(
49
+ Protocols::ChatCompletions::Media.render_attachment(
46
50
  attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
47
51
  )
48
52
  end
@@ -7,34 +7,40 @@ module RubyLLM
7
7
  # OpenRouter has a unified images endpoint that generates and edits
8
8
  # images across providers and reports the exact cost of each call.
9
9
  module Images
10
+ ASPECT_RATIO = /\A(?:\d+:\d+|auto)\z/
11
+
12
+ def rejected_options(operation)
13
+ operation == :paint ? %i[count] : super
14
+ end
15
+
10
16
  module_function
11
17
 
12
- def images_url(with: nil, mask: nil) # rubocop:disable Lint/UnusedMethodArgument
18
+ def images_url(**)
13
19
  'images'
14
20
  end
15
21
 
16
- def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
17
- RubyLLM.logger.debug { "Ignoring size #{size}. Use aspect_ratio/resolution provider options instead." }
18
- if count && count > 1
19
- RubyLLM.logger.debug do
20
- "Ignoring count #{count}. OpenRouter generates one image per request."
21
- end
22
- end
22
+ def render_image_payload(prompt, model:, size:, with: nil, provider_options: {}, **)
23
23
  payload = { model: model, prompt: prompt }
24
24
  references = build_input_references(with)
25
25
  payload[:input_references] = references if references.any?
26
+ payload.merge!(render_image_size(size)) if size
26
27
  payload.merge(provider_options)
27
28
  end
28
29
 
30
+ def render_image_size(size)
31
+ value = size.to_s.strip
32
+ value.match?(ASPECT_RATIO) ? { aspect_ratio: value } : { size: value }
33
+ end
34
+
29
35
  def build_input_references(with)
30
36
  Attachment.wrap(with, config: @config).map do |attachment|
31
37
  raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
32
38
 
33
- Protocols::ChatCompletions::Media.format_image(attachment)
39
+ Protocols::ChatCompletions::Media.render_image(attachment)
34
40
  end
35
41
  end
36
42
 
37
- def validate_paint_inputs!(with:, mask:) # rubocop:disable Lint/UnusedMethodArgument
43
+ def validate_paint_inputs!(mask:, **)
38
44
  raise UnsupportedAttachmentError, 'image mask' unless mask.nil?
39
45
  end
40
46
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenRouter
6
+ # OpenRouter's dialect of the System One API: the Decisions route, media
7
+ # parts inside the state, and the exact cost of each judgment.
8
+ module Judgments
9
+ module_function
10
+
11
+ def judgment_url
12
+ '../alpha/decisions'
13
+ end
14
+
15
+ def render_judgment_payload(input, questions:, model:, with: [], provider_options: {})
16
+ parts = with.map { |attachment| render_judgment_part(attachment) }
17
+ super(render_judgment_state(input, parts), questions:, model:, provider_options:)
18
+ end
19
+
20
+ def render_judgment_state(input, parts)
21
+ return input if parts.empty?
22
+ return parts if input.nil?
23
+
24
+ input.is_a?(Array) ? input + parts : [input, *parts]
25
+ end
26
+
27
+ def render_judgment_part(attachment)
28
+ raise UnsupportedAttachmentError, attachment.mime_type if attachment.provider_file?
29
+
30
+ case attachment.type
31
+ when :image
32
+ Protocols::ChatCompletions::Media.with_image_detail(
33
+ { type: 'image_url', image_url: { url: attachment.for_llm } }, attachment
34
+ )
35
+ when :audio then Protocols::ChatCompletions::Media.render_audio(attachment)
36
+ when :video then { type: 'video_url', video_url: { url: attachment.for_llm } }
37
+ else raise UnsupportedAttachmentError, attachment.mime_type
38
+ end
39
+ end
40
+
41
+ def reported_cost(usage)
42
+ usage['cost']
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -12,19 +12,19 @@ module RubyLLM
12
12
  attachment.video? || Protocols::ChatCompletions::Media.supported_attachment?(attachment)
13
13
  end
14
14
 
15
- def format_content(content, attachments = [])
16
- Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
15
+ def render_content(content, attachments = [])
16
+ Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
17
17
  if attachment.type == :video
18
- format_video(attachment)
18
+ render_video(attachment)
19
19
  else
20
- Protocols::ChatCompletions::Media.format_attachment(
20
+ Protocols::ChatCompletions::Media.render_attachment(
21
21
  attachment, document_attachments: :pdf, image_attachments: true, audio_attachments: true
22
22
  )
23
23
  end
24
24
  end
25
25
  end
26
26
 
27
- def format_video(video)
27
+ def render_video(video)
28
28
  {
29
29
  type: 'video_url',
30
30
  video_url: {
@@ -4,8 +4,8 @@ module RubyLLM
4
4
  module Providers
5
5
  class OpenRouter
6
6
  # Models methods of the OpenRouter API integration. Chat models come
7
- # from the main catalog; embedding, speech, transcription, and image
8
- # models live in separate catalogs that are merged in.
7
+ # from the main catalog; embedding, speech, transcription, image, and
8
+ # decision models live in separate catalogs that are merged in.
9
9
  module Models
10
10
  CATALOG_URLS = [
11
11
  'models',
@@ -13,18 +13,26 @@ module RubyLLM
13
13
  'models?output_modalities=speech',
14
14
  'models?output_modalities=transcription',
15
15
  'models?output_modalities=rerank',
16
+ 'models?output_modalities=decisions',
16
17
  'images/models'
17
18
  ].freeze
18
19
 
19
20
  OUTPUT_MODALITY_MAP = {
20
21
  'speech' => 'audio',
21
- 'transcription' => 'text'
22
+ 'transcription' => 'text',
23
+ 'decisions' => 'judgment'
22
24
  }.freeze
23
25
 
24
26
  CAPABILITY_BY_OUTPUT_MODALITY = {
25
27
  'speech' => 'speech_generation',
26
28
  'transcription' => 'transcription',
27
- 'image' => 'image_generation'
29
+ 'image' => 'image_generation',
30
+ 'decisions' => 'judgment'
31
+ }.freeze
32
+
33
+ CAPABILITIES_BY_INPUT_MODALITY = {
34
+ 'image' => %w[vision],
35
+ 'video' => %w[vision video]
28
36
  }.freeze
29
37
 
30
38
  CAPABILITY_PARAMETERS = {
@@ -71,7 +79,9 @@ module RubyLLM
71
79
  end
72
80
 
73
81
  capabilities = supported_parameters_to_capabilities(model_data['supported_parameters']) |
82
+ input_modality_capabilities(modalities[:input]) |
74
83
  output_modalities.filter_map { |modality| CAPABILITY_BY_OUTPUT_MODALITY[modality] }
84
+ capabilities -= ['streaming'] if output_modalities.include?('decisions')
75
85
 
76
86
  Model.new(
77
87
  id: model_data['id'],
@@ -97,14 +107,16 @@ module RubyLLM
97
107
  end
98
108
  end
99
109
 
110
+ def input_modality_capabilities(input_modalities)
111
+ input_modalities.flat_map { |modality| CAPABILITIES_BY_INPUT_MODALITY.fetch(modality, []) }.uniq
112
+ end
113
+
100
114
  def supported_parameters_to_capabilities(params)
101
115
  return [] unless params
102
116
 
103
- capabilities = ['streaming'] + CAPABILITY_PARAMETERS.filter_map do |capability, parameters|
117
+ ['streaming'] + CAPABILITY_PARAMETERS.filter_map do |capability, parameters|
104
118
  capability if parameters.any? { |parameter| params.include?(parameter) }
105
119
  end
106
- capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
107
- capabilities
108
120
  end
109
121
  end
110
122
  end
@@ -16,11 +16,11 @@ module RubyLLM
16
16
  Chunk.new(
17
17
  role: :assistant,
18
18
  model: data['model'],
19
- content: delta['content'],
20
- citations: extract_chunk_citations(delta, data),
19
+ content: delta['content'] || delta['refusal'],
20
+ citations: parse_chunk_citations(delta, data),
21
21
  thinking: Thinking.build(
22
- text: extract_thinking_text(delta),
23
- signature: extract_thinking_signature(delta)
22
+ text: parse_thinking_text(delta),
23
+ signature: parse_thinking_signature(delta)
24
24
  ),
25
25
  raw_reasoning: accumulate_raw_reasoning(delta['reasoning_details']),
26
26
  tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false, stream_keys: true),
@@ -36,7 +36,7 @@ module RubyLLM
36
36
  "videos/#{job.id}"
37
37
  end
38
38
 
39
- def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
39
+ def parse_video_job_status(response, **)
40
40
  body = response.body
41
41
  status = job_status(body)
42
42
  state = { status: status, raw: body, reported_cost: reported_cost(body['usage'] || {}) }