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
@@ -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,14 +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 && response['body']
102
+ body = response['body'] if batch_response_succeeded?(response)
103
103
 
104
- if body && body['data'].is_a?(Array) && !embedding_positions?(body['data'])
104
+ if body && body['data'].is_a?(Array) && !valid_embedding_positions?(body['data'])
105
105
  return [index, nil, batch_failure(line['custom_id'], 'Invalid or duplicate embedding record positions')]
106
106
  end
107
107
 
108
108
  if body
109
- [index, parse_mistral_batch_body(body, shape:)]
109
+ billed = body unless body['data'].is_a?(Array)
110
+ parsed_batch_result(index, custom_id, billed) { parse_mistral_batch_body(body, shape:) }
110
111
  else
111
112
  [index, nil, batch_failure(line['custom_id'], batch_error_message(line))]
112
113
  end
@@ -115,17 +116,7 @@ module RubyLLM
115
116
  def parse_mistral_batch_body(body, shape:)
116
117
  return parse_completion_body(body, raw: body) unless body['data'].is_a?(Array)
117
118
 
118
- ordered = body.merge('data' => body['data'].sort_by { |row| row['index'] })
119
- parse_embedding_response(Response.new(ordered), model: body['model'], text: shape == 'array' ? [] : nil)
120
- end
121
-
122
- # A row's index names the input its vector belongs to, so rows are put
123
- # back in that order and positions that are not exactly 0...N fail the
124
- # request rather than pairing vectors with the wrong inputs.
125
- def embedding_positions?(rows)
126
- positions = rows.map { |row| row.is_a?(Hash) ? row['index'] : nil }
127
-
128
- positions.all?(Integer) && positions.sort == (0...positions.size).to_a
119
+ parse_embedding_response(Response.new(body), model: body['model'], text: shape == 'array' ? [] : nil)
129
120
  end
130
121
  end
131
122
  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
@@ -9,17 +9,16 @@ module RubyLLM
9
9
 
10
10
  def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
11
11
  schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
12
- if thinking&.budget
13
- RubyLLM.logger.debug { "Ollama has no thinking budgets; ignoring budget #{thinking.budget}" }
14
- end
12
+ raise ArgumentError, 'Ollama has no thinking budget; use effort:' if thinking&.budget
13
+
15
14
  super
16
15
  end
17
16
 
18
- def format_content(content, attachments = [])
19
- Ollama::Media.format_content(content, attachments)
17
+ def render_content(content, attachments = [])
18
+ Ollama::Media.render_content(content, attachments)
20
19
  end
21
20
 
22
- def format_role(role)
21
+ def render_role(role)
23
22
  role.to_s
24
23
  end
25
24
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Ollama embeds text, an image, and audio together when the input is
7
+ # one object holding each as a field.
8
+ module Embeddings
9
+ EMBEDDING_MEDIA_FIELDS = %i[image audio].freeze
10
+
11
+ module_function
12
+
13
+ def embedding_media?
14
+ true
15
+ end
16
+
17
+ def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, with: [],
18
+ provider_options: {})
19
+ input = with.any? ? media_embedding_input(text, with) : text
20
+ super(input, model:, dimensions:, task_type:, title:, provider_options:)
21
+ end
22
+
23
+ def media_embedding_input(text, attachments)
24
+ raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
25
+
26
+ input = text.to_s.empty? ? {} : { text: text }
27
+ attachments.each do |attachment|
28
+ unless EMBEDDING_MEDIA_FIELDS.include?(attachment.type)
29
+ raise UnsupportedAttachmentError,
30
+ attachment.mime_type
31
+ end
32
+ raise ArgumentError, "Ollama embeds one #{attachment.type} per input" if input.key?(attachment.type)
33
+
34
+ input[attachment.type] = attachment.encoded
35
+ end
36
+ input
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end