ruby_llm 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +160 -120
  3. data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
  4. data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
  5. data/app/assets/javascripts/ruby_llm/marked.js +71 -0
  6. data/config/importmap.rb +5 -0
  7. data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
  8. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
  9. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
  10. data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
  13. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
  14. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
  18. data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
  31. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
  33. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
  35. data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
  37. data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
  49. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  50. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
  51. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
  52. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
  53. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
  54. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
  55. data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
  56. data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
  57. data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
  58. data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
  59. data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
  60. data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
  61. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
  62. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
  63. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
  64. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
  65. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
  66. data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
  67. data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
  68. data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
  69. data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
  70. data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
  71. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
  73. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
  74. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
  75. data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
  76. data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
  77. data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
  78. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
  79. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
  80. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
  81. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
  82. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
  83. data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
  84. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
  85. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
  86. data/lib/ruby_llm/accounting/operation.rb +73 -0
  87. data/lib/ruby_llm/accounting/usage.rb +74 -24
  88. data/lib/ruby_llm/active_record/acts_as.rb +78 -11
  89. data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
  90. data/lib/ruby_llm/active_record/batch.rb +67 -9
  91. data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
  92. data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
  93. data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
  94. data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
  95. data/lib/ruby_llm/active_record/chat_file.rb +37 -0
  96. data/lib/ruby_llm/active_record/chat_files.rb +30 -0
  97. data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
  98. data/lib/ruby_llm/active_record/chat_methods.rb +515 -274
  99. data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
  100. data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
  101. data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
  102. data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
  103. data/lib/ruby_llm/active_record/chat_run.rb +51 -0
  104. data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
  105. data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
  106. data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
  107. data/lib/ruby_llm/active_record/file_store.rb +269 -0
  108. data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
  109. data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
  110. data/lib/ruby_llm/active_record/message_methods.rb +83 -11
  111. data/lib/ruby_llm/active_record/model.rb +37 -9
  112. data/lib/ruby_llm/active_record/provider_file.rb +21 -4
  113. data/lib/ruby_llm/active_record/queued_message.rb +50 -0
  114. data/lib/ruby_llm/active_record/record.rb +3 -1
  115. data/lib/ruby_llm/active_record/run_claims.rb +608 -0
  116. data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
  117. data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
  118. data/lib/ruby_llm/active_record/tool_call.rb +55 -4
  119. data/lib/ruby_llm/active_record/usage.rb +79 -9
  120. data/lib/ruby_llm/agent/callbacks.rb +98 -0
  121. data/lib/ruby_llm/agent/context_budget.rb +26 -0
  122. data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
  123. data/lib/ruby_llm/agent/model_options.rb +56 -0
  124. data/lib/ruby_llm/agent/ownership.rb +22 -0
  125. data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
  126. data/lib/ruby_llm/agent.rb +620 -236
  127. data/lib/ruby_llm/agent_tool.rb +91 -0
  128. data/lib/ruby_llm/aliases.json +84 -91
  129. data/lib/ruby_llm/attachment.rb +101 -20
  130. data/lib/ruby_llm/batch.rb +132 -46
  131. data/lib/ruby_llm/cached_content.rb +4 -4
  132. data/lib/ruby_llm/chat/agent_run.rb +22 -0
  133. data/lib/ruby_llm/chat/context_budget.rb +119 -0
  134. data/lib/ruby_llm/chat/file_references.rb +259 -0
  135. data/lib/ruby_llm/chat/message_queue.rb +56 -0
  136. data/lib/ruby_llm/chat/ownership.rb +18 -0
  137. data/lib/ruby_llm/chat/rescuable.rb +48 -0
  138. data/lib/ruby_llm/chat/run.rb +32 -0
  139. data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
  140. data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
  141. data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
  142. data/lib/ruby_llm/chat/tool_search.rb +36 -1
  143. data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
  144. data/lib/ruby_llm/chat.rb +1593 -331
  145. data/lib/ruby_llm/chunk.rb +27 -1
  146. data/lib/ruby_llm/citation.rb +2 -1
  147. data/lib/ruby_llm/code_execution/language.rb +28 -0
  148. data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
  149. data/lib/ruby_llm/code_execution/packages.rb +241 -0
  150. data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
  151. data/lib/ruby_llm/code_execution/workspace.rb +314 -0
  152. data/lib/ruby_llm/code_execution.rb +781 -0
  153. data/lib/ruby_llm/configuration.rb +151 -4
  154. data/lib/ruby_llm/content_filter.rb +68 -0
  155. data/lib/ruby_llm/cost.rb +68 -41
  156. data/lib/ruby_llm/downloaded_file.rb +10 -9
  157. data/lib/ruby_llm/embedding.rb +30 -45
  158. data/lib/ruby_llm/embedding_request.rb +1 -1
  159. data/lib/ruby_llm/error.rb +167 -14
  160. data/lib/ruby_llm/evaluation/dataset.rb +5 -4
  161. data/lib/ruby_llm/evaluation/evidence.rb +5 -3
  162. data/lib/ruby_llm/evaluation/report.rb +7 -3
  163. data/lib/ruby_llm/evaluation/result.rb +1 -1
  164. data/lib/ruby_llm/evaluation.rb +2 -1
  165. data/lib/ruby_llm/fallback.rb +32 -4
  166. data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
  167. data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
  168. data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
  169. data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
  170. data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
  171. data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
  172. data/lib/ruby_llm/file_store/converters/office.rb +106 -0
  173. data/lib/ruby_llm/file_store/converters/text.rb +27 -0
  174. data/lib/ruby_llm/file_store/converters.rb +84 -0
  175. data/lib/ruby_llm/file_store/directory.rb +190 -0
  176. data/lib/ruby_llm/file_store/memory.rb +75 -0
  177. data/lib/ruby_llm/file_store/tools/base.rb +99 -0
  178. data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
  179. data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
  180. data/lib/ruby_llm/file_store/tools/list.rb +57 -0
  181. data/lib/ruby_llm/file_store/tools/read.rb +120 -0
  182. data/lib/ruby_llm/file_store/tools/search.rb +208 -0
  183. data/lib/ruby_llm/file_store/tools/write.rb +40 -0
  184. data/lib/ruby_llm/file_store/tools.rb +39 -0
  185. data/lib/ruby_llm/file_store.rb +338 -0
  186. data/lib/ruby_llm/files/saving.rb +14 -0
  187. data/lib/ruby_llm/image.rb +67 -45
  188. data/lib/ruby_llm/input_required_error.rb +28 -0
  189. data/lib/ruby_llm/judge/builder.rb +6 -1
  190. data/lib/ruby_llm/judge.rb +1 -1
  191. data/lib/ruby_llm/judgment.rb +6 -24
  192. data/lib/ruby_llm/mcp/client.rb +50 -24
  193. data/lib/ruby_llm/mcp/collection.rb +39 -2
  194. data/lib/ruby_llm/mcp/content.rb +7 -2
  195. data/lib/ruby_llm/mcp/error.rb +8 -2
  196. data/lib/ruby_llm/mcp/http.rb +117 -37
  197. data/lib/ruby_llm/mcp/input_request.rb +3 -112
  198. data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
  199. data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
  200. data/lib/ruby_llm/mcp/listener.rb +7 -1
  201. data/lib/ruby_llm/mcp/oauth.rb +102 -39
  202. data/lib/ruby_llm/mcp/outages.rb +172 -0
  203. data/lib/ruby_llm/mcp/resource.rb +9 -6
  204. data/lib/ruby_llm/mcp/resource_template.rb +3 -2
  205. data/lib/ruby_llm/mcp/session_expired.rb +10 -2
  206. data/lib/ruby_llm/mcp/stdio.rb +66 -18
  207. data/lib/ruby_llm/mcp/task.rb +13 -40
  208. data/lib/ruby_llm/mcp/tool.rb +34 -11
  209. data/lib/ruby_llm/mcp.rb +123 -59
  210. data/lib/ruby_llm/message.rb +217 -23
  211. data/lib/ruby_llm/model/modalities.rb +1 -1
  212. data/lib/ruby_llm/model/pricing.rb +26 -4
  213. data/lib/ruby_llm/model/pricing_category.rb +38 -15
  214. data/lib/ruby_llm/model/pricing_tier.rb +3 -2
  215. data/lib/ruby_llm/model.rb +24 -6
  216. data/lib/ruby_llm/models/lookup.rb +37 -4
  217. data/lib/ruby_llm/models/registry.rb +4 -19
  218. data/lib/ruby_llm/models.json +36186 -27832
  219. data/lib/ruby_llm/models.rb +44 -39
  220. data/lib/ruby_llm/moderation.rb +10 -30
  221. data/lib/ruby_llm/ocr.rb +5 -22
  222. data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
  223. data/lib/ruby_llm/open_telemetry/content.rb +105 -0
  224. data/lib/ruby_llm/open_telemetry.rb +18 -4
  225. data/lib/ruby_llm/prompt/context.rb +2 -2
  226. data/lib/ruby_llm/prompt.rb +7 -5
  227. data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
  228. data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
  229. data/lib/ruby_llm/protocol/native_content.rb +69 -0
  230. data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
  231. data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
  232. data/lib/ruby_llm/protocol/streaming.rb +105 -22
  233. data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
  234. data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
  235. data/lib/ruby_llm/protocol.rb +127 -106
  236. data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
  237. data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
  238. data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
  239. data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
  240. data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
  241. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
  242. data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
  243. data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
  244. data/lib/ruby_llm/protocols/anthropic.rb +61 -6
  245. data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
  246. data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
  247. data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
  248. data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
  249. data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
  250. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
  251. data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
  252. data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
  253. data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
  254. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +15 -15
  255. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
  256. data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
  257. data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
  258. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
  259. data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
  260. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
  261. data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
  262. data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
  263. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
  264. data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
  265. data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
  266. data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
  267. data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
  268. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
  269. data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
  270. data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
  271. data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
  272. data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
  273. data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
  274. data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
  275. data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
  276. data/lib/ruby_llm/protocols/converse/media.rb +24 -22
  277. data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
  278. data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
  279. data/lib/ruby_llm/protocols/converse.rb +17 -1
  280. data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
  281. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
  282. data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
  283. data/lib/ruby_llm/protocols/deepgram.rb +3 -2
  284. data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
  285. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
  286. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
  287. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
  288. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
  289. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
  290. data/lib/ruby_llm/protocols/files.rb +27 -12
  291. data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
  292. data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
  293. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
  294. data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
  295. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
  296. data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
  297. data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
  298. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
  299. data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
  300. data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
  301. data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
  302. data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
  303. data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
  304. data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
  305. data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
  306. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
  307. data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
  308. data/lib/ruby_llm/protocols/gemini.rb +16 -0
  309. data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
  310. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
  311. data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
  312. data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
  313. data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
  314. data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
  315. data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
  316. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  317. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
  318. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
  319. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
  320. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
  321. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
  322. data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
  323. data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
  324. data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
  325. data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
  326. data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
  327. data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
  328. data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
  329. data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
  330. data/lib/ruby_llm/protocols/minimax.rb +21 -0
  331. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
  332. data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
  333. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
  334. data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
  335. data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
  336. data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
  337. data/lib/ruby_llm/protocols/openai/files.rb +13 -6
  338. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
  339. data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
  340. data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
  341. data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
  342. data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
  343. data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
  344. data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
  345. data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
  346. data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
  347. data/lib/ruby_llm/protocols/responses/media.rb +17 -9
  348. data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
  349. data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
  350. data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
  351. data/lib/ruby_llm/protocols/responses.rb +21 -0
  352. data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
  353. data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
  354. data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
  355. data/lib/ruby_llm/protocols/xai/files.rb +13 -6
  356. data/lib/ruby_llm/protocols/zai/files.rb +37 -0
  357. data/lib/ruby_llm/provider.rb +128 -31
  358. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  359. data/lib/ruby_llm/providers/azure/chat.rb +17 -3
  360. data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
  361. data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
  362. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
  363. data/lib/ruby_llm/providers/azure/media.rb +6 -6
  364. data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
  365. data/lib/ruby_llm/providers/azure/models.rb +16 -2
  366. data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
  367. data/lib/ruby_llm/providers/azure/responses.rb +9 -0
  368. data/lib/ruby_llm/providers/azure/videos.rb +2 -2
  369. data/lib/ruby_llm/providers/azure.rb +43 -1
  370. data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
  371. data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
  372. data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
  373. data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
  374. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
  375. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
  376. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
  377. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
  378. data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
  379. data/lib/ruby_llm/providers/bedrock/models.rb +1 -7
  380. data/lib/ruby_llm/providers/bedrock.rb +51 -8
  381. data/lib/ruby_llm/providers/cohere.rb +5 -1
  382. data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
  383. data/lib/ruby_llm/providers/deepgram.rb +21 -2
  384. data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
  385. data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
  386. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  387. data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
  388. data/lib/ruby_llm/providers/gemini.rb +13 -1
  389. data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
  390. data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
  391. data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
  392. data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
  393. data/lib/ruby_llm/providers/gpustack.rb +1 -1
  394. data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
  395. data/lib/ruby_llm/providers/hetzner.rb +5 -1
  396. data/lib/ruby_llm/providers/meta/responses.rb +81 -0
  397. data/lib/ruby_llm/providers/meta.rb +37 -0
  398. data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
  399. data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
  400. data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
  401. data/lib/ruby_llm/providers/minimax.rb +104 -0
  402. data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
  403. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +5 -14
  404. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
  405. data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
  406. data/lib/ruby_llm/providers/mistral/media.rb +8 -8
  407. data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
  408. data/lib/ruby_llm/providers/mistral.rb +13 -1
  409. data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
  410. data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
  411. data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
  412. data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
  413. data/lib/ruby_llm/providers/moonshot.rb +54 -0
  414. data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
  415. data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
  416. data/lib/ruby_llm/providers/ollama/media.rb +6 -6
  417. data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
  418. data/lib/ruby_llm/providers/ollama.rb +13 -1
  419. data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
  420. data/lib/ruby_llm/providers/openai/responses.rb +8 -0
  421. data/lib/ruby_llm/providers/openai.rb +7 -3
  422. data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
  423. data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
  424. data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
  425. data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
  426. data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
  427. data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
  428. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
  429. data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
  430. data/lib/ruby_llm/providers/openrouter.rb +32 -5
  431. data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
  432. data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
  433. data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
  434. data/lib/ruby_llm/providers/perplexity.rb +13 -2
  435. data/lib/ruby_llm/providers/typesafe.rb +1 -1
  436. data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
  437. data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
  438. data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
  439. data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
  440. data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
  441. data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
  442. data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
  443. data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
  444. data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
  445. data/lib/ruby_llm/providers/vertexai.rb +56 -9
  446. data/lib/ruby_llm/providers/xai/chat.rb +3 -3
  447. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
  448. data/lib/ruby_llm/providers/xai/images.rb +9 -4
  449. data/lib/ruby_llm/providers/xai/models.rb +2 -2
  450. data/lib/ruby_llm/providers/xai/responses.rb +10 -0
  451. data/lib/ruby_llm/providers/xai/speech.rb +2 -2
  452. data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
  453. data/lib/ruby_llm/providers/xai/videos.rb +1 -1
  454. data/lib/ruby_llm/providers/xai.rb +9 -1
  455. data/lib/ruby_llm/providers/zai/chat.rb +117 -0
  456. data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
  457. data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
  458. data/lib/ruby_llm/providers/zai/videos.rb +67 -0
  459. data/lib/ruby_llm/providers/zai.rb +66 -0
  460. data/lib/ruby_llm/question.rb +390 -0
  461. data/lib/ruby_llm/queued_message.rb +74 -0
  462. data/lib/ruby_llm/railtie.rb +43 -0
  463. data/lib/ruby_llm/request_shape/part.rb +2 -2
  464. data/lib/ruby_llm/request_shape/problem.rb +2 -2
  465. data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
  466. data/lib/ruby_llm/request_shape/turn.rb +2 -2
  467. data/lib/ruby_llm/request_shape.rb +1 -1
  468. data/lib/ruby_llm/rerank.rb +10 -37
  469. data/lib/ruby_llm/research_job.rb +26 -13
  470. data/lib/ruby_llm/sandbox/landlock.rb +78 -0
  471. data/lib/ruby_llm/sandbox/mxc.rb +137 -0
  472. data/lib/ruby_llm/sandbox.rb +141 -0
  473. data/lib/ruby_llm/search_results.rb +9 -4
  474. data/lib/ruby_llm/server_tool_call.rb +1 -1
  475. data/lib/ruby_llm/skill/loader.rb +131 -0
  476. data/lib/ruby_llm/skill.rb +263 -0
  477. data/lib/ruby_llm/speech.rb +25 -53
  478. data/lib/ruby_llm/stored_file.rb +94 -0
  479. data/lib/ruby_llm/support/cancellation.rb +4 -4
  480. data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
  481. data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
  482. data/lib/ruby_llm/support/inspectable.rb +1 -1
  483. data/lib/ruby_llm/support/instrumentation.rb +6 -5
  484. data/lib/ruby_llm/support/rescuable.rb +84 -0
  485. data/lib/ruby_llm/support/utils.rb +26 -3
  486. data/lib/ruby_llm/task.rb +126 -0
  487. data/lib/ruby_llm/test_helper.rb +129 -0
  488. data/lib/ruby_llm/thinking.rb +3 -3
  489. data/lib/ruby_llm/tokens.rb +52 -12
  490. data/lib/ruby_llm/tool.rb +339 -5
  491. data/lib/ruby_llm/tool_call/collection.rb +132 -0
  492. data/lib/ruby_llm/tool_call.rb +68 -6
  493. data/lib/ruby_llm/tools/provider_tools.rb +23 -0
  494. data/lib/ruby_llm/tools/search.rb +118 -0
  495. data/lib/ruby_llm/transcription/segment.rb +67 -0
  496. data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
  497. data/lib/ruby_llm/transcription.rb +25 -49
  498. data/lib/ruby_llm/transcription_chunk.rb +9 -9
  499. data/lib/ruby_llm/transport/connection.rb +146 -33
  500. data/lib/ruby_llm/transport/error_middleware.rb +94 -49
  501. data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
  502. data/lib/ruby_llm/transport/json_response.rb +9 -3
  503. data/lib/ruby_llm/transport/log_formatter.rb +20 -0
  504. data/lib/ruby_llm/transport/release_request.rb +35 -0
  505. data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
  506. data/lib/ruby_llm/uploaded_file.rb +15 -6
  507. data/lib/ruby_llm/version.rb +1 -1
  508. data/lib/ruby_llm/video.rb +34 -10
  509. data/lib/ruby_llm/video_job.rb +55 -42
  510. data/lib/ruby_llm/web_source.rb +38 -0
  511. data/lib/ruby_llm/workflow.rb +2 -2
  512. data/lib/ruby_llm.rb +63 -25
  513. data/lib/tasks/ruby_llm/evaluations.rb +1 -1
  514. data/skills/rubyllm/SKILL.md +128 -54
  515. data/skills/rubyllm/references/cost-and-tracing.md +8 -0
  516. data/skills/rubyllm/references/files-code-and-context.md +6 -0
  517. data/skills/rubyllm/references/operations.md +8 -0
  518. data/skills/rubyllm/references/rails-chats.md +22 -0
  519. data/skills/rubyllm/references/tools-and-agents.md +11 -0
  520. metadata +213 -14
  521. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  522. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
