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,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ class ChatCompletions
7
+ # Mistral batch jobs for chat completions and embeddings.
8
+ module Batches
9
+ include RubyLLM::Batch::Helpers
10
+
11
+ TERMINAL_STATUSES = %w[SUCCESS FAILED TIMEOUT_EXCEEDED CANCELLED].freeze
12
+ Response = Struct.new(:body)
13
+ private_constant :TERMINAL_STATUSES, :Response
14
+
15
+ def create_batch(requests)
16
+ model = single_batch_model!(requests, 'mistral')
17
+ response = @connection.post('batch/jobs', {
18
+ endpoint: mistral_batch_endpoint(requests),
19
+ model: model,
20
+ requests: requests.map { |request| mistral_batch_request(request) }
21
+ }, idempotent: false)
22
+
23
+ parse_batch_response(response.body)
24
+ end
25
+
26
+ def find_batch(id)
27
+ attempts = 0
28
+ begin
29
+ parse_batch_response @connection.get(batch_url(id)).body
30
+ rescue Error => e
31
+ attempts += 1
32
+ raise unless e.response&.status == 404 && attempts < 3
33
+
34
+ sleep(0.5 * attempts)
35
+ retry
36
+ end
37
+ end
38
+
39
+ def cancel_batch(id)
40
+ parse_batch_response @connection.post("#{batch_url(id)}/cancel", {}).body
41
+ end
42
+
43
+ def batch_results(id)
44
+ response = @connection.get(batch_url(id)) { |request| request.params[:inline] = true }
45
+ Array(response.body['outputs']).filter_map { |line| parse_batch_result(line) }
46
+ end
47
+
48
+ private
49
+
50
+ def batch_url(id)
51
+ "batch/jobs/#{id}"
52
+ end
53
+
54
+ def mistral_batch_request(request)
55
+ body = batch_payload(request, except: :model)
56
+ custom_id = request[:custom_id]
57
+ custom_id = "#{custom_id}:array" if (body[:input] || body['input']).is_a?(Array)
58
+
59
+ {
60
+ custom_id: custom_id,
61
+ body: body
62
+ }
63
+ end
64
+
65
+ def mistral_batch_endpoint(requests)
66
+ endpoints = requests.map do |request|
67
+ payload = request.fetch(:payload)
68
+ payload.key?(:input) || payload.key?('input') ? '/v1/embeddings' : '/v1/chat/completions'
69
+ end.uniq
70
+ return endpoints.first if endpoints.one?
71
+
72
+ raise Error, 'Mistral batches cannot mix chat and embedding requests'
73
+ end
74
+
75
+ def parse_batch_response(data)
76
+ {
77
+ id: data['id'],
78
+ raw_status: data['status'],
79
+ completed: TERMINAL_STATUSES.include?(data['status']),
80
+ request_count: data['total_requests'],
81
+ request_counts: {
82
+ 'total' => data['total_requests'],
83
+ 'completed' => data['completed_requests'],
84
+ 'succeeded' => data['succeeded_requests'],
85
+ 'failed' => data['failed_requests']
86
+ }.compact
87
+ }
88
+ end
89
+
90
+ def parse_batch_status(raw_status, completed:)
91
+ return :pending unless completed
92
+ return :succeeded if raw_status == 'SUCCESS'
93
+ return :cancelled if raw_status == 'CANCELLED'
94
+
95
+ :failed
96
+ end
97
+
98
+ def parse_batch_result(line)
99
+ custom_id, shape = line['custom_id'].split(':', 2)
100
+ index = batch_result_index(custom_id)
101
+ response = line['response']
102
+
103
+ if response && response['body']
104
+ body = response['body']
105
+ [index, parse_mistral_batch_body(body, shape:)]
106
+ else
107
+ [index, nil, batch_failure(line['custom_id'], batch_error_message(line))]
108
+ end
109
+ end
110
+
111
+ def parse_mistral_batch_body(body, shape:)
112
+ return parse_completion_body(body, raw: body) unless body['data'].is_a?(Array)
113
+
114
+ parse_embedding_response(Response.new(body), model: body['model'], text: shape == 'array' ? [] : nil)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Mistral's dialect of the Chat Completions API.
7
+ class ChatCompletions < Protocols::ChatCompletions
8
+ include Mistral::Chat
9
+ include Mistral::Embeddings
10
+ include Mistral::Media
11
+ include Mistral::Models
12
+ include Mistral::OCR
13
+ include Mistral::Speech
14
+ include Mistral::Transcription
15
+ include Protocols::Mistral::MultiCompletion
16
+
17
+ public :server_tool_aliases
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Mistral Conversations with its document and image attachment formats.
7
+ class Conversations < Protocols::Mistral::Conversations
8
+ include Mistral::Media
9
+ end
10
+ end
11
+ end
12
+ end
@@ -11,16 +11,18 @@ module RubyLLM
11
11
  'embeddings'
