ruby_llm 1.16.0 → 2.0.0.rc2

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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ class Conversations
7
+ module Chat # :nodoc:
8
+ module_function
9
+
10
+ def completion_url
11
+ 'conversations'
12
+ end
13
+
14
+ def render(...)
15
+ payload = super
16
+ payload[:tools] = Support::Utils.deep_stringify_keys(payload[:tools]).uniq
17
+ if payload[:tools].any? { |tool| Array(tool.dig('tool_configuration', 'requires_confirmation')).any? }
18
+ raise ArgumentError, 'Mistral hosted tool confirmations require provider conversation storage'
19
+ end
20
+
21
+ payload
22
+ end
23
+
24
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
25
+ schema: nil, thinking: nil, tool_prefs: nil, **)
26
+ options = super(messages, tools:, temperature:, model:, stream:, max_output_tokens:,
27
+ schema:, thinking:, citations: false, caching: nil, tool_prefs:)
28
+ normalize_conversation_choice(options)
29
+ {
30
+ model: model.id,
31
+ inputs: format_entries(messages),
32
+ instructions: messages.select { |message| message.role == :system }.map(&:content).join("\n\n"),
33
+ completion_args: options.slice(:temperature, :max_tokens, :response_format, :reasoning_effort,
34
+ :tool_choice),
35
+ tools: options.fetch(:tools, []),
36
+ store: false,
37
+ stream: stream
38
+ }
39
+ end
40
+
41
+ def normalize_conversation_choice(options)
42
+ choice = options[:tool_choice]
43
+ if choice.is_a?(Hash)
44
+ raise ArgumentError, 'Mistral Conversations supports :auto, :none, or :required for tool choice'
45
+ end
46
+
47
+ options[:tool_choice] = 'any' if choice == 'required'
48
+ end
49
+
50
+ def format_entries(messages)
51
+ entries = messages.reject { |message| message.role == :system }.flat_map do |message|
52
+ if message.raw_content
53
+ Array(message.raw_content)
54
+ elsif message.tool_result?
55
+ [{ type: 'function.result', tool_call_id: message.tool_call_id, result: message.content.to_s }]
56
+ else
57
+ format_conversation_message(message)
58
+ end
59
+ end
60
+ entries.each_with_index.flat_map { |entry, index| replay_conversation_entry(entry, index) }
61
+ end
62
+
63
+ def replay_conversation_entry(entry, index)
64
+ return replay_conversation_execution(entry, index) if entry['type'] == 'tool.execution'
65
+
66
+ result = entry.except('id', 'object', 'created_at', 'completed_at')
67
+ if entry['type'] == 'message.output' && entry['content'].is_a?(Array)
68
+ result['content'] = entry['content'].map do |part|
69
+ next part unless part['type'] == 'tool_reference'
70
+
71
+ { 'type' => 'text', 'text' => "[#{part['title']}](#{part['url']})" }
72
+ end
73
+ end
74
+ [result]
75
+ end
76
+
77
+ def replay_conversation_execution(entry, index)
78
+ identity = entry['id'] || "#{index}:#{JSON.generate(entry)}"
79
+ id = entry['tool_call_id'] || Digest::SHA256.hexdigest(identity)[0, 9]
80
+ info = entry['info']
81
+ result = info.is_a?(Hash) && info.key?('result') ? info['result'] : info
82
+ [
83
+ { 'type' => 'function.call', 'tool_call_id' => id,
84
+ 'name' => entry['function'] || entry['name'], 'arguments' => entry['arguments'] },
85
+ { 'type' => 'function.result', 'tool_call_id' => id,
86
+ 'result' => result.is_a?(String) ? result : JSON.generate(result) }
87
+ ]
88
+ end
89
+
90
+ def format_conversation_message(message)
91
+ entries = []
92
+ if message.content || message.attachments.any?
93
+ entries << {
94
+ type: 'message.input', role: message.role.to_s,
95
+ content: format_message_content(message)
96
+ }
97
+ end
98
+ message.tool_calls&.each_value do |call|
99
+ entries << { type: 'function.call', tool_call_id: call.id, name: call.name,
100
+ arguments: JSON.generate(call.arguments) }
101
+ end
102
+ entries
103
+ end
104
+
105
+ def parse_completion_body(data, raw:)
106
+ output = data.fetch('outputs')
107
+ content = parse_conversation_content(output)
108
+ calls = parse_conversation_calls(output, raw:)
109
+ response_model = output.filter_map { |entry| entry['model'] }.last || @model&.id
110
+ Message.new(
111
+ role: :assistant, content: content[:text], attachments: content[:attachments],
112
+ citations: content[:citations], thinking: Thinking.build(text: content[:thinking]),
113
+ tool_calls: calls, server_tool_calls: parse_conversation_steps(output),
114
+ raw_content: output, model: response_model,
115
+ finish_reason: calls.empty? ? :stop : :tool_calls, raw: raw,
116
+ **parse_conversation_usage(data['usage'] || {})
117
+ )
118
+ end
119
+
120
+ def parse_conversation_calls(output, raw:)
121
+ if output.any? { |entry| entry['type'] == 'function.call' && entry['confirmation_status'] == 'pending' }
122
+ raise Error.new('Mistral returned a hosted tool confirmation that requires provider conversation storage',
123
+ response: raw)
124
+ end
125
+
126
+ output.select { |entry| pending_conversation_call?(entry) }.to_h do |entry|
127
+ call = parse_tool_calls([
128
+ { 'id' => entry['tool_call_id'], 'type' => 'function',
129
+ 'function' => entry.slice('name', 'arguments') }
130
+ ], response: raw, finish_reason: :tool_calls).values.first
131
+ [call.id, call]
132
+ end
133
+ end
134
+
135
+ def pending_conversation_call?(entry)
136
+ entry['type'] == 'function.call' && entry['confirmation_status'].nil?
137
+ end
138
+
139
+ def parse_conversation_steps(output)
140
+ output.filter_map do |entry|
141
+ next unless entry['type'] == 'tool.execution' ||
142
+ (entry['type'] == 'function.call' && !pending_conversation_call?(entry))
143
+
144
+ ServerToolCall.new(type: entry['type'], name: entry['name'], id: entry['id'],
145
+ input: entry['arguments'], result: entry['info'], raw: entry)
146
+ end
147
+ end
148
+
149
+ def parse_conversation_usage(usage)
150
+ input = usage['prompt_tokens'] && (usage['prompt_tokens'] + usage.fetch('connector_tokens', 0).to_i)
151
+ {
152
+ input_tokens: input,
153
+ output_tokens: usage['completion_tokens'], server_tool_use: usage['connectors']
154
+ }
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ class Conversations
7
+ module Images # :nodoc:
8
+ def images_url(**)
9
+ 'conversations'
10
+ end
11
+
12
+ def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
13
+ if size || (count && count != 1)
14
+ raise ArgumentError,
15
+ 'Mistral image generation does not accept size or count options'
16
+ end
17
+ raise UnsupportedAttachmentError, 'image editing' if with || mask
18
+
19
+ payload = { model: model, store: false, inputs: prompt, tools: [{ type: 'image_generation' }] }
20
+ Support::Utils.deep_merge(payload, provider_options)
21
+ end
22
+
23
+ def parse_image_response(response, model:)
24
+ parse_image_responses(response, model:).first
25
+ end
26
+
27
+ def parse_image_responses(response, model:)
28
+ data = response.body
29
+ attachments = parse_conversation_content(data.fetch('outputs'))[:attachments]
30
+ raise Error.new('Mistral returned no generated image', response:) if attachments.empty?
31
+
32
+ usage = parse_conversation_usage(data['usage'] || {}).transform_keys(&:to_s)
33
+ attachments.each_with_index.map do |attachment, index|
34
+ bytes = @provider.download_file(attachment.provider_file_id)
35
+ Image.new(data: Base64.strict_encode64(bytes), mime_type: RubyLLM::Files::MimeType.for(StringIO.new(bytes)),
36
+ model: model, usage: index.zero? ? usage : {})
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ class Conversations
7
+ module Streaming # :nodoc:
8
+ module_function
9
+
10
+ def stream_response(payload, additional_headers = {})
11
+ @conversation_output = {}
12
+ @conversation_response = { 'object' => 'conversation.response' }
13
+ @conversation_done = false
14
+ response = stream_events(completion_url, payload, additional_headers) { |data| yield build_chunk(data) }
15
+ raise Error.new('Mistral conversation stream ended before completion', response:) unless @conversation_done
16
+
17
+ parse_completion_body(streamed_conversation_response, raw: response)
18
+ end
19
+
20
+ def build_chunk(data)
21
+ @conversation_output ||= {}
22
+ @conversation_response ||= { 'object' => 'conversation.response' }
23
+ case data['type']
24
+ when 'conversation.response.done'
25
+ @conversation_done = true
26
+ @conversation_response['usage'] = data['usage']
27
+ return final_conversation_chunk
28
+ when 'conversation.response.error'
29
+ raise Error, data['message'] || data.dig('error', 'message') || 'Mistral conversation failed'
30
+ when 'message.output.delta'
31
+ return conversation_text_chunk(data)
32
+ when 'function.call.delta', 'tool.execution.started', 'tool.execution.delta', 'tool.execution.done'
33
+ accumulate_conversation_entry(data)
34
+ end
35
+ Chunk.new(role: :assistant, content: nil)
36
+ end
37
+
38
+ def streamed_conversation_response
39
+ @conversation_response.merge('outputs' => @conversation_output.sort.map(&:last))
40
+ end
41
+
42
+ def accumulate_conversation_entry(data)
43
+ type = data['type'].delete_suffix('.delta').delete_suffix('.started').delete_suffix('.done')
44
+ entry = @conversation_output[data.fetch('output_index', 0)] ||= { 'type' => type, 'arguments' => +'' }
45
+ entry.merge!(data.slice('id', 'model', 'name', 'tool_call_id', 'confirmation_status', 'function', 'info'))
46
+ entry['arguments'] << data['arguments'].to_s
47
+ end
48
+
49
+ def conversation_text_chunk(data)
50
+ entry = @conversation_output[data.fetch('output_index',
51
+ 0)] ||= { 'type' => 'message.output', 'content' => [] }
52
+ entry.merge!(data.slice('id', 'model', 'role'))
53
+ part = data['content'].is_a?(String) ? { 'type' => 'text', 'text' => data['content'] } : data['content']
54
+ merge_conversation_part(entry['content'], data.fetch('content_index', 0), part)
55
+ content = { text: +'', thinking: +'', attachments: [], citations: [] }
56
+ parse_conversation_parts([part], content)
57
+ Chunk.new(role: :assistant, content: content[:text], model: data['model'],
58
+ thinking: Thinking.build(text: content[:thinking].empty? ? nil : content[:thinking]))
59
+ end
60
+
61
+ def merge_conversation_part(parts, index, part)
62
+ existing = parts[index]
63
+ if existing && part['type'] == 'text'
64
+ existing['text'] << part['text'].to_s
65
+ elsif existing && part['type'] == 'thinking'
66
+ existing['thinking'].concat(Array(part['thinking']))
67
+ else
68
+ parts[index] = Support::Utils.deep_dup(part)
69
+ end
70
+ end
71
+
72
+ def final_conversation_chunk
73
+ message = parse_completion_body(streamed_conversation_response, raw: nil)
74
+ Chunk.new(role: :assistant, content: nil, model: message.model, tokens: message.tokens,
75
+ citations: message.citations, tool_calls: message.tool_calls,
76
+ server_tool_calls: message.server_tool_calls, raw_content: message.raw_content,
77
+ attachments: message.attachments, finish_reason: message.finish_reason)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ # Mistral's Conversations API, including provider-executed tools.
7
+ class Conversations < ChatCompletions
8
+ include Mistral::Content
9
+ include Conversations::Chat
10
+ include Conversations::Streaming
11
+ include Conversations::Images
12
+
13
+ public :render
14
+
15
+ SERVER_TOOL_ALIASES = {
16
+ web_search: { tool: { type: 'web_search' } },
17
+ web_fetch: { tool: { type: 'web_search' } },
18
+ code_execution: { tool: { type: 'code_interpreter' } },
19
+ file_search: { tool: { type: 'document_library' } },
20
+ image_generation: { tool: { type: 'image_generation' } },
21
+ mcp: { tool: { type: 'connector' } }
22
+ }.freeze
23
+
24
+ def server_tool_aliases
25
+ SERVER_TOOL_ALIASES
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ # Mistral Files API.
7
+ class Files < Protocols::Files
8
+ private
9
+
10
+ # rubocop:disable-next Lint/UnusedMethodArgument
11
+ def render_upload_payload(attachment, purpose: nil, expires_in: nil, visibility: nil,
12
+ display_name: nil, uri: nil, content_type: nil)
13
+ multipart_payload(attachment, purpose:, expiry: expiry_hours(expires_in), visibility:)
14
+ end
15
+
16
+ def expiry_hours(expires_in)
17
+ expires_in&.fdiv(3600)&.ceil
18
+ end
19
+
20
+ def parse_file_response(data)
21
+ uploaded_file(
22
+ data,
23
+ id: data['id'],
24
+ filename: data['filename'],
25
+ byte_size: data['bytes'],
26
+ created_at: timestamp(data['created_at']),
27
+ expires_at: timestamp(data['expires_at']),
28
+ mime_type: data['mimetype'],
29
+ purpose: data['purpose'],
30
+ status: data['deleted'] ? 'deleted' : nil
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module Mistral
6
+ module MultiCompletion # :nodoc:
7
+ include Mistral::Content
8
+
9
+ SERVER_TOOL_ALIASES = {
10
+ image_generation: { tool: { type: 'image_generation' } },
11
+ mcp: { tool: { type: 'connector' } }
12
+ }.freeze
13
+
14
+ module_function
15
+
16
+ def server_tool_aliases
17
+ SERVER_TOOL_ALIASES
18
+ end
19
+
20
+ def format_message_group(group, **options)
21
+ content = group.first.raw_content
22
+ return super unless group.one? && content.is_a?(Array) &&
23
+ content.all? { |entry| entry.is_a?(Hash) && entry['role'] }
24
+
25
+ content.map { |entry| entry.except('index') }
26
+ end
27
+
28
+ def parse_completion_body(data, raw:)
29
+ messages = data.dig('choices', 0, 'messages')
30
+ return super unless messages.is_a?(Array)
31
+
32
+ parse_multi_message(data, messages, raw:)
33
+ end
34
+
35
+ def multi_content(messages)
36
+ output = messages.filter_map do |message|
37
+ message.merge('type' => 'message.output') if message['role'] == 'assistant'
38
+ end
39
+ parse_conversation_content(output)
40
+ end
41
+
42
+ def multi_pending_calls(calls, results, raw:)
43
+ pending = calls.reject { |call| results.key?(call['id']) }
44
+ if pending.any? { |call| call.dig('metadata', 'tool_type') || call.dig('metadata', 'integration_id') }
45
+ raise Error.new('Mistral returned an unfinished hosted tool call on Chat Completions', response: raw)
46
+ end
47
+
48
+ pending
49
+ end
50
+
51
+ def parse_multi_message(data, messages, raw:)
52
+ content = multi_content(messages)
53
+ results = messages.filter_map do |message|
54
+ [message['tool_call_id'], message] if message['role'] == 'tool'
55
+ end.to_h
56
+ calls = messages.flat_map { |message| Array(message['tool_calls']) }
57
+ pending = multi_pending_calls(calls, results, raw:)
58
+ usage = data['usage'] || {}
59
+ Message.new(role: :assistant, content: content[:text], thinking: Thinking.build(text: content[:thinking]),
60
+ attachments: content[:attachments], citations: content[:citations],
61
+ tool_calls: parse_tool_calls(pending, response: raw, finish_reason: :tool_calls),
62
+ server_tool_calls: parse_multi_steps(calls, results), raw_content: messages,
63
+ input_tokens: input_tokens(usage), output_tokens: output_tokens(usage),
64
+ cache_read_tokens: cache_read_tokens(usage), model: data['model'], raw: raw,
65
+ finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason')))
66
+ end
67
+
68
+ def parse_multi_steps(calls, results)
69
+ calls.filter_map do |call|
70
+ result = results[call['id']]
71
+ next unless result
72
+
73
+ ServerToolCall.new(type: result['role'], id: call['id'], name: call.dig('function', 'name'),
74
+ input: call.dig('function', 'arguments'), result: result['content'], raw: result)
75
+ end
76
+ end
77
+
78
+ def stream_response(payload, additional_headers = {}, &block)
79
+ return super unless Array(payload[:tools]).any? { |tool| (tool[:type] || tool['type']) != 'function' }
80
+
81
+ @multi_messages = {}
82
+ @multi_usage = {}
83
+ @multi_finish_reason = nil
84
+ response = stream_events(completion_url, payload, additional_headers) do |data|
85
+ block.call(build_multi_chunk(data))
86
+ end
87
+ raise Error.new('Mistral tool stream ended before completion', response:) unless @multi_finish_reason
88
+
89
+ message = parse_completion_body(multi_response, raw: response)
90
+ block.call(Chunk.new(role: :assistant, content: nil, tokens: message.tokens, model: message.model,
91
+ citations: message.citations, server_tool_calls: message.server_tool_calls,
92
+ tool_calls: message.tool_calls, raw_content: message.raw_content,
93
+ attachments: message.attachments, finish_reason: message.finish_reason))
94
+ message
95
+ end
96
+
97
+ def build_multi_chunk(data)
98
+ @multi_model = data['model']
99
+ @multi_usage[data['id']] = data['usage'] if data['usage']
100
+ choice = data.dig('choices', 0) || {}
101
+ delta = choice['delta'] || {}
102
+ index = delta.fetch('index', 0)
103
+ @multi_finish_reason = nil unless @multi_messages.key?(index)
104
+ entry = @multi_messages[index] ||= { 'content' => [], 'tool_calls' => [] }
105
+ entry.merge!(delta.slice('role', 'tool_call_id', 'metadata'))
106
+ append_multi_content(entry, delta['content'])
107
+ append_multi_calls(entry, delta['tool_calls'])
108
+ @multi_finish_reason = choice['finish_reason'] if choice['finish_reason']
109
+ content, thinking = extract_content_and_thinking(delta['content']) if entry['role'] == 'assistant'
110
+ Chunk.new(role: :assistant, content: content, thinking: Thinking.build(text: thinking), model: data['model'])
111
+ end
112
+
113
+ def append_multi_content(entry, content)
114
+ return if content.nil?
115
+
116
+ if content.is_a?(String)
117
+ entry['content'] << { 'type' => 'text', 'text' => content }
118
+ else
119
+ entry['content'].concat(content)
120
+ end
121
+ end
122
+
123
+ def append_multi_calls(entry, calls)
124
+ Array(calls).each do |call|
125
+ target = entry['tool_calls'][call.fetch('index', 0)] ||= { 'function' => { 'arguments' => +'' } }
126
+ target.merge!(call.slice('id', 'type', 'metadata'))
127
+ target['function']['name'] = call.dig('function', 'name') if call.dig('function', 'name')
128
+ target['function']['arguments'] << call.dig('function', 'arguments').to_s
129
+ end
130
+ end
131
+
132
+ def multi_response
133
+ { 'model' => @multi_model, 'usage' => multi_usage,
134
+ 'choices' => [{ 'messages' => multi_messages, 'finish_reason' => @multi_finish_reason }] }
135
+ end
136
+
137
+ def multi_usage
138
+ usages = @multi_usage.values
139
+ usage = %w[prompt_tokens completion_tokens total_tokens].to_h do |key|
140
+ [key, usages.sum { |value| value[key].to_i }]
141
+ end
142
+ usage['prompt_tokens_details'] = { 'cached_tokens' => usages.sum do |value|
143
+ value.dig('prompt_tokens_details', 'cached_tokens').to_i
144
+ end }
145
+ usage
146
+ end
147
+
148
+ def multi_messages
149
+ @multi_messages.sort.map do |_, message|
150
+ message = message.reject { |key, value| key == 'tool_calls' && value.empty? }
151
+ if message['role'] == 'tool' && message['content'].all? { |part| part['type'] == 'text' }
152
+ message['content'] = message['content'].map { |part| part['text'] }.join
153
+ end
154
+ message
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ module OpenAI
8
+ # The OpenAI file-backed Batch API: upload JSONL, create /batches,
9
+ # then read output/error files back in custom_id order.
10
+ module Batches
11
+ include RubyLLM::Batch::Helpers
12
+
13
+ TERMINAL_STATUSES = %w[completed failed expired cancelled].freeze
14
+ private_constant :TERMINAL_STATUSES
15
+
16
+ def create_batch(requests)
17
+ single_batch_model!(requests, @provider.slug)
18
+ validate_batch_requests!(requests)
19
+
20
+ response = @connection.post(batch_create_url, {
21
+ input_file_id: upload_batch_file(requests),
22
+ endpoint: batch_endpoint,
23
+ completion_window: '24h'
24
+ }, idempotent: false)
25
+
26
+ parse_batch_response(response.body)
27
+ end
28
+
29
+ def find_batch(id)
30
+ parse_batch_response @connection.get(batch_url(id)).body
31
+ end
32
+
33
+ def cancel_batch(id)
34
+ parse_batch_response @connection.post("#{batch_url(id)}/cancel", {}).body
35
+ end
36
+
37
+ # Results are JSONL and may arrive out of order. Each line carries the
38
+ # custom_id we assigned from the submission index.
39
+ def batch_results(id)
40
+ batch = @connection.get(batch_url(id)).body
41
+ %w[output_file_id error_file_id].flat_map do |key|
42
+ file_id = batch[key]
43
+ next [] unless file_id && !file_id.to_s.empty?
44
+
45
+ download_batch_file(file_id).to_s.each_line.filter_map { |line| parse_batch_result(JSON.parse(line)) }
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def upload_batch_file(requests)
52
+ @provider.upload_file(
53
+ StringIO.new(batch_jsonl(requests)),
54
+ purpose: 'batch',
55
+ filename: 'ruby_llm_batch.jsonl'
56
+ ).id
57
+ end
58
+
59
+ def download_batch_file(file_id)
60
+ @provider.download_file(file_id)
61
+ end
62
+
63
+ def batch_jsonl(requests)
64
+ requests.map { |request| JSON.generate(batch_request_line(request)) }.join("\n")
65
+ end
66
+
67
+ def batch_request_line(request)
68
+ {
69
+ custom_id: request[:custom_id],
70
+ method: 'POST',
71
+ url: batch_endpoint,
72
+ body: batch_payload(request)
73
+ }
74
+ end
75
+
76
+ def validate_batch_requests!(_requests); end
77
+
78
+ def batch_endpoint
79
+ raise NotImplementedError
80
+ end
81
+
82
+ def batch_create_url
83
+ 'batches'
84
+ end
85
+
86
+ def batch_url(id)
87
+ "batches/#{id}"
88
+ end
89
+
90
+ def parse_batch_response(data)
91
+ request_counts = data['request_counts']
92
+
93
+ {
94
+ id: data['id'],
95
+ raw_status: data['status'],
96
+ completed: TERMINAL_STATUSES.include?(data['status']),
97
+ request_counts:,
98
+ request_count: request_counts&.fetch('total', nil),
99
+ endpoint: data['endpoint']
100
+ }.compact
101
+ end
102
+
103
+ def parse_batch_status(raw_status, completed:)
104
+ return :pending unless completed
105
+
106
+ case raw_status
107
+ when 'completed' then :succeeded
108
+ when 'cancelled' then :cancelled
109
+ else :failed
110
+ end
111
+ end
112
+
113
+ def parse_batch_result(line)
114
+ index = batch_result_index(line['custom_id'])
115
+ response = line['response']
116
+
117
+ if response && response['status_code'].to_i.between?(200, 299)
118
+ [index, parse_batch_completion_response(response['body'])]
119
+ else
120
+ [index, nil, batch_failure(line['custom_id'], batch_error_message(line))]
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ module OpenAI
6
+ # OpenAI Files API.
7
+ class Files < Protocols::Files
8
+ UPLOAD_PURPOSES = %w[assistants batch fine-tune vision user_data evals].freeze
9
+
10
+ private
11
+
12
+ # rubocop:disable-next Lint/UnusedMethodArgument
13
+ def render_upload_payload(attachment, purpose: nil, expires_in: nil, visibility: nil,
14
+ display_name: nil, uri: nil, content_type: nil)
15
+ unless purpose
16
+ raise ArgumentError, "#{@provider.name} file uploads require purpose: " \
17
+ "#{UPLOAD_PURPOSES.join(', ')}"
18
+ end
19
+
20
+ multipart_payload(attachment, purpose:, expires_after: expires_after(expires_in))
21
+ end
22
+
23
+ def expires_after(expires_in)
24
+ { anchor: 'created_at', seconds: expires_in } if expires_in
25
+ end
26
+
27
+ def parse_file_response(data)
28
+ uploaded_file(
29
+ data,
30
+ id: data['id'],
31
+ filename: data['filename'],
32
+ byte_size: data['bytes'],
33
+ created_at: timestamp(data['created_at']),
34
+ expires_at: timestamp(data['expires_at']),
35
+ status: data['status'],
36
+ purpose: data['purpose']
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end