@@ -4,12 +4,15 @@ module RubyLLM
4
4
  module Providers
5
5
  # Native Gemini API implementation
6
6
  class Gemini < Provider
7
+ include Protocols::Gemini::RetryInfo
8
+
7
9
  protocol :gemini, Protocols::Gemini, batches: Protocols::Gemini::Batches
8
10
  protocol :interactions, Protocols::Interactions
9
11
  protocol :live_transcription, Protocols::Gemini::LiveTranscription
10
12
  protocol :files, Protocols::Gemini::Files
11
13
 
12
14
  def protocol_for(model, operation: nil, **)
15
+ return protocols[:interactions] if operation == :animate
13
16
  return protocols[:interactions] if operation == :transcribe && model.id == 'gemini-3.5-transcribe'
14
17
  return protocols[:live_transcription] if operation == :transcribe && model.id == 'gemini-3.5-transcribe-live'
15
18
 
@@ -17,7 +20,8 @@ module RubyLLM
17
20
  end
18
21
 
19
22
  def api_base
20
- @config.gemini_api_base || 'https://generativelanguage.googleapis.com/v1beta'
23
+ configured_api_base(:gemini_api_base, 'https://generativelanguage.googleapis.com/v1beta',
24
+ endpoints: %r{/models/[^/]+:\w+\z})
21
25
  end