12
12
  end
13
13
 
14
- def render_embedding_payload(text, model:, dimensions:) # rubocop:disable Lint/UnusedMethodArgument
14
+ # rubocop:disable-next Lint/UnusedMethodArgument
15
+ def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
15
16
  {
16
17
  model: model,
17
- input: text
18
- }
18
+ input: text,
19
+ output_dimension: dimensions
20
+ }.compact.merge(provider_options)
19
21
  end
20
22
 
21
23
  def parse_embedding_response(response, model:, text:)
22
24
  data = response.body
23
- input_tokens = data.dig('usage', 'prompt_tokens') || 0
25
+ input_tokens = data.dig('usage', 'prompt_tokens')
24
26
  vectors = data['data'].map { |d| d['embedding'] }
25
27
 
26
28
  vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
@@ -7,33 +7,21 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
- if content.is_a?(RubyLLM::Content::Raw)
12
- value = content.value
13
- return value.is_a?(Hash) ? value.to_json : value
14
- end
15
- return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
16
- return content unless content.is_a?(Content)
17
-
18
- parts = []
19
- parts << OpenAI::Media.format_text(content.text) if content.text
20
-
21
- content.attachments.each do |attachment|
10
+ def format_content(content, attachments = [])
11
+ Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
22
12
  case attachment.type
23
13
  when :image
24
- parts << format_image(attachment)
14
+ format_image(attachment)
25
15
  when :audio
26
- parts << OpenAI::Media.format_audio(attachment)
16
+ Protocols::ChatCompletions::Media.format_audio(attachment)
27
17
  when :pdf, :document
28
- parts << format_document(attachment)
18
+ format_document(attachment)
29
19
  when :text
30
- parts << OpenAI::Media.format_text_file(attachment)
20
+ Protocols::ChatCompletions::Media.format_text_file(attachment)
31
21
  else
32
22
  raise UnsupportedAttachmentError, attachment.mime_type
33
23
  end
34
24
  end
35
-
36
- parts
37
25
  end
38
26
 
39
27
  def format_image(image)
@@ -1,49 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
4
-
5
3
  module RubyLLM
6
4
  module Providers
7
5
  class Mistral
8
6
  # Model information for Mistral
9
7
  module Models
8
+ # Mistral's capability flags, named as Models::Schema::CAPABILITIES names.
9
+ CAPABILITY_FLAGS = {
10
+ 'function_calling' => 'function_calling',
11
+ 'reasoning' => 'reasoning',
12
+ 'fine_tuning' => 'fine_tuning',
13
+ 'vision' => 'vision',
14
+ 'ocr' => 'vision',
15
+ 'moderation' => 'moderation',
16
+ 'audio_transcription' => 'transcription',
17
+ 'audio_transcription_realtime' => 'realtime',
18
+ 'audio_speech' => 'speech_generation'
19
+ }.freeze
20
+
10
21
  module_function
11
22
 
12
23
  def models_url
13
24
  'models'
14
25
  end
15
26
 
16
- def headers(config)
17
- {
18
- 'Authorization' => "Bearer #{config.mistral_api_key}"
19
- }
27
+ def models_dev_alias(model_id, models_dev_by_key, provider_model = nil)
28
+ source = Array(provider_model&.metadata&.[](:aliases)).filter_map do |alias_id|
29
+ models_dev_by_key["mistral:#{alias_id}"]
30
+ end.first
31
+ Model.new(source.to_h.merge(id: model_id)) if source
20
32
  end
21
33
 
22
- def parse_list_models_response(response, slug, capabilities)
34
+ def parse_list_models_response(response, slug)
23
35
  Array(response.body['data']).map do |model_data|
24
36
  model_id = model_data['id']
37
+ flags = model_data['capabilities'] || {}
25
38
 
