ruby_llm 1.16.0 → 2.0.0.rc4

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 (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  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/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +242 -54
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +662 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +126 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -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
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Model information for the Cohere API
7
+ module Models
8
+ module_function
9
+
10
+ FEATURE_CAPABILITIES = {
11
+ 'tools' => 'function_calling',
12
+ 'tool_choice' => 'tool_choice',
13
+ 'json_schema' => 'structured_output',
14
+ 'json_mode' => 'json_mode',
15
+ 'citations' => 'citations'
16
+ }.freeze
17
+
18
+ # The model catalog is the one endpoint Cohere still serves from v1.
19
+ def models_url
20
+ 'v1/models?page_size=1000'
21
+ end
22
+
23
+ def parse_list_models_response(response, slug)
24
+ Array(response.body['models']).reject { |model| model['is_deprecated'] }.map do |model_data|
25
+ model_id = model_data['name']
26
+ endpoints = Array(model_data['endpoints'])
27
+
28
+ Model.new(
29
+ id: model_id,
30
+ name: model_id,
31
+ provider: slug,
32
+ context_window: model_data['context_length']&.to_i,
33
+ modalities: modalities_from(endpoints),
34
+ capabilities: capabilities_from(endpoints, model_data['features']),
35
+ metadata: {
36
+ endpoints: endpoints,
37
+ default_endpoints: Array(model_data['default_endpoints']),
38
+ features: Array(model_data['features']),
39
+ finetuned: model_data['finetuned'],
40
+ tokenizer_url: model_data['tokenizer_url']
41
+ }.compact
42
+ )
43
+ end
44
+ end
45
+
46
+ def modalities_from(endpoints)
47
+ return { input: ['image'], output: ['text'] } if endpoints.include?('parse')
48
+ return { input: ['audio'], output: ['text'] } if transcription_endpoint?(endpoints)
49
+
50
+ return embedding_modalities(endpoints) if embedding_endpoint?(endpoints)
51
+ return { input: ['text'], output: ['rerank'] } if rerank_endpoint?(endpoints)
52
+
53
+ { input: ['text'], output: ['text'] }
54
+ end
55
+
56
+ def capabilities_from(endpoints, features)
57
+ return ['ocr'] if endpoints.include?('parse')
58
+ return ['transcription'] if transcription_endpoint?(endpoints)
59
+ return [] if embedding_endpoint?(endpoints) || rerank_endpoint?(endpoints)
60
+
61
+ features = Array(features)
62
+ capabilities = []
63
+ capabilities << 'streaming' if endpoints.intersect?(%w[chat generate])
64
+ reported = FEATURE_CAPABILITIES.filter_map do |feature, capability|
65
+ capability if features.include?(feature)
66
+ end
67
+ capabilities.concat(reported)
68
+ capabilities
69
+ end
70
+
71
+ def embedding_endpoint?(endpoints)
72
+ endpoints.any? { |endpoint| endpoint.start_with?('embed') }
73
+ end
74
+
75
+ def embedding_modalities(endpoints)
76
+ input = []
77
+ input << 'text' if endpoints.include?('embed')
78
+ input << 'image' if endpoints.any? { |endpoint| endpoint.start_with?('embed_image') }
79
+ { input: input.empty? ? ['text'] : input, output: ['embeddings'] }
80
+ end
81
+
82
+ def rerank_endpoint?(endpoints)
83
+ endpoints.any? { |endpoint| endpoint.start_with?('rerank') }
84
+ end
85
+
86
+ def transcription_endpoint?(endpoints)
87
+ endpoints.any? { |endpoint| endpoint.start_with?('transcri') }
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end