22
26
 
23
27
  def headers
@@ -35,6 +39,14 @@ module RubyLLM
35
39
  end
36
40
 
37
41
  class << self
42
+ def open_telemetry_name # :nodoc:
43
+ 'gcp.gemini'
44
+ end
45
+
46
+ def models_dev_provider_ids # :nodoc:
47
+ %w[google]
48
+ end
49
+
38
50
  def capabilities
39
51
  Gemini::Capabilities
40
52
  end
@@ -7,13 +7,33 @@ module RubyLLM
7
7
  module Chat
8
8
  module_function
9
9
 
10
- def format_content(content, attachments = [])
11
- GPUStack::Media.format_content(content, attachments)
10
+ def render_content(content, attachments = [])
11
+ GPUStack::Media.render_content(content, attachments)
12
12
  end
13
13
 
14
- def format_role(role)
14
+ def render_role(role)
15
15
  role.to_s
16
16
  end
17
+
18
+ # vLLM truncates the rendered prompt to its last tokens. It summarizes
19
+ # nothing, and the cut can reach the system prompt. Truncating to the
20
+ # model's own limit without an output limit leaves no room to answer,
21
+ # and vLLM rejects the request.
22
+ def apply_compaction(payload, compaction)
23
+ log_ignored_compaction_options(compaction)
24
+ limit = compaction[:at] || (-1 if payload.key?(:max_tokens))
25
+ return payload.merge(truncate_prompt_tokens: limit, truncation_side: 'left') if limit
26
+
27
+ RubyLLM.logger.debug { "#{@provider.name} needs at: or an output limit to truncate, not truncating" }
28
+ payload
29
+ end
30
+
31
+ def log_ignored_compaction_options(compaction)
32
+ ignored = compaction.except(:at)
33
+ return if ignored.empty?
34
+
35
+ RubyLLM.logger.debug { "#{@provider.name} only truncates the prompt, dropping #{ignored.inspect}" }
36
+ end
17
37
  end