26
- release_date = capabilities.release_date_for(model_id)
27
- created_at = release_date ? Time.parse(release_date) : nil
28
-
29
- Model::Info.new(
39
+ Model.new(
30
40
  id: model_id,
31
- name: capabilities.format_display_name(model_id),
41
+ name: model_id,
32
42
  provider: slug,
33
- family: capabilities.model_family(model_id),
34
- created_at: created_at,
35
- context_window: capabilities.context_window_for(model_id),
36
- max_output_tokens: capabilities.max_tokens_for(model_id),
37
- modalities: capabilities.modalities_for(model_id),
38
- capabilities: capabilities.capabilities_for(model_id),
39
- pricing: capabilities.pricing_for(model_id),
40
- metadata: {
41
- object: model_data['object'],
42
- owned_by: model_data['owned_by']
43
- }
43
+ context_window: model_data['max_context_length'],
44
+ modalities: modalities_from(flags, model_data),
45
+ capabilities: capabilities_from(flags),
46
+ metadata: metadata_from(model_data)
44
47
  )
45
48
  end
46
49
  end
50
+
51
+ def capabilities_from(flags)
52
+ CAPABILITY_FLAGS.filter_map { |flag, capability| capability if flags[flag] }
53
+ end
54
+
55
+ def modalities_from(flags, model_data)
56
+ return { input: ['text'], output: ['embeddings'] } if embedding_model?(model_data)
57
+ return { input: ['audio'], output: ['text'] } if flags['audio_transcription']
58
+
59
+ input = ['text']
60
+ input << 'image' if flags['vision'] || flags['ocr']
61
+ output = ['text']
62
+ output << 'audio' if flags['audio_speech']
63
+ { input: input, output: output }
64
+ end
65
+
66
+ def embedding_model?(model_data)
67
+ values = [model_data['id'], model_data['description'], *Array(model_data['aliases'])]
68
+ values.any? { |value| value.to_s.match?(/(?:\A|[-_ ])embed(?:ding)?(?:\z|[-_ ])/i) }
69
+ end
70
+
71
+ def metadata_from(model_data)
72
+ {
73
+ object: model_data['object'],
74
+ owned_by: model_data['owned_by'],
75
+ description: model_data['description'],
76
+ aliases: model_data['aliases'],
77
+ deprecation: model_data['deprecation'],
78
+ deprecation_replacement_model: model_data['deprecation_replacement_model']
79
+ }.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
80
+ end
47
81
  end
48
82
  end
49
83
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Document OCR methods for the Mistral API. Remote files are passed as
7
+ # URLs; local files are inlined as base64 data URIs. Images go through
8
+ # the image_url document variant, everything else through document_url.
9
+ module OCR
10
+ module_function
11
+
12
+ def ocr_url
13
+ 'ocr'
14
+ end
15
+
16
+ def render_ocr_payload(file, model:, pages: nil, provider_options: {})
17
+ attachment = file.is_a?(Attachment) ? file : Attachment.new(file, config: @config)
18
+
19
+ payload = { model: model, document: ocr_document_part(attachment) }
20
+ payload[:pages] = pages if pages
21
+ payload.merge(provider_options)
22
+ end
23
+
24
+ def ocr_document_part(attachment)
25
+ reference = attachment.url? ? attachment.source.to_s : attachment.for_llm
26
+
27
+ if attachment.image?
28
+ { type: 'image_url', image_url: reference }
29
+ else
30
+ { type: 'document_url', document_url: reference }
31
+ end
32
+ end
33
+
34
+ def parse_ocr_response(response, model:)
35
+ data = response.body
36
+
37
+ RubyLLM::OCR.new(
38
+ pages: data['pages'],
39
+ model: data['model'] || model,
40
+ usage: data['usage_info'],
41
+ raw: data
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Speech dialect for the Mistral API, which requires a voice from its own
7
+ # catalog and returns JSON with base64 audio instead of raw bytes.
8
+ module Speech
9
+ module_function
10
+
11
+ def stream_speech(payload, model:, voice:, format:)
12
+ audio = +''.b
13
+ final = nil
14
+ stream_events(speech_url(model:), payload.merge(stream: true)) do |event|
15
+ case event['type']
16
+ when 'speech.audio.delta'
17
+ data = Base64.strict_decode64(event.fetch('audio_data'))
18
+ audio << data
19
+ yield SpeechChunk.new(data:, format: format || 'mp3') unless data.empty?
20
+ when 'speech.audio.done'
21
+ final = event
22
+ end
23
+ end
24
+ raise Error, 'Mistral speech stream ended before its completion event' unless final
25
+
26
+ usage = final['usage'] || {}
27
+ RubyLLM::Speech.new(data: audio, model:, voice:, format: format || 'mp3',
28
+ input_tokens: usage['prompt_tokens'], output_tokens: usage['completion_tokens'])
29
+ end
30
+
31
+ def render_speech_payload(input, model:, voice:, format:, provider_options: {})
32
+ {
33
+ model: model,
34
+ input: input,
35
+ voice_id: voice,
36
+ response_format: format
37
+ }.compact.merge(provider_options)
38
+ end
39
+
40
+ def parse_speech_response(response, model:, voice:, format:)
41
+ RubyLLM::Speech.new(
42
+ data: Base64.decode64(response.body['audio_data'].to_s),
43
+ model: model,
44
+ voice: voice,
45
+ format: (format || 'mp3').to_s
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Transcription dialect for the Mistral audio API. Giving speaker
7
+ # names turns on diarization, which labels each segment with a
8
+ # speaker id; a prompt rides along as context biasing terms.
9
+ module Transcription
10
+ STREAM_TYPES = {
11
+ 'transcription.text.delta' => RubyLLM::TranscriptionChunk::DELTA,
12
+ 'transcription.segment' => RubyLLM::TranscriptionChunk::SEGMENT,
13
+ 'transcription.done' => RubyLLM::TranscriptionChunk::DONE
14
+ }.freeze
15
+
16
+ def render_transcription_options(timestamps:, **)
17
+ return {} if timestamps.nil?
18
+ raise ArgumentError, 'Mistral transcription timestamps must be segment' unless timestamps == :segment
19
+
20
+ { timestamp_granularities: ['segment'] }
21
+ end
22
+
23
+ module_function
24
+
25
+ def build_transcription_chunk(data)
26
+ type = STREAM_TYPES[data['type']]
27
+ return super unless type
28
+
29
+ RubyLLM::TranscriptionChunk.new(
30
+ type: type,
31
+ delta: (data['text'] if type == RubyLLM::TranscriptionChunk::DELTA),
32
+ text: (data['text'] if type == RubyLLM::TranscriptionChunk::DONE),
33
+ segment: (data.except('type') if type == RubyLLM::TranscriptionChunk::SEGMENT),
34
+ raw: data
35
+ )
36
+ end
37
+
38
+ def transcription_duration(usage)
39
+ usage['prompt_audio_seconds'] || super
40
+ end
41
+
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
+ payload = {
47
+ model: model,
48
+ file: file_part,
49
+ language: language,
50
+ temperature: temperature,
51
+ context_bias: prompt ? Array(prompt) : nil
52
+ }.compact
53
+ if speaker_names
54
+ payload[:diarize] = true
55
+ payload[:timestamp_granularities] = ['segment']
56
+ end
57
+ payload.merge(provider_options)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,10 +3,16 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # Mistral API integration.
6
- class Mistral < OpenAI
7
- include Mistral::Chat
8
- include Mistral::Models
9
- include Mistral::Embeddings
6
+ class Mistral < Provider
7
+ protocol :chat_completions, ChatCompletions, batches: Mistral::ChatCompletions::Batches
8
+ protocol :conversations, Conversations
9
+ protocol :files, Protocols::Mistral::Files
10
+
11
+ def protocol_for(model, operation: nil, **)
12
+ return Conversations if operation == :paint
13
+
14
+ super
15
+ end
10
16
 
11
17
  def api_base
12
18
  @config.mistral_api_base || 'https://api.mistral.ai/v1'
@@ -18,11 +24,17 @@ module RubyLLM
18
24
  }
19
25
  end
20
26
 
27
+ def batch_cost_multiplier(**) = 0.5
28
+
21
29
  class << self
22
30
  def capabilities
23
31
  Mistral::Capabilities
24
32
  end
25
33
 
34
+ def models_dev_alias(...)
35
+ Mistral::Models.models_dev_alias(...)
36
+ end
37
+
26
38
  def configuration_options
27
39
  %i[mistral_api_key mistral_api_base]
28
40
  end
@@ -7,22 +7,16 @@ module RubyLLM
7
7
  module Chat
8
8
  module_function
9
9
 
10
- def format_messages(messages)
11
- messages.map do |msg|
12
- {
13
- role: format_role(msg.role),
14
- content: format_message_content(msg),
15
- tool_calls: format_tool_calls(msg.tool_calls),
16
- tool_call_id: msg.tool_call_id
17
- }.compact.merge(OpenAI::Chat.format_thinking(msg))
10
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
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}" }
18
14
  end
15
+ super
19
16
  end
20
17
 
21
- def format_message_content(msg)
22
- content = Ollama::Media.format_content(msg.content)
23
- return '' if content.nil? && OpenAI::Chat.thinking_only_assistant_message?(msg)
24
-
25
- content
18
+ def format_content(content, attachments = [])
19
+ Ollama::Media.format_content(content, attachments)
26
20
  end
27
21
 
28
22
  def format_role(role)
@@ -5,30 +5,21 @@ module RubyLLM
5
5
  class Ollama
6
6
  # Handles formatting of media content (images, audio) for Ollama APIs
7
7
  module Media
8
- extend OpenAI::Media
9
-
10
8
  module_function
11
9
 
12
- def format_content(content)
13
- return content.value if content.is_a?(RubyLLM::Content::Raw)
14
- return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
15
- return content unless content.is_a?(Content)
16
-
17
- parts = []
18
- parts << format_text(content.text) if content.text
19
-
20
- content.attachments.each do |attachment|
10
+ def format_content(content, attachments = [])
11
+ Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
21
12
  case attachment.type
22
13
  when :image
23
- parts << Ollama::Media.format_image(attachment)
14
+ format_image(attachment)
15
+ when :audio
16
+ Protocols::ChatCompletions::Media.format_audio(attachment)
24
17
  when :text
25
- parts << format_text_file(attachment)
18
+ Protocols::ChatCompletions::Media.format_text_file(attachment)
26
19
  else
27
20
  raise UnsupportedAttachmentError, attachment.mime_type
28
21
  end
29
22
  end
30
-
31
- parts
32
23
  end
33
24
 
34
25
  def format_image(image)
@@ -5,24 +5,37 @@ module RubyLLM
5
5
  class Ollama
6
6
  # Models methods for the Ollama API integration
7
7
  module Models
8
+ SHOW_URL = '../api/show'
9
+ CAPABILITY_MAP = {
10
+ 'tools' => 'function_calling',
11
+ 'vision' => 'vision',
12
+ 'thinking' => 'reasoning'
13
+ }.freeze
14
+
8
15
  def models_url
9
16
  'models'
10
17
  end
11
18
 
12
- def parse_list_models_response(response, slug, _capabilities)
13
- data = response.body['data'] || []
14
- data.map do |model|
15
- Model::Info.new(
19
+ def model_capabilities
20
+ %w[streaming structured_output]
21
+ end
22
+
23
+ def list_models
24
+ response = @connection.get models_url
25
+ parse_list_models_response(response, @provider.slug, details: show_models(response))
26
+ end
27
+
28
+ def parse_list_models_response(response, slug, details: {})
29
+ Array(response.body['data']).map do |model|
30
+ reported = Array(details[model['id']])
31
+ Model.new(
16
32
  id: model['id'],
17
33
  name: model['id'],
18
34
  provider: slug,
19
35
  family: 'ollama',
20
36
  created_at: model['created'] ? Time.at(model['created']) : nil,
21
- modalities: {
22
- input: %w[text image],
23
- output: %w[text]
24
- },
25
- capabilities: %w[streaming function_calling structured_output vision],
37
+ modalities: build_modalities(reported),
38
+ capabilities: build_capabilities(reported),
26
39
  pricing: {},
27
40
  metadata: {
28
41
  owned_by: model['owned_by']
@@ -30,6 +43,34 @@ module RubyLLM
30
43
  )
31
44
  end
32
45
  end
46
+
47
+ private
48
+
49
+ def show_models(response)
50
+ Array(response.body['data']).filter_map { |model| model['id'] }.to_h { |id| [id, show_model(id)] }
51
+ end
52
+
53
+ def show_model(id)
54
+ Array(@connection.post(SHOW_URL, { model: id }).body['capabilities'])
55
+ rescue Error, Faraday::Error => e
56
+ RubyLLM.logger.debug "Ollama did not report capabilities for #{id} (#{e.message})."
57
+ []
58
+ end
59
+
60
+ def build_capabilities(reported)
61
+ return [] if reported.include?('embedding')
62
+
63
+ derived = CAPABILITY_MAP.filter_map { |native, capability| capability if reported.include?(native) }
64
+ model_capabilities + derived
65
+ end
66
+
67
+ def build_modalities(reported)
68
+ return { input: %w[text], output: %w[embeddings] } if reported.include?('embedding')
69
+
70
+ input = %w[text]
71
+ input << 'image' if reported.include?('vision')
72
+ { input: input, output: %w[text] }
73
+ end
33
74
  end
34
75
  end
35
76
  end