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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The OpenAI Chat Completions API — the lingua franca of LLM APIs.
6
+ class ChatCompletions < Protocol
7
+ include ChatCompletions::Chat
8
+ include ChatCompletions::Embeddings
9
+ include ChatCompletions::Models
10
+ include ChatCompletions::Moderation
11
+ include ChatCompletions::Streaming
12
+ include ChatCompletions::Tools
13
+ include ChatCompletions::Images
14
+ include ChatCompletions::Media
15
+ include ChatCompletions::Speech
16
+ include ChatCompletions::Transcription
17
+
18
+ public :render_transcription_options
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ module BatchRequests # :nodoc: all
7
+ CHAT_FIELDS = %w[
8
+ messages tools temperature p frequency_penalty presence_penalty reasoning thinking_budget
9
+ return_prompt logprobs max_tokens max_input_tokens k seed
10
+ ].freeze
11
+ EMBEDDING_FIELDS = %w[texts images input_type inputs max_tokens output_dimension embedding_types
12
+ truncate].freeze
13
+ private_constant :CHAT_FIELDS, :EMBEDDING_FIELDS
14
+
15
+ def batch_dataset_type(requests)
16
+ types = requests.map do |request|
17
+ body = request.fetch(:payload)
18
+ body.key?(:messages) || body.key?('messages') ? 'batch-chat-v2-input' : 'batch-embed-v2-input'
19
+ end.uniq
20
+ raise ArgumentError, 'Cohere batches cannot mix chat and embeddings' unless types.one?
21
+
22
+ types.first
23
+ end
24
+
25
+ def render_batch_request(request, type:)
26
+ body = JSON.parse(JSON.generate(batch_payload(request, except: :model)))
27
+ if type == 'batch-embed-v2-input' && body['output_dimension']
28
+ raise ArgumentError,
29
+ 'Cohere batch datasets currently reject dimensions; omit dimensions to use the model default'
30
+ end
31
+
32
+ render_batch_chat(body) if type == 'batch-chat-v2-input'
33
+ allowed = type == 'batch-chat-v2-input' ? CHAT_FIELDS : EMBEDDING_FIELDS
34
+ unsupported = body.keys - allowed
35
+ unless unsupported.empty?
36
+ raise ArgumentError, "Cohere batches do not support these request options: #{unsupported.join(', ')}"
37
+ end
38
+
39
+ custom_id = request.fetch(:custom_id)
40
+ custom_id = "#{custom_id}:array" if request[:text].is_a?(Array)
41
+ { custom_id:, body: }
42
+ end
43
+
44
+ def render_batch_chat(body)
45
+ if (thinking = body.delete('thinking'))
46
+ body['reasoning'] = thinking['type'] != 'disabled'
47
+ body['thinking_budget'] = thinking['token_budget'] if thinking['token_budget']
48
+ end
49
+ Array(body['messages']).each { |message| render_batch_message(message) }
50
+ Array(body['tools']).each do |tool|
51
+ function = tool.fetch('function')
52
+ parameters = function['parameters']
53
+ function['parameters'] = JSON.generate(parameters) unless parameters.is_a?(String)
54
+ end
55
+ end
56
+
57
+ def render_batch_message(message)
58
+ content = message['content']
59
+ content = [{ 'type' => 'text', 'text' => content }] if content.is_a?(String)
60
+ message['content'] = content&.map { |part| render_batch_content(part) }
61
+ end
62
+
63
+ def render_batch_content(part)
64
+ unless %w[text thinking image_url].include?(part['type'])
65
+ raise ArgumentError, "Cohere batches do not support #{part['type']} content"
66
+ end
67
+
68
+ part = part.dup
69
+ part['image_url'] = part['image_url'].fetch('url') if part['image_url'].is_a?(Hash)
70
+ part
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ module Batches # :nodoc: all
7
+ include RubyLLM::Batch::Helpers
8
+ include Cohere::BatchRequests
9
+
10
+ TERMINAL_STATUSES = %w[BATCH_STATUS_COMPLETED BATCH_STATUS_FAILED BATCH_STATUS_CANCELED].freeze
11
+ Response = Struct.new(:body)
12
+ private_constant :TERMINAL_STATUSES, :Response
13
+
14
+ def create_batch(requests)
15
+ model = single_batch_model!(requests, 'Cohere')
16
+ type = batch_dataset_type(requests)
17
+ rows = requests.map { |request| render_batch_request(request, type:) }
18
+ file = datasets.upload(StringIO.new(rows.map { |row| "#{JSON.generate(row)}\n" }.join),
19
+ filename: 'ruby-llm-batch.jsonl', purpose: type)
20
+ datasets.wait_for_validation(file.id)
21
+ response = @connection.post('v2/batches', {
22
+ name: 'ruby-llm-batch', input_dataset_id: file.id, model: model
23
+ }, idempotent: false)
24
+ parse_batch_response(response.body.fetch('batch'))
25
+ end
26
+
27
+ def find_batch(id)
28
+ parse_batch_response(batch_data(id))
29
+ end
30
+
31
+ def cancel_batch(id)
32
+ @connection.post("v2/batches/#{id}/cancel", {})
33
+ find_batch(id)
34
+ end
35
+
36
+ def batch_results(id)
37
+ data = batch_data(id)
38
+ return [] if data['output_dataset_id'].to_s.empty?
39
+
40
+ file = datasets.wait_for_validation(data.fetch('output_dataset_id'))
41
+ model = RubyLLM.models.find(data.fetch('model'), provider: @provider.slug, config: @config)
42
+ parser = self.class.new(@provider, model)
43
+ results = datasets.records(file).map { |row| parse_batch_result(row, parser:, model: model.id) }
44
+ unless results.map(&:first).uniq.size == results.size
45
+ raise Error,
46
+ 'Cohere returned duplicate batch request IDs'
47
+ end
48
+
49
+ results
50
+ end
51
+
52
+ private
53
+
54
+ def datasets
55
+ @datasets ||= Cohere::Datasets.new(@provider)
56
+ end
57
+
58
+ def batch_data(id)
59
+ @connection.get("v2/batches/#{id}").body.fetch('batch')
60
+ end
61
+
62
+ def parse_batch_response(data)
63
+ {
64
+ id: data.fetch('id'), raw_status: data.fetch('status'),
65
+ completed: TERMINAL_STATUSES.include?(data['status']), request_count: data['num_records'],
66
+ request_counts: {
67
+ 'total' => data['num_records'], 'succeeded' => data['num_successful_records'],
68
+ 'failed' => data['num_failed_records']
69
+ }.compact
70
+ }
71
+ end
72
+
73
+ def parse_batch_status(raw_status, completed:)
74
+ return :pending unless completed
75
+ return :succeeded if raw_status == 'BATCH_STATUS_COMPLETED'
76
+ return :cancelled if raw_status == 'BATCH_STATUS_CANCELED'
77
+
78
+ :failed
79
+ end
80
+
81
+ def parse_batch_result(row, parser:, model:)
82
+ custom_id, shape = row.fetch('custom_id').split(':', 2)
83
+ index = batch_result_index(custom_id)
84
+ return [index, nil, batch_failure(custom_id, row['error'])] if !row['error'].to_s.empty? || !row['body']
85
+
86
+ body = row.fetch('body')
87
+ result = if body['embeddings']
88
+ parser.send(:parse_embedding_response, Response.new(body), model:,
89
+ text: shape == 'array' ? [] : nil)
90
+ else
91
+ parser.send(:parse_completion_body, body, raw: body)
92
+ end
93
+ [index, result]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Chat methods for the Cohere v2 API implementation
7
+ module Chat
8
+ FINISH_REASONS = {
9
+ 'COMPLETE' => :stop, 'STOP_SEQUENCE' => :stop, 'MAX_TOKENS' => :max_tokens, 'TOOL_CALL' => :tool_calls
10
+ }.freeze
11
+
12
+ module_function
13
+
14
+ def finish_reasons = FINISH_REASONS
15
+
16
+ def normalize_finish_reason(reason)
17
+ return nil if reason.nil?
18
+
19
+ finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
20
+ end
21
+
22
+ def completion_url
23
+ 'v2/chat'
24
+ end
25
+
26
+ # rubocop:disable-next Lint/UnusedMethodArgument
27
+ def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
28
+ schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
29
+ warn_unsupported_citations(model) if citations && !model.supports?(:citations)
30
+
31
+ payload = {
32
+ model: model.id,
33
+ messages: format_messages(messages, citations: citations),
34
+ stream: stream
35
+ }
36
+
37
+ add_optional_fields(payload, messages, temperature:, max_output_tokens:, citations:, schema:)
38
+ add_tools(payload, tools, tool_prefs || {})
39
+ add_thinking(payload, thinking)
40
+ payload
41
+ end
42
+
43
+ def add_optional_fields(payload, messages, temperature:, max_output_tokens:, citations:, schema:)
44
+ payload[:temperature] = temperature unless temperature.nil?
45
+ payload[:max_tokens] = max_output_tokens unless max_output_tokens.nil?
46
+ payload[:documents] = Media.format_documents(messages) if citations && Media.documents?(messages)
47
+ payload[:response_format] = build_response_format(schema) if schema
48
+ end
49
+
50
+ def warn_unsupported_citations(model)
51
+ RubyLLM.logger.warn(
52
+ "#{model.id} does not support citations according to the model registry. " \
53
+ 'with_citations may have no effect.'
54
+ )
55
+ end
56
+
57
+ def add_tools(payload, tools, tool_prefs)
58
+ return if tools.empty?
59
+
60
+ payload[:tools] = tools.values.map { |tool| Tools.function_for(tool) }
61
+ tool_choice = Tools.build_tool_choice(tool_prefs[:choice])
62
+ payload[:tool_choice] = tool_choice if tool_choice
63
+ end
64
+
65
+ # Cohere takes a bare JSON Schema under json_schema, with no name or
66
+ # strict wrapper.
67
+ def build_response_format(schema)
68
+ normalized = RubyLLM::Support::Utils.deep_dup(schema[:schema])
69
+ normalized.delete(:strict)
70
+ normalized.delete('strict')
71
+
72
+ { type: 'json_object', json_schema: normalized }
73
+ end
74
+
75
+ # Reasoning is on by default for models that support it, so an
76
+ # explicit disable is as meaningful as an explicit enable.
77
+ def add_thinking(payload, thinking)
78
+ return unless thinking&.enabled?
79
+ return payload[:thinking] = { type: 'disabled' } if thinking.disabled?
80
+
81
+ payload[:thinking] = { type: 'enabled', token_budget: thinking.budget }.compact
82
+ end
83
+
84
+ def format_messages(messages, citations: false)
85
+ messages.map { |msg| format_message(msg, citations: citations) }
86
+ end
87
+
88
+ def format_message(msg, citations: false)
89
+ return Tools.format_tool_result(msg) if msg.tool_result?
90
+ return format_assistant_message(msg) if msg.role == :assistant
91
+
92
+ {
93
+ role: msg.role.to_s,
94
+ content: Media.format_content(msg.content, msg.attachments, citations: citations)
95
+ }
96
+ end
97
+
98
+ # Cohere returns tool_plan alongside tool calls, and RubyLLM surfaces
99
+ # it as thinking, but the newer models reject it on the way back in.
100
+ # It is optional in a request, so the plan stays out of the history.
101
+ def format_assistant_message(msg)
102
+ message = { role: 'assistant' }
103
+ content = Media.format_content(msg.content, msg.attachments)
104
+ message[:content] = content unless content.empty?
105
+ message[:tool_calls] = Tools.format_tool_calls(msg.tool_calls) if msg.tool_call?
106
+ message
107
+ end
108
+
109
+ def parse_completion_body(data, raw:)
110
+ message_data = data['message'] || {}
111
+ blocks = Array(message_data['content'])
112
+ content, offsets = extract_text(blocks)
113
+ finish_reason = normalize_finish_reason(data['finish_reason'])
114
+
115
+ Message.new(
116
+ role: :assistant,
117
+ content: content,
118
+ citations: parse_citations(message_data['citations'], offsets),
119
+ thinking: Thinking.build(text: extract_thinking(blocks, message_data)),
120
+ tool_calls: Tools.parse_tool_calls(message_data['tool_calls'], response: raw, finish_reason:),
121
+ finish_reason: finish_reason,
122
+ model: model&.id,
123
+ raw: raw,
124
+ **usage_tokens(data['usage'] || {})
125
+ )
126
+ end
127
+
128
+ # Cohere reports what the model processed under tokens and what it
129
+ # charges for under billed_units; the two differ because Cohere does
130
+ # not bill its own preamble.
131
+ def usage_tokens(usage)
132
+ tokens = usage['tokens'] || {}
133
+ billed = usage['billed_units'] || {}
134
+
135
+ {
136
+ input_tokens: tokens['input_tokens'] || billed['input_tokens'],
137
+ output_tokens: tokens['output_tokens'] || billed['output_tokens'],
138
+ cache_read_tokens: usage['cached_tokens']
139
+ }
140
+ end
141
+
142
+ # Returns the joined text of the response along with the offset each
143
+ # content block starts at, so citation spans resolve against the
144
+ # content string RubyLLM exposes.
145
+ def extract_text(blocks)
146
+ text = +''
147
+ offsets = {}
148
+
149
+ blocks.each_with_index do |block, index|
150
+ next unless block['type'] == 'text'
151
+
152
+ offsets[index] = text.length
153
+ text << block['text'].to_s
154
+ end
155
+
156
+ [text, offsets]
157
+ end
158
+
159
+ # The tool plan is the model's reasoning for a tool-calling turn, and
160
+ # is the only reasoning Cohere returns when thinking blocks are absent.
161
+ def extract_thinking(blocks, message_data)
162
+ thoughts = blocks.select { |block| block['type'] == 'thinking' }
163
+ .map { |block| block['thinking'] }.join
164
+ thoughts.empty? ? message_data['tool_plan'] : thoughts
165
+ end
166
+
167
+ def parse_citations(citations, offsets)
168
+ Array(citations).map { |citation| parse_citation(citation, offsets) }
169
+ end
170
+
171
+ # Only citations of the response text carry offsets into #content.
172
+ # Citations of the thinking blocks or the tool plan point into text
173
+ # RubyLLM exposes elsewhere, so they keep their snippet without a span.
174
+ def parse_citation(data, offsets = {})
175
+ source = Array(data['sources']).first
176
+ document = source_document(source)
177
+ start_index, end_index = citation_span(data, offsets)
178
+
179
+ Citation.new(
180
+ url: citation_url(document),
181
+ title: document['title'] || document['id'],
182
+ cited_text: document['text'] || document['snippet'],
183
+ text: data['text'],
184
+ start_index: start_index,
185
+ end_index: end_index,
186
+ source_index: source_index(source)
187
+ )
188
+ end
189
+
190
+ def citation_span(data, offsets)
191
+ return [nil, nil] unless text_citation?(data)
192
+
193
+ offset = offsets[data['content_index'] || 0]
194
+ return [nil, nil] unless offset
195
+
196
+ [data['start'] && (offset + data['start']), data['end'] && (offset + data['end'])]
197
+ end
198
+
199
+ def text_citation?(data)
200
+ type = data['type']
201
+ type.nil? || type == 'TEXT_CONTENT'
202
+ end
203
+
204
+ def source_document(source)
205
+ return {} unless source
206
+
207
+ source['document'] || source['tool_output'] || {}
208
+ end
209
+
210
+ def citation_url(document)
211
+ url = document['url']
212
+ url if url.is_a?(String) && url.match?(%r{\Ahttps?://}i)
213
+ end
214
+
215
+ # Documents RubyLLM sends, and those Cohere numbers itself, are
216
+ # identified as doc:N where N is the document's position.
217
+ def source_index(source)
218
+ id = source && source['id']
219
+ return unless id.is_a?(String)
220
+
221
+ match = id.match(/\Adoc:(\d+)\z/)
222
+ match && match[1].to_i
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Cohere datasets for batch input and generated results.
7
+ class Datasets < Protocols::Files
8
+ def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
9
+ provider_options: {})
10
+ raise ArgumentError, 'Cohere datasets require purpose: with a dataset type' unless purpose
11
+ raise ArgumentError, 'Cohere datasets do not accept expires_in or uri' if expires_in || uri
12
+
13
+ attachment = file_attachment(file, filename:)
14
+ options = { name: attachment.filename, type: purpose, keep_original_file: true }
15
+ .merge(provider_options.transform_keys(&:to_sym))
16
+ response = @connection.post('v1/datasets', { data: file_part(attachment, content_type:) },
17
+ idempotent: false) do |request|
18
+ request.headers.delete('Content-Type')
19
+ request.params.update(options)
20
+ end
21
+ find(response.body.fetch('id'))
22
+ end
23
+
24
+ def download(file_id)
25
+ file = wait_for_validation(file_id)
26
+ parts = dataset_parts(file.metadata)
27
+ originals = parts.filter_map { |part| part['original_url'] }.uniq
28
+ if parts.any? && parts.all? { |part| part['original_url'] }
29
+ return originals.map do |url|
30
+ download_part(url)
31
+ end.join
32
+ end
33
+
34
+ records(file).map { |row| "#{JSON.generate(row)}\n" }.join
35
+ end
36
+
37
+ def records(file)
38
+ load_avro
39
+ dataset_parts(file.metadata).flat_map do |part|
40
+ reader = nil
41
+ content = StringIO.new(download_part(part.fetch('url')))
42
+ reader = Avro::DataFile::Reader.new(content, Avro::IO::DatumReader.new)
43
+ reader.to_a
44
+ ensure
45
+ reader&.close
46
+ end
47
+ end
48
+
49
+ def wait_for_validation(id)
50
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config.request_timeout
51
+ loop do
52
+ file = find(id)
53
+ return file if file.status == 'validated'
54
+ if file.status == 'failed'
55
+ raise Error, "Cohere dataset #{id} failed validation: #{file.metadata['validation_error']}"
56
+ end
57
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
58
+ raise Error, "Cohere dataset validation timed out: #{id}"
59
+ end
60
+
61
+ sleep 1
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def files_url
68
+ 'v1/datasets'
69
+ end
70
+
71
+ def parse_file_response(response)
72
+ data = response.fetch('dataset')
73
+ parts = dataset_parts(data)
74
+ original = parts.first&.fetch('original_url', nil)
75
+ filename = original ? File.basename(URI.parse(original).path) : "#{data.fetch('name')}.jsonl"
76
+ mime_type = if File.extname(filename) == '.jsonl'
77
+ 'application/jsonl'
78
+ else
79
+ RubyLLM::Files::MimeType.for(name: filename)
80
+ end
81
+ uploaded_file(data, id: data.fetch('id'), filename:, mime_type:,
82
+ created_at: timestamp(data['created_at']), status: data['validation_status'],
83
+ purpose: data['dataset_type'], downloadable: !parts.empty?)
84
+ end
85
+
86
+ def dataset_parts(data)
87
+ Array(data['dataset_parts']).sort_by.with_index { |part, index| part.fetch('index', index) }
88
+ end
89
+
90
+ def download_part(url)
91
+ Transport::Connection.basic(@config).get(url).body
92
+ end
93
+
94
+ def load_avro
95
+ require 'avro'
96
+ rescue LoadError
97
+ raise LoadError, 'Add gem "avro" to your Gemfile to read Cohere batch results and processed datasets'
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Embeddings methods for the Cohere v2 API integration
7
+ module Embeddings
8
+ DEFAULT_INPUT_TYPE = 'search_document'
9
+
10
+ module_function
11
+
12
+ def embedding_url(...)
13
+ 'v2/embed'
14
+ end
15
+
16
+ # rubocop:disable-next Lint/UnusedMethodArgument
17
+ def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, with: [],
18
+ provider_options: {})
19
+ image_only = with.any? && separate_image_embeddings?(model)
20
+ payload = {
21
+ model: model,
22
+ input_type: task_type || (image_only ? 'image' : DEFAULT_INPUT_TYPE),
23
+ embedding_types: ['float'],
24
+ output_dimension: dimensions
25
+ }.compact
26
+
27
+ payload.merge!(image_only ? image_embedding_inputs(text, with) : embedding_inputs(text, with))
28
+ Support::Utils.deep_merge(payload, provider_options)
29
+ end
30
+
31
+ def supports_embedding_media?
32
+ true
33
+ end
34
+
35
+ def separate_image_embeddings?(model) # :nodoc:
36
+ %w[embed-english-v3.0 embed-multilingual-v3.0].include?(model)
37
+ end
38
+
39
+ def image_embedding_inputs(text, attachments) # :nodoc:
40
+ raise ArgumentError, 'Cohere Embed v3 accepts text or an image, not both' unless text.nil? || text == ''
41
+ raise ArgumentError, 'Cohere Embed v3 accepts one image per request' unless attachments.one?
42
+ raise UnsupportedAttachmentError, attachments.first.mime_type unless attachments.first.image?
43
+
44
+ { images: ["data:#{attachments.first.mime_type};base64,#{attachments.first.encoded}"] }
45
+ end
46
+
47
+ def parse_embedding_response(response, model:, text:)
48
+ data = response.body
49
+ vectors = data.dig('embeddings', 'float')
50
+ vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
51
+ billed = data.dig('meta', 'billed_units') || {}
52
+
53
+ Embedding.new(vectors:, model:, input_tokens: billed['input_tokens'])
54
+ end
55
+
56
+ # embed-v4 takes mixed text and images as `inputs`; text-only requests
57
+ # keep using the simpler `texts` array every Embed model accepts.
58
+ def embedding_inputs(text, attachments)
59
+ return { texts: Support::Utils.to_safe_array(text).map(&:to_s) } if attachments.empty?
60
+
61
+ raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
62
+
63
+ { inputs: [{ content: Media.format_content(text, attachments) }] }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Handles formatting of media content for the Cohere v2 API
7
+ module Media
8
+ # Cohere resolves a remote image by its extension and rejects a URL
9
+ # without one, so those are inlined from the bytes already fetched.
10
+ URL_IMAGE_EXTENSIONS = %w[png jpeg jpg gif webp].freeze
11
+
12
+ module_function
13
+
14
+ # Cohere user messages accept only text and image_url blocks. Text
15
+ # attachments become citable documents when citations are on, so they
16
+ # are pulled out of the message and left out of the content blocks.
17
+ def format_content(content, attachments = [], citations: false)
18
+ parts = []
19
+ parts << format_text(content) if content
20
+
21
+ attachments.each do |attachment|
22
+ case attachment.type
23
+ when :image
24
+ parts << format_image(attachment)
25
+ when :text
26
+ parts << format_text(attachment.for_llm) unless citations
27
+ else
28
+ raise UnsupportedAttachmentError, attachment.mime_type
29
+ end
30
+ end
31
+
32
+ parts
33
+ end
34
+
35
+ def format_text(text)
36
+ {
37
+ type: 'text',
38
+ text: text
39
+ }
40
+ end
41
+
42
+ def format_image(image)
43
+ {
44
+ type: 'image_url',
45
+ image_url: { url: image_source(image) }
46
+ }
47
+ end
48
+
49
+ def image_source(image)
50
+ return image.source.to_s if image.url? && URL_IMAGE_EXTENSIONS.include?(image.extension)
51
+
52
+ "data:#{image.mime_type};base64,#{image.encoded}"
53
+ end
54
+
55
+ # Text attachments across the conversation become the request's
56
+ # top-level documents array, which is what Cohere cites against.
57
+ def format_documents(messages)
58
+ index = -1
59
+
60
+ messages.flat_map do |message|
61
+ message.attachments.select(&:text?).map do |attachment|
62
+ index += 1
63
+ {
64
+ id: "doc:#{index}",
65
+ data: { title: attachment.filename, text: attachment.content }.compact
66
+ }
67
+ end
68
+ end
69
+ end
70
+
71
+ def documents?(messages)
72
+ messages.any? { |message| message.attachments.any?(&:text?) }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end