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
@@ -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
@@ -6,15 +6,43 @@ module RubyLLM
6
6
  # Chat methods for Mistral API
7
7
  module Chat
8
8
  PROMPT_CACHE_OPTIONS = %i[key].freeze
9
+ FINISH_REASONS = Protocols::ChatCompletions::Chat::FINISH_REASONS.merge('model_length' => :max_tokens).freeze
9
10
 
10
11
  module_function
11
12
 
12
- def format_role(role)
13
+ def finish_reasons = FINISH_REASONS
14
+
15
+ # A tool that supplies numbered sources gets reference chunks back,
16
+ # whose reference_ids are the keys of that tool's own result.
17
+ def extract_citations(message_data, data, content)
18
+ references = reference_citations(message_data['content'])
19
+ references.any? ? references : super
20
+ end
21
+
22
+ def parse_chunk_citations(delta, data)
23
+ references = reference_citations(delta['content'], offsets: false)
24
+ references.any? ? references : super
25
+ end
26
+
27
+ def reference_citations(content, offsets: true)
28
+ return [] unless content.is_a?(Array)
29
+
30
+ position = 0
31
+ content.flat_map do |part|
32
+ position += part['text'].to_s.length if part['type'] == 'text'
33
+ span = offsets ? { start_index: position, end_index: position } : {}
34
+ Array(part['reference_ids']).map { |id| Citation.new(source_index: id, **span) }
35
+ end
36
+ end
37
+
38
+ def render_role(role)
13
39
  role.to_s
14
40
  end
15
41
 
16
42
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
17
43
  schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
44
+ raise ArgumentError, 'Mistral has no thinking budget; use effort:' if thinking&.budget
45
+
18
46
  payload = super
19
47
  payload.delete(:stream_options)
20
48
  normalize_required_tool_choice(payload)
@@ -42,6 +70,8 @@ module RubyLLM
42
70
  raise ArgumentError, "Mistral prompt caching accepts :key, got #{format_cache_option_keys(unsupported)}"
43
71
  end
44
72
 
73
+ def cache_option_names = PROMPT_CACHE_OPTIONS
74
+
45
75
  def format_cache_option_keys(keys)
46
76
  keys.map { |key| ":#{key}" }.join(', ')
47
77
  end
@@ -49,7 +79,7 @@ module RubyLLM
49
79
  # extra_content carries Gemini's thought signatures on Google's
50
80
  # OpenAI-compatible endpoints, and Mistral refuses any field it does
51
81
  # not define.
52
- def format_tool_calls(tool_calls)
82
+ def render_tool_calls(tool_calls)
53
83
  super&.map { |call| call.except(:extra_content) }
54
84
  end
55
85
 
@@ -73,7 +103,7 @@ module RubyLLM
73
103
 
74
104
  # Mistral carries reasoning in content blocks, not in the top-level
75
105
  # reasoning fields the rest of the Chat Completions family uses.
76
- def format_message_content(msg, **)
106
+ def render_message_content(msg, **)
77
107
  formatted_content = super
78
108
  return formatted_content unless msg.role == :assistant && msg.thinking
79
109
 
@@ -83,7 +113,7 @@ module RubyLLM
83
113
  content_blocks
84
114
  end
85
115
 
86
- def format_thinking(_msg)
116
+ def render_thinking(_msg)
87
117
  {}
88
118
  end
89
119
 
@@ -99,10 +99,15 @@ module RubyLLM
99
99
  custom_id, shape = line['custom_id'].split(':', 2)
100
100
  index = batch_result_index(custom_id)
101
101
  response = line['response']
102
+ body = response['body'] if batch_response_succeeded?(response)
102
103
 
103
- if response && response['body']
104
- body = response['body']
105
- [index, parse_mistral_batch_body(body, shape:)]
104
+ if body && body['data'].is_a?(Array) && !valid_embedding_positions?(body['data'])
105
+ return [index, nil, batch_failure(line['custom_id'], 'Invalid or duplicate embedding record positions')]
106
+ end
107
+
108
+ if body
109
+ billed = body unless body['data'].is_a?(Array)
110
+ parsed_batch_result(index, custom_id, billed) { parse_mistral_batch_body(body, shape:) }
106
111
  else
107
112
  [index, nil, batch_failure(line['custom_id'], batch_error_message(line))]
108
113
  end
@@ -11,24 +11,13 @@ module RubyLLM
11
11
  'embeddings'
12
12
  end
13
13
 
14
- # rubocop:disable-next Lint/UnusedMethodArgument
15
- def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
14
+ def render_embedding_payload(text, model:, dimensions:, provider_options: {}, **)
16
15
  {
17
16
  model: model,
18
17
  input: text,
19
18
  output_dimension: dimensions
20
19
  }.compact.merge(provider_options)