18
38
  end
19
39
  end
@@ -7,7 +7,7 @@ module RubyLLM
7
7
  module Embeddings
8
8
  module_function
9
9
 
10
- def supports_embedding_media?
10
+ def embedding_media?
11
11
  true
12
12
  end
13
13
 
@@ -18,7 +18,7 @@ module RubyLLM
18
18
  raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
19
19
 
20
20
  payload.delete(:input)
21
- payload[:messages] = [{ role: 'user', content: GPUStack::Media.format_content(text, with) }]
21
+ payload[:messages] = [{ role: 'user', content: GPUStack::Media.render_content(text, with) }]
22
22
  end
23
23
  payload.merge(provider_options)
24
24
  end
@@ -11,24 +11,24 @@ module RubyLLM
11
11
  %i[image audio video 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 :video
24
- format_video(attachment)
24
+ render_video(attachment)
25
25
  when :text
26
- Protocols::ChatCompletions::Media.format_text_file(attachment)
26
+ Protocols::ChatCompletions::Media.render_text_file(attachment)
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
- def format_image(image)
31
+ def render_image(image)
32
32
  {
33
33
  type: 'image_url',
34
34
  image_url: {
@@ -38,7 +38,7 @@ module RubyLLM
38
38
  }
39
39
  end
40
40
 
41
- def format_video(video)
41
+ def render_video(video)
42
42
  {
43
43
  type: 'video_url',
44
44
  video_url: { url: video.for_llm }
@@ -7,19 +7,27 @@ module RubyLLM
7
7
  module Transcription
8
8
  module_function
9
9
 
10
- def stream_transcription(payload, model:, &)
11
- super({ stream_include_usage: 'true' }.merge(payload), model:, &)
10
+ # Usage may follow the finish reason in an event of its own, so only
11
+ # the end of the stream can say the transcript is done.
12
+ def stream_transcription(payload, model:, &block)
13
+ last = nil
14
+ result = super({ stream_include_usage: 'true' }.merge(payload), model:) do |chunk|
15
+ last = chunk
16
+ block.call(chunk) unless chunk.done?
17
+ end
18
+ block.call(RubyLLM::TranscriptionChunk.new(type: RubyLLM::TranscriptionChunk::DONE, text: result.text,
19
+ raw: last&.raw))
20
+ result
12
21
  end
13
22
 
14
23
  def build_transcription_chunk(data)
15
24
  return super unless data.key?('choices')
16
25
 
17
- choice = data['choices'].first || {}
18
- finished = choice['finish_reason'] || data['choices'].empty?
26
+ choice = data['choices'].first
19
27
 
20
28
  RubyLLM::TranscriptionChunk.new(
21
- type: finished ? RubyLLM::TranscriptionChunk::DONE : RubyLLM::TranscriptionChunk::DELTA,
22
- delta: choice.dig('delta', 'content'),
29
+ type: choice ? RubyLLM::TranscriptionChunk::DELTA : RubyLLM::TranscriptionChunk::DONE,
30
+ delta: choice&.dig('delta', 'content'),
23
31
  raw: data
24
32
  )
25
33
  end
@@ -27,7 +27,7 @@ module RubyLLM
27
27
  end
28
28
 
29
29
  def api_base
30
- @config.gpustack_api_base
30
+ configured_api_base(:gpustack_api_base, example: 'http://HOST/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
31
31
  end
32
32
 
33
33
  def backend_api_base # :nodoc:
@@ -7,22 +7,39 @@ module RubyLLM
7
7
  module Chat
8
8
  module_function
9
9
 
10
- def format_role(role)
10
+ REJECTED_EFFORT = /\AUnexpected reasoning effort /
11
+
12
+ def render_role(role)
11
13
  role.to_s
12
14
  end
13
15
 
16
+ # The API accepts cache fields and ignores them; it caches long
17
+ # prompts by itself.
18
+ def openai_prompt_caching?
19
+ false
20
+ end
21
+
22
+ def apply_prompt_cache_params(_payload, caching)
23
+ prompt_cache_options(caching) if caching
24
+ end
25
+
26
+ # The API names a rejected effort only in its message.
27
+ def parse_unsupported_parameter(response)
28
+ super || (:thinking if parse_error(response).to_s.match?(REJECTED_EFFORT))
29
+ end
30
+
14
31
  def supported_attachment?(attachment)
15
32
  Protocols::ChatCompletions::Media.supported_attachment?(
16
33
  attachment, document_attachments: :none, audio_attachments: false
17
34
  )
18
35
  end
19
36
 
20
- def format_content(content, attachments = [])
21
- Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
37
+ def render_content(content, attachments = [])
38
+ Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
22
39
  if attachment.type == :image && !attachment.provider_file?
23
- format_inline_image(attachment)
40
+ render_inline_image(attachment)
24
41
  else
25
- Protocols::ChatCompletions::Media.format_attachment(
42
+ Protocols::ChatCompletions::Media.render_attachment(
26
43
  attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
27
44
  )
28
45
  end
@@ -30,7 +47,7 @@ module RubyLLM
30
47
  end
31
48
 
32
49
  # Hetzner's servers can't fetch URLs, so remote images travel inline too.
33
- def format_inline_image(image)
50
+ def render_inline_image(image)
34
51
  { type: 'image_url', image_url: { url: image.for_llm } }
35
52
  end
36
53
  end
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  protocol :chat_completions, ChatCompletions
13
13
 
14
14
  def api_base
15
- @config.hetzner_api_base || 'https://inference.hetzner.com/api/v1'
15
+ configured_api_base(:hetzner_api_base, 'https://inference.hetzner.com/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
16
16
  end
17
17
 
18
18
  def headers
@@ -20,6 +20,10 @@ module RubyLLM
20
20
  end
21
21
 
22
22
  class << self
23
+ def models_dev_provider_ids # :nodoc:
24
+ %w[hetzner]
25
+ end
26
+
23
27
  def configuration_options
24
28
  %i[hetzner_api_key hetzner_api_base]
25
29
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Meta
6
+ # Meta's dialect of the OpenAI Responses API, the format Meta
7
+ # recommends because it replays encrypted reasoning across turns.
8
+ # https://dev.meta.ai/docs/protocols/responses
9
+ class Responses < Protocols::Responses
10
+ include Protocols::Responses::TokenCounting
11
+ include Protocols::Meta::Transcription
12
+
13
+ SERVER_TOOL_ALIASES = {
14
+ web_search: { tool: { type: 'web_search' } }
15
+ }.freeze
16
+
17
+ PROMPT_CACHE_OPTIONS = %i[key ttl].freeze
18
+ AUDIO_FORMATS = %w[mp3 wav].freeze
19
+
20
+ def server_tool_aliases
21
+ SERVER_TOOL_ALIASES
22
+ end
23
+
24
+ def document_urls?
25
+ true
26
+ end
27
+
28
+ def web_search_sources?
29
+ true
30
+ end
31
+
32
+ def apply_end_user(payload, identifier)
33
+ payload.merge(safety_identifier: identifier)
34
+ end
35
+
36
+ # Meta takes a cache key and a retention hint, in_memory or 24h,
37
+ # and no cache breakpoints.
38
+ def prompt_cache_params(caching)
39
+ options = caching.to_h.transform_keys(&:to_sym)
40
+ unsupported = options.keys - PROMPT_CACHE_OPTIONS
41
+ unless unsupported.empty?
42
+ raise ArgumentError,
43
+ "Meta prompt caching accepts :key and :ttl, got #{format_cache_option_keys(unsupported)}"
44
+ end
45
+
46
+ ttl = options[:ttl] && render_cache_lifetime(options[:ttl])
47
+ { prompt_cache_key: options[:key], prompt_cache_retention: ttl }.compact
48
+ end
49
+
50
+ def cache_option_names = PROMPT_CACHE_OPTIONS
51
+
52
+ def inject_cache_breakpoint(item)
53
+ item
54
+ end
55
+
56
+ def supported_attachment?(attachment)
57
+ return super if attachment.provider_file?
58
+ return AUDIO_FORMATS.include?(attachment.format) if attachment.audio?
59
+
60
+ attachment.video? || super
61
+ end
62
+
63
+ # Meta's fetcher is refused by many hosts, so remote media travels inline.
64
+ def render_attachment(attachment)
65
+ return super if attachment.provider_file? || !supported_attachment?(attachment)
66
+
67
+ case attachment.type
68
+ when :video then { type: 'input_video', video_url: attachment.for_llm }
69
+ when :audio then { type: 'input_audio', input_audio: { data: attachment.encoded, format: attachment.format } }
70
+ else super
71
+ end
72
+ end
73
+
74
+ # Meta refuses to fetch many remote images, so images travel inline.
75
+ def render_image(image, image_url: image.for_llm, **)
76
+ super
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Meta Model API integration for the Muse models.
6
+ class Meta < Provider
7
+ protocol :responses, Meta::Responses
8
+ protocol :files, Protocols::OpenAI::Files
9
+
10
+ def api_base
11
+ configured_api_base(:meta_api_base, 'https://api.meta.ai/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
12
+ end
13
+
14
+ def headers
15
+ { 'Authorization' => "Bearer #{@config.meta_api_key}" }
16
+ end
17
+
18
+ def account_identity
19
+ account_digest(api_base, @config.meta_api_key)
20
+ end
21
+
22
+ class << self
23
+ def models_dev_provider_ids # :nodoc:
24
+ %w[meta]
25
+ end
26
+
27
+ def configuration_options
28
+ %i[meta_api_key meta_api_base]
29
+ end
30
+
31
+ def configuration_requirements
32
+ %i[meta_api_key]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class MiniMax
6
+ # MiniMax's dialect of the Anthropic Messages API. MiniMax has no
7
+ # Files API on this endpoint, and web search is the one server tool it
8
+ # runs, in Anthropic's first version.
9
+ class Anthropic < Protocols::Anthropic
10
+ SERVER_TOOL_ALIASES = {
11
+ web_search: { tool: { type: 'web_search_20250305', name: 'web_search' } }
12
+ }.freeze
13
+
14
+ RESPONSES_ITEMS = %w[reasoning web_search_call message function_call].freeze
15
+ private_constant :RESPONSES_ITEMS
16
+
17
+ def render_payload(messages, tools:, schema: nil, tool_prefs: nil, **)
18
+ raise ArgumentError, "MiniMax's Messages API has no structured output" if schema
19
+ raise ArgumentError, 'MiniMax does not accept calls: :one' if tools.any? && tool_prefs&.dig(:calls) == :one
20
+
21
+ super
22
+ end
23
+
24
+ def render_count_tokens_payload(messages, tool_prefs: nil, **)
25
+ super(messages, **, schema: nil, tool_prefs: tool_prefs&.except(:calls))
26
+ end
27
+
28
+ def server_tool_aliases
29
+ SERVER_TOOL_ALIASES
30
+ end
31
+
32
+ def provider_file_references?
33
+ false
34
+ end
35
+
36
+ def supported_attachment?(attachment)
37
+ return attachment.source.provider == 'minimax' if attachment.provider_file? && attachment.video?
38
+
39
+ attachment.video? || super
40
+ end
41
+
42
+ private
43
+
44
+ # A video block is documented for user messages only; tool results
45
+ # and system prompts take the attachments Anthropic's format does.
46
+ def supported_message_attachment?(message, attachment)
47
+ return false if attachment.video? && message.role != :user
48
+
49
+ super
50
+ end
51
+
52
+ def render_content(content, attachments = [], citations: false)
53
+ blocks = super(content, [], citations:)
54
+ attachments.each do |attachment|
55
+ blocks.concat(attachment.video? ? [render_video(attachment)] : super(nil, [attachment], citations:))
56
+ end
57
+ blocks
58
+ end
59
+
60
+ def append_formatted_content(content_blocks, msg, citations: false)
61
+ video = msg.attachments.find(&:video?) unless msg.role == :user
62
+ raise UnsupportedAttachmentError, video.mime_type if video
63
+
64
+ content_blocks.concat(render_content(msg.content, msg.attachments, citations:))
65
+ end
66
+
67
+ def render_video(video)
68
+ raise UnsupportedAttachmentError, video.mime_type unless supported_attachment?(video)
69
+
70
+ { type: 'video', source: video_source(video) }
71
+ end
72
+
73
+ # MiniMax's fetcher is refused by many hosts, so remote videos travel inline.
74
+ def video_source(video)
75
+ return { type: 'url', url: "mm_file://#{video.provider_file_id}" } if video.provider_file?
76
+
77
+ media_type = video.mime_type == 'video/quicktime' ? 'video/mov' : video.mime_type
78
+ { type: 'base64', media_type:, data: video.encoded }
79
+ end
80
+
81
+ # A web search turn answered on Responses keeps that route's output
82
+ # items, which a Messages request cannot carry.
83
+ def foreign_native_content?(message)
84
+ super || (message.role == :assistant && responses_output?(message.raw_content))
85
+ end
86
+
87
+ def responses_output?(raw_content)
88
+ raw_content.is_a?(Array) && raw_content.any? do |item|
89
+ item.is_a?(Hash) && RESPONSES_ITEMS.include?(item['type'])
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class MiniMax
6
+ # MiniMax's own Chat Completion v2 route, used only for structured
7
+ # output on the one model that applies response_format. That model
8
+ # nests its answer under the schema name, so parsing removes the
9
+ # wrapper, which cannot be done part by part on a stream.
10
+ class ChatCompletions < Protocols::ChatCompletions
11
+ def completion_url
12
+ "#{@provider.media_base}/v1/text/chatcompletion_v2"
13
+ end
14
+
15
+ def render_payload(messages, schema: nil, stream: false, **)
16
+ raise ArgumentError, "#{@model.id} does not stream structured output" if schema && stream
17
+
18
+ @schema_name = schema&.dig(:name)
19
+ super
20
+ end
21
+
22
+ def parse_completion_body(data, raw:)
23
+ super(unwrap_schema_result(data), raw: raw)
24
+ end
25
+
26
+ private
27
+
28
+ def unwrap_schema_result(data)
29
+ content = data.dig('choices', 0, 'message', 'content')
30
+ return data unless @schema_name && content.is_a?(String)
31
+
32
+ parsed = parse_json(content)
33
+ return data unless parsed.is_a?(Hash) && parsed.keys == [@schema_name]
34
+
35
+ choice = data['choices'][0]
36
+ message = choice['message'].merge('content' => JSON.generate(parsed[@schema_name]))
37
+ data.merge('choices' => [choice.merge('message' => message), *data['choices'][1..]])
38
+ end
39
+
40
+ def parse_json(content)
41
+ JSON.parse(content)
42
+ rescue JSON::ParserError
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class MiniMax
6
+ # MiniMax's Responses route, used for web search because only this
7
+ # endpoint returns citations. It holds no state: store is always false.
8
+ # https://platform.minimax.io/docs/api-reference/responses-create
9
+ class Responses < Protocols::Responses
10
+ SERVER_TOOL_ALIASES = {
11
+ web_search: { tool: { type: 'web_search' } }
12
+ }.freeze
13
+
14
+ # MiniMax reports a search's pages only through its citations.
15
+ def self.parse_web_sources(message)
16
+ message.citations.filter_map do |citation|
17
+ WebSource.new(url: citation.url, title: citation.title) if citation.url
18
+ end
19
+ end
20
+
21
+ def completion_url
22
+ "#{@provider.media_base}/v1/responses"
23
+ end
24
+
25
+ def server_tool_aliases
26
+ SERVER_TOOL_ALIASES
27
+ end
28
+
29
+ private
30
+
31
+ def provider_file_references?
32
+ false
33
+ end
34
+
35
+ # MiniMax sends url_citation annotations with both offsets at 0, so
36
+ # they say nothing about the span they support. Its content field is
37
+ # the quoted snippet.
38
+ def parse_annotations(annotations, _content)
39
+ Array(annotations).filter_map do |annotation|
40
+ next unless annotation['type'] == 'url_citation'
41
+
42
+ Citation.new(url: annotation['url'], title: annotation['title'], cited_text: annotation['content'])
43
+ end
44
+ end
45
+
46
+ def offset_citations(citations, *)
47
+ citations
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # MiniMax API integration, through its Anthropic-compatible endpoint.
6
+ class MiniMax < Provider
7
+ protocol :anthropic, MiniMax::Anthropic
8
+ protocol :media, Protocols::MiniMax
9
+ protocol :files, Protocols::MiniMax::Files
10
+ protocol :chat_completions, MiniMax::ChatCompletions
11
+ protocol :responses, MiniMax::Responses
12
+
13
+ MEDIA_OPERATIONS = %i[paint animate speak transcribe].freeze
14
+
15
+ # The one MiniMax model that applies a response_format, on Chat
16
+ # Completion v2. The others ignore every structured output field.
17
+ STRUCTURED_OUTPUT_MODEL = 'MiniMax-Text-01'
18
+
19
+ # The HTTP status each MiniMax status code stands for on endpoints that
20
+ # answer 200 and report the failure in base_resp.
21
+ FAILURE_STATUSES = {
22
+ 1000 => 500, 1001 => 504, 1002 => 429, 1004 => 401, 1008 => 402, 1039 => 429, 2049 => 401, 2056 => 429
23
+ }.freeze
24
+
25
+ def resolve_protocol(name, model, operation: nil, **options)
26
+ name ||= :media if MEDIA_OPERATIONS.include?(operation)
27
+ super
28
+ end
29
+
30
+ def protocol_for(model, schema: nil, thinking: nil, tool_prefs: nil, provider_tools: [], messages: [], **)
31
+ return protocols[:chat_completions] if schema && model.id == STRUCTURED_OUTPUT_MODEL
32
+ return protocols[:responses] if web_search_route?(schema, thinking, tool_prefs, provider_tools, messages)
33
+
34
+ super
35
+ end
36
+
37
+ def api_base
38
+ configured_api_base(:minimax_api_base, 'https://api.minimax.io/anthropic', endpoints: %r{/v1/messages\z})
39
+ end
40
+
41
+ def media_base # :nodoc:
42
+ api_base.sub(%r{/anthropic/?\z}, '')
43
+ end
44
+
45
+ def headers
46
+ { 'Authorization' => "Bearer #{@config.minimax_api_key}" }
47
+ end
48
+
49
+ def account_identity
50
+ account_digest(api_base, @config.minimax_api_key)
51
+ end
52
+
53
+ def error_status(response)
54
+ code = failure_code(response)
55
+ FAILURE_STATUSES.fetch(code, 400) if code
56
+ end
57
+
58
+ def parse_error(response)
59
+ return super unless failure_code(response)
60
+
61
+ parse_error_body(response).dig('base_resp', 'status_msg')
62
+ end
63
+
64
+ class << self
65
+ def models_dev_provider_ids # :nodoc:
66
+ %w[minimax]
67
+ end
68
+
69
+ def configuration_options
70
+ %i[minimax_api_key minimax_api_base]
71
+ end
72
+
73
+ def configuration_requirements
74
+ %i[minimax_api_key]
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # Only Responses cites web pages, but it ignores an attached PDF, so a
81
+ # chat that holds one stays on Messages, which reads it. Messages also
82
+ # raises on what MiniMax cannot carry and honours a thinking budget or
83
+ # off switch, all of which Responses would drop.
84
+ def web_search_route?(schema, thinking, tool_prefs, provider_tools, messages)
85
+ return false unless provider_tools.any? { |entry| entry[:name] == :web_search }
86
+ return false if messages_only_request?(schema, thinking, tool_prefs)
87
+
88
+ messages.none? { |message| message.attachments.any?(&:pdf?) }
89
+ end
90
+
91
+ def messages_only_request?(schema, thinking, tool_prefs)
92
+ schema || tool_prefs&.dig(:calls) == :one || (thinking && !thinking.effort)
93
+ end
94
+
95
+ def failure_code(response)
96
+ return unless response.body.to_s.include?('base_resp')
97
+
98
+ body = parse_error_body(response)
99
+ code = body.dig('base_resp', 'status_code') if body.is_a?(Hash)
100
+ code unless code.nil? || code.zero?
101
+ end
102
+ end
103
+ end
104
+ end