21
20
  end
22
-
23
- def parse_embedding_response(response, model:, text:)
24
- data = response.body
25
- input_tokens = data.dig('usage', 'prompt_tokens')
26
- vectors = data['data'].map { |d| d['embedding'] }
27
-
28
- vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
29
-
30
- Embedding.new(vectors:, model:, input_tokens:)
31
- end
32
21
  end
33
22
  end
34
23
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Counts a request through Mistral's Responses input-token endpoint,
7
+ # the only token counting route Mistral offers, whichever protocol the
8
+ # chat itself runs on.
9
+ class InputTokens < Protocols::Responses
10
+ include Protocols::Responses::TokenCounting
11
+
12
+ def render_count_tokens_payload(messages, **)
13
+ payload = super(messages.flat_map { |message| countable_messages(message) }, **)
14
+ payload.merge(input: payload[:input].map { |item| typed_item(item) })
15
+ end
16
+
17
+ private
18
+
19
+ # Mistral rejects an input item that doesn't name its type.
20
+ def typed_item(item)
21
+ item.key?(:role) ? { type: 'message' }.merge(item) : item
22
+ end
23
+
24
+ # Native content from other protocols doesn't fit Responses input,
25
+ # and the endpoint can't take unsigned thinking, so thinking goes in
26
+ # as text to keep it in the count.
27
+ def countable_messages(message)
28
+ plain = message.without_native_content
29
+ text = message.thinking&.text
30
+ text ? [Message.new(role: :assistant, content: text), plain] : [plain]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -11,31 +11,31 @@ module RubyLLM
11
11
  %i[image audio pdf document 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 :pdf, :document
24
- format_document(attachment)
24
+ render_document(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: image.url_or_data_uri
35
35
  }
36
36
  end
37
37
 
38
- def format_document(document)
38
+ def render_document(document)
39
39
  {
40
40
  type: 'document_url',
41
41
  document_url: document.url_or_data_uri
@@ -3,8 +3,8 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Mistral
6
- # Transcription dialect for the Mistral audio API. Giving speaker
7
- # names turns on diarization, which labels each segment with a
6
+ # Transcription dialect for the Mistral audio API. Requesting speaker
7
+ # labels turns on diarization, which labels each segment with a
8
8
  # speaker id; a prompt rides along as context biasing terms.
9
9
  module Transcription
10
10
  STREAM_TYPES = {
@@ -20,6 +20,10 @@ module RubyLLM
20
20
  { timestamp_granularities: ['segment'] }
21
21
  end
22
22
 
23
+ def rejected_options(operation)
24
+ operation == :transcribe ? %i[format speaker_references named_speakers] : super
25
+ end
26
+
23
27
  module_function
24
28
 
25
29
  def build_transcription_chunk(data)
@@ -39,10 +43,8 @@ module RubyLLM
39
43
  usage['prompt_audio_seconds'] || super
40
44
  end
41
45
 
42
- # rubocop:disable-next Lint/UnusedMethodArgument
43
- def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
44
- speaker_references: nil, provider_options: {}, prompt: nil,
45
- temperature: nil)
46
+ def render_transcription_payload(file_part, model:, language:, speaker_names: nil, provider_options: {},
47
+ prompt: nil, temperature: nil, **)
46
48
  payload = {
47
49
  model: model,
48
50
  file: file_part,
@@ -14,8 +14,12 @@ module RubyLLM
14
14
  super
15
15
  end
16
16
 
17
+ def count_tokens(messages, model:, tools: {}, protocol: nil, **) # rubocop:disable Lint/UnusedMethodArgument
18
+ InputTokens.new(self, model).count_tokens(messages, tools:, **)
19
+ end
20
+
17
21
  def api_base
18
- @config.mistral_api_base || 'https://api.mistral.ai/v1'
22
+ configured_api_base(:mistral_api_base, 'https://api.mistral.ai/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
19
23
  end
20
24
 
21
25
  def headers
@@ -27,6 +31,14 @@ module RubyLLM
27
31
  def batch_cost_multiplier(**) = 0.5
28
32
 
29
33
  class << self
34
+ def open_telemetry_name # :nodoc:
35
+ 'mistral_ai'
36
+ end
37
+
38
+ def models_dev_provider_ids # :nodoc:
39
+ %w[mistral]
40
+ end
41
+
30
42
  def capabilities
31
43
  Mistral::Capabilities
32
44
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Moonshot
6
+ # Feature capability gaps not represented in upstream model catalogs.
7
+ module Capabilities
8
+ def self.augment(capabilities, **)
9
+ return capabilities unless capabilities.include?('function_calling')
10
+
11
+ capabilities | ['tool_choice']
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Moonshot
6
+ # Chat methods of the Moonshot AI API integration
7
+ # https://platform.kimi.ai/docs/api/chat
8
+ module Chat
9
+ module_function
10
+
11
+ def render_role(role)
12
+ role.to_s
13
+ end
14
+
15
+ def max_output_tokens_field(_model)
16
+ :max_completion_tokens
17
+ end
18
+
19
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
20
+ schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
21
+ payload = super
22
+ raise ArgumentError, 'Moonshot does not accept calls: :one' if payload.delete(:parallel_tool_calls) == false
23
+
24
+ payload[:thinking] = { type: thinking.enabled ? 'enabled' : 'disabled' } unless thinking&.enabled.nil?
25
+ payload.merge!(prompt_cache_params(caching)) if caching
26
+ payload
27
+ end
28
+
29
+ def count_tokens_url
30
+ 'tokenizers/estimate-token-count'
31
+ end
32
+
33
+ def render_count_tokens_payload(messages, model:, **)
34
+ render_payload(messages, model:, temperature: nil, **).slice(:model, :messages, :tools, :response_format)
35
+ end
36
+
37
+ def parse_count_tokens_response(response)
38
+ response.body.dig('data', 'total_tokens')
39
+ end
40
+
41
+ def apply_end_user(payload, identifier)
42
+ payload.merge(safety_identifier: identifier)
43
+ end
44
+
45
+ # Moonshot takes prompt_cache_key and prompt_cache_options, and
46
+ # refuses the explicit cache breakpoints OpenAI accepts.
47
+ def openai_prompt_caching?
48
+ false
49
+ end
50
+
51
+ def render_thinking(msg)
52
+ text = msg.thinking&.text if msg.role == :assistant
53
+ text ? { reasoning_content: text } : {}
54
+ end
55
+
56
+ def supported_attachment?(attachment)
57
+ return attachment.image? || own_video?(attachment) if attachment.provider_file?
58
+
59
+ attachment.video? || Protocols::ChatCompletions::Media.supported_attachment?(
60
+ attachment, document_attachments: :none, audio_attachments: false
61
+ )
62
+ end
63
+
64
+ def render_content(content, attachments = [])
65
+ Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
66
+ if attachment.image?
67
+ render_inline_image(attachment)
68
+ elsif attachment.video?
69
+ render_inline_video(attachment)
70
+ else
71
+ Protocols::ChatCompletions::Media.render_attachment(
72
+ attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
73
+ )
74
+ end
75
+ end
76
+ end
77
+
78
+ def render_inline_image(image)
79
+ { type: 'image_url', image_url: { url: inline_url(image) } }
80
+ end
81
+
82
+ def own_video?(attachment)
83
+ attachment.video? && attachment.source.provider == 'moonshot'
84
+ end
85
+
86
+ def render_inline_video(video)
87
+ raise UnsupportedAttachmentError, video.mime_type unless supported_attachment?(video)
88
+
89
+ { type: 'video_url', video_url: { url: inline_url(video) } }
90
+ end
91
+
92
+ # Moonshot reads media only from data URIs and its own file references,
93
+ # so remote files travel inline.
94
+ def inline_url(attachment)
95
+ attachment.provider_file? ? "ms://#{attachment.provider_file_id}" : attachment.for_llm
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module RubyLLM
6
+ module Providers
7
+ class Moonshot
8
+ # Kimi models cannot read a document attached to a message. Kimi
9
+ # documents one way to ask about a file: extract its text through the
10
+ # Files API and send that text in the conversation.
11
+ # https://platform.kimi.ai/docs/guide/ask-questions-about-pdf-content
12
+ module Documents
13
+ EXTRACT_PURPOSE = 'file-extract'
14
+
15
+ def preprocess_message(message, unsupported_attachment: nil)
16
+ return super unless message.attachments.any? { |attachment| extracted_document?(attachment) }
17
+
18
+ super(message.with_attachments(message.attachments.map do |attachment|
19
+ extracted_document?(attachment) ? extracted_attachment(attachment) : attachment
20
+ end), unsupported_attachment:)
21
+ end
22
+
23
+ # Kimi extracts a whole document without a model, so +model:+ only
24
+ # labels the result. The upload exists only to read it back.
25
+ def ocr(file, model:, pages: nil, provider_options: {})
26
+ raise ArgumentError, "#{@provider.name} extracts whole documents; pages: is not available" if pages
27
+
28
+ track_usage(:ocr) do
29
+ upload = @provider.upload_file(file, purpose: EXTRACT_PURPOSE, provider_options:)
30
+ begin
31
+ text = @provider.download_file(upload.id)
32
+ RubyLLM::OCR.new(pages: [{ 'index' => 0, 'markdown' => text }], model:, raw: upload.metadata)
33
+ ensure
34
+ delete_upload(upload)
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def provider_file_upload_options(_attachment)
42
+ { purpose: EXTRACT_PURPOSE }
43
+ end
44
+
45
+ def extracted_document?(attachment)
46
+ return attachment.source.purpose == EXTRACT_PURPOSE if attachment.provider_file?
47
+
48
+ %i[pdf document].include?(attachment.type)
49
+ end
50
+
51
+ def extracted_attachment(attachment)
52
+ filename = "#{File.basename(attachment.filename.to_s, '.*')}.md"
53
+ Attachment.new(StringIO.new(extracted_text(attachment)), filename:, config: @config)
54
+ end
55
+
56
+ def extracted_text(attachment)
57
+ return @provider.download_file(attachment.provider_file_id) if attachment.provider_file?
58
+
59
+ download_upload(attachment)
60
+ rescue Error => e
61
+ message = Message.new(role: :user, content: nil, attachments: [attachment])
62
+ raise if attachment.provider_file? || discard_missing_uploads([message], e).empty?
63
+
64
+ download_upload(attachment)
65
+ end
66
+
67
+ def download_upload(attachment)
68
+ @provider.download_file(provider_upload(attachment).id)
69
+ end
70
+
71
+ def delete_upload(upload)
72
+ @connection.delete("files/#{upload.id}")
73
+ rescue Error
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Moonshot
6
+ # Moonshot's dialect of the OpenAI Responses API, the route to Kimi's
7
+ # server-side web search. It holds no state: store is always false.
8
+ # https://platform.kimi.ai/docs/api/responses
9
+ class Responses < Protocols::Responses
10
+ include Moonshot::Documents
11
+
12
+ SERVER_TOOL_ALIASES = {
13
+ web_search: { tool: { type: 'web_search' } }
14
+ }.freeze
15
+
16
+ def server_tool_aliases
17
+ SERVER_TOOL_ALIASES
18
+ end
19
+
20
+ def web_search_sources?
21
+ true
22
+ end
23
+
24
+ def apply_end_user(payload, identifier)
25
+ payload.merge(safety_identifier: identifier)
26
+ end
27
+
28
+ # Kimi reads images only from data URIs, so remote images travel inline.
29
+ def render_image(image, image_url: image.for_llm, **)
30
+ super
31
+ end
32
+
33
+ private
34
+
35
+ def provider_file_references?
36
+ false
37
+ end
38
+
39
+ # Kimi silently drops the web_search tool for a message whose content
40
+ # is a bare string, so text always travels as input_text parts.
41
+ def render_item(msg, caching: nil)
42
+ item = super
43
+ return item unless item.is_a?(Hash) && item[:role] && item[:content].is_a?(String)
44
+
45
+ item.merge(content: [{ type: 'input_text', text: item[:content] }])
46
+ end
47
+
48
+ def parse_server_tool_use(response)
49
+ searches = Array(response['output']).count { |item| item['type'] == 'web_search_call' }
50
+ searches.positive? ? { 'web_search_requests' => searches } : {}
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Moonshot AI API integration for the Kimi models.
6
+ class Moonshot < Provider
7
+ # Moonshot's dialect of the Chat Completions API.
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include Moonshot::Chat
10
+ include Moonshot::Documents
11
+ end
12
+
13
+ protocol :chat_completions, ChatCompletions, batches: Protocols::ChatCompletions::Batches
14
+ protocol :responses, Moonshot::Responses
15
+ protocol :files, Protocols::Moonshot::Files
16
+
17
+ def api_base
18
+ configured_api_base(:moonshot_api_base, 'https://api.moonshot.ai/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
19
+ end
20
+
21
+ def headers
22
+ { 'Authorization' => "Bearer #{@config.moonshot_api_key}" }
23
+ end
24
+
25
+ def account_identity
26
+ account_digest(api_base, @config.moonshot_api_key)
27
+ end
28
+
29
+ def batch_cost_multiplier(**) = 0.6
30
+
31
+ class << self
32
+ def open_telemetry_name # :nodoc:
33
+ 'moonshot_ai'
34
+ end
35
+
36
+ def models_dev_provider_ids # :nodoc:
37
+ %w[moonshotai]
38
+ end
39
+
40
+ def capabilities
41
+ Moonshot::Capabilities
42
+ end
43
+
44
+ def configuration_options
45
+ %i[moonshot_api_key moonshot_api_base]
46
+ end
47
+
48
+ def configuration_requirements
49
+ %i[moonshot_api_key]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end