ruby_llm 1.15.0 → 2.0.0.rc1

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 (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  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 -6
  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 +25 -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 +100 -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 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  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 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  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 +1208 -150
  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 +274 -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 +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  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 +155 -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 +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -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 +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  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 +540 -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 +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -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/protocols/chat_completions/media.rb +121 -0
  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 +17 -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 +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -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
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Cohere Parse accepts one document image and returns Markdown or blocks.
7
+ module OCR
8
+ module_function
9
+
10
+ def ocr_url
11
+ 'v2/parse'
12
+ end
13
+
14
+ def render_ocr_payload(file, model:, pages: nil, provider_options: {})
15
+ attachment = file.is_a?(Attachment) ? file : Attachment.new(file, config: @config)
16
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
17
+ raise ArgumentError, 'Cohere Parse accepts one image; pages must be [0]' unless pages.nil? || pages == [0]
18
+
19
+ reference = attachment.url? ? attachment.source.to_s : attachment.for_llm
20
+ {
21
+ model: model,
22
+ document: { type: 'image_url', image_url: reference },
23
+ output_format: 'markdown'
24
+ }.merge(provider_options)
25
+ end
26
+
27
+ def parse_ocr_response(response, model:)
28
+ data = response.body
29
+ RubyLLM::OCR.new(
30
+ pages: data.fetch('pages').map { |page| parse_ocr_page(page) },
31
+ model: model,
32
+ usage: data.dig('meta', 'billed_units'),
33
+ raw: data
34
+ )
35
+ end
36
+
37
+ def parse_ocr_page(page)
38
+ content = page['markdown'] || parse_ocr_blocks(page.fetch('blocks'))
39
+ RubyLLM::OCR::Page.new(
40
+ index: page['index'], markdown: content['content'],
41
+ images: content['images'], tables: content['tables'], raw: page
42
+ )
43
+ end
44
+
45
+ def parse_ocr_blocks(blocks)
46
+ {
47
+ 'content' => blocks.filter_map { |block| parse_ocr_block_text(block) }.join("\n\n"),
48
+ 'images' => blocks.filter_map { |block| block['image'] },
49
+ 'tables' => blocks.filter_map { |block| block['table'] }
50
+ }
51
+ end
52
+
53
+ def parse_ocr_block_text(block)
54
+ case block['type']
55
+ when 'text' then block.dig('text', 'content')
56
+ when 'table' then block.dig('table', 'html')
57
+ when 'image' then "![#{block.dig('image', 'description')}](#{block.dig('image', 'id')})"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Rerank methods for the Cohere v2 API integration
7
+ module Rerank
8
+ module_function
9
+
10
+ def rerank_url
11
+ 'v2/rerank'
12
+ end
13
+
14
+ def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
15
+ {
16
+ model: model,
17
+ query: query,
18
+ documents: documents,
19
+ top_n: top_n
20
+ }.compact.merge(provider_options)
21
+ end
22
+
23
+ # Cohere returns positions and scores only, so the ranked text comes
24
+ # back from the documents that were sent.
25
+ def parse_rerank_response(response, model:, documents: [])
26
+ data = response.body
27
+ billed = data.dig('meta', 'billed_units') || {}
28
+ tokens = data.dig('meta', 'tokens') || {}
29
+
30
+ RubyLLM::Rerank.new(
31
+ results: parse_rerank_results(data, documents),
32
+ model: model,
33
+ raw: data,
34
+ input_tokens: tokens['input_tokens'] || billed['input_tokens']
35
+ )
36
+ end
37
+
38
+ def parse_rerank_results(data, documents)
39
+ Array(data['results']).map do |result|
40
+ index = result['index']
41
+
42
+ RubyLLM::Rerank::Result.new(
43
+ index: index,
44
+ document: result.dig('document', 'text') || (index && documents[index]),
45
+ score: result['relevance_score']
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Streaming methods of the Cohere v2 API integration
7
+ module Streaming
8
+ private
9
+
10
+ def stream_url
11
+ completion_url
12
+ end
13
+
14
+ def build_chunk(data)
15
+ type = data['type']
16
+ track_content_lengths(data, type)
17
+
18
+ Chunk.new(
19
+ role: :assistant,
20
+ model: model&.id,
21
+ content: extract_content_delta(data, type),
22
+ citations: extract_citations_delta(data, type),
23
+ thinking: Thinking.build(text: extract_thinking_delta(data, type)),
24
+ tool_calls: extract_tool_calls(data, type),
25
+ finish_reason: normalize_finish_reason(data.dig('delta', 'finish_reason')),
26
+ **stream_usage(data.dig('delta', 'usage'))
27
+ )
28
+ end
29
+
30
+ # Citation spans are offsets into their own content block, so the
31
+ # running length of every earlier text block is what turns them into
32
+ # offsets into the accumulated response content.
33
+ def track_content_lengths(data, type)
34
+ return unless type == 'content-delta'
35
+
36
+ text = data.dig('delta', 'message', 'content', 'text')
37
+ return unless text
38
+
39
+ @stream_content_lengths ||= Hash.new(0)
40
+ @stream_content_lengths[data['index']] += text.length
41
+ end
42
+
43
+ def content_offset(index)
44
+ return 0 unless @stream_content_lengths
45
+
46
+ @stream_content_lengths.sum { |block_index, length| block_index < index ? length : 0 }
47
+ end
48
+
49
+ def extract_content_delta(data, type)
50
+ data.dig('delta', 'message', 'content', 'text') if type == 'content-delta'
51
+ end
52
+
53
+ def extract_thinking_delta(data, type)
54
+ case type
55
+ when 'content-delta' then data.dig('delta', 'message', 'content', 'thinking')
56
+ when 'tool-plan-delta' then data.dig('delta', 'message', 'tool_plan')
57
+ end
58
+ end
59
+
60
+ def extract_citations_delta(data, type)
61
+ return nil unless type == 'citation-start'
62
+
63
+ citation = data.dig('delta', 'message', 'citations')
64
+ return nil unless citation
65
+
66
+ index = citation['content_index'] || 0
67
+ [Chat.parse_citation(citation, { index => content_offset(index) })]
68
+ end
69
+
70
+ def extract_tool_calls(data, type)
71
+ case type
72
+ when 'tool-call-start' then stream_tool_call_start(data)
73
+ when 'tool-call-delta' then stream_tool_call_delta(data)
74
+ end
75
+ end
76
+
77
+ def stream_tool_call_start(data)
78
+ tool_call = data.dig('delta', 'message', 'tool_calls')
79
+ return nil unless tool_call
80
+
81
+ {
82
+ data['index'] => ToolCall.new(
83
+ id: tool_call['id'],
84
+ name: tool_call.dig('function', 'name'),
85
+ arguments: tool_call.dig('function', 'arguments').to_s
86
+ )
87
+ }
88
+ end
89
+
90
+ def stream_tool_call_delta(data)
91
+ arguments = data.dig('delta', 'message', 'tool_calls', 'function', 'arguments')
92
+ return nil unless arguments
93
+
94
+ { data['index'] => ToolCall.new(id: nil, name: nil, arguments: arguments) }
95
+ end
96
+
97
+ def stream_usage(usage)
98
+ return {} unless usage
99
+
100
+ Chat.usage_tokens(usage)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Cohere's plain-text tokenizer is served by the v1 API.
7
+ module Tokenization
8
+ def tokenization_url
9
+ 'v1/tokenize'
10
+ end
11
+
12
+ def render_tokenization_payload(text, model:)
13
+ { model:, text: }
14
+ end
15
+
16
+ def parse_tokenization_response(response, model:)
17
+ RubyLLM::Tokenization.new(ids: response.body.fetch('tokens'), model:, raw: response.body)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Cohere
8
+ # Tools methods of the Cohere v2 API integration
9
+ module Tools
10
+ EMPTY_PARAMETERS_SCHEMA = {
11
+ 'type' => 'object',
12
+ 'properties' => {},
13
+ 'required' => []
14
+ }.freeze
15
+
16
+ module_function
17
+
18
+ def function_for(tool)
19
+ definition = {
20
+ type: 'function',
21
+ function: {
22
+ name: tool.name,
23
+ description: tool.description,
24
+ parameters: parameters_schema_for(tool)
25
+ }.compact
26
+ }
27
+
28
+ return definition if tool.provider_options.empty?
29
+
30
+ RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
31
+ end
32
+
33
+ # Cohere takes plain JSON Schema here and controls strictness with the
34
+ # request's own strict_tools flag, so the OpenAI strict keyword goes.
35
+ def parameters_schema_for(tool)
36
+ schema = tool.parameters_schema ||
37
+ RubyLLM::Tool::SchemaDefinition.from_parameters(tool.declared_parameters)&.json_schema
38
+ return EMPTY_PARAMETERS_SCHEMA unless schema
39
+
40
+ schema = RubyLLM::Support::Utils.deep_dup(schema)
41
+ schema.delete(:strict)
42
+ schema.delete('strict')
43
+ schema
44
+ end
45
+
46
+ # Cohere only forces tool use on or off; it cannot pin a named tool.
47
+ def build_tool_choice(choice)
48
+ case choice
49
+ when nil, :auto then nil
50
+ when :none then 'NONE'
51
+ when :required then 'REQUIRED'
52
+ else
53
+ raise ArgumentError,
54
+ "Cohere tool choice accepts :auto, :required, or :none, got #{choice.inspect}"
55
+ end
56
+ end
57
+
58
+ def format_tool_calls(tool_calls)
59
+ tool_calls.map do |_, tool_call|
60
+ {
61
+ id: tool_call.id,
62
+ type: 'function',
63
+ function: {
64
+ name: tool_call.name,
65
+ arguments: JSON.generate(tool_call.arguments)
66
+ }
67
+ }
68
+ end
69
+ end
70
+
71
+ def format_tool_result(msg)
72
+ {
73
+ role: 'tool',
74
+ tool_call_id: msg.tool_call_id,
75
+ content: format_tool_result_content(msg)
76
+ }
77
+ end
78
+
79
+ # Search results become document content blocks, the shape Cohere
80
+ # cites tool output against. A tool that attaches files sends its
81
+ # text and each attachment as blocks, so the model reads what the
82
+ # tool fetched rather than only the sentence describing it.
83
+ def format_tool_result_content(msg)
84
+ search_results = RubyLLM::SearchResults.from_content(msg.content)
85
+ return document_blocks(search_results) if search_results
86
+ return msg.content.to_s if msg.attachments.empty?
87
+
88
+ [Media.format_text(msg.content.to_s)] + msg.attachments.map do |attachment|
89
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.type == :text
90
+
91
+ Media.format_text(attachment.for_llm)
92
+ end
93
+ end
94
+
95
+ def document_blocks(search_results)
96
+ search_results.results.map.with_index do |result, index|
97
+ {
98
+ type: 'document',
99
+ document: {
100
+ id: result[:url] || "tool:#{index}",
101
+ data: { title: result[:title], text: result[:text], url: result[:url] }.compact
102
+ }
103
+ }
104
+ end
105
+ end
106
+
107
+ def parse_tool_calls(tool_calls, response: nil, finish_reason: nil)
108
+ return nil unless tool_calls&.any?
109
+
110
+ tool_calls.to_h do |tool_call|
111
+ [
112
+ tool_call['id'],
113
+ ToolCall.new(
114
+ id: tool_call['id'],
115
+ name: tool_call.dig('function', 'name'),
116
+ arguments: parse_arguments(tool_call.dig('function', 'arguments'), response, finish_reason)
117
+ )
118
+ ]
119
+ end
120
+ end
121
+
122
+ def parse_arguments(arguments, response, finish_reason)
123
+ return {} if arguments.nil? || arguments.empty?
124
+
125
+ JSON.parse(arguments)
126
+ rescue JSON::ParserError => e
127
+ raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ # Audio transcription methods for the Cohere v2 API integration
7
+ module Transcription
8
+ DEFAULT_LANGUAGE = 'en'
9
+
10
+ module_function
11
+
12
+ def transcription_url
13
+ 'v2/audio/transcriptions'
14
+ end
15
+
16
+ # Cohere parses the multipart body as it streams, so every form field
17
+ # has to be written before the file part or the request is rejected as
18
+ # missing them. The file goes in last, after provider options.
19
+ # rubocop:disable-next Lint/UnusedMethodArgument
20
+ def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
21
+ speaker_references: nil, provider_options: {}, prompt: nil,
22
+ temperature: nil)
23
+ {
24
+ model: model,
25
+ language: language || DEFAULT_LANGUAGE,
26
+ temperature: temperature
27
+ }.compact.merge(provider_options).merge(file: file_part)
28
+ end
29
+
30
+ # Cohere Transcribe returns the transcript alone: no timestamps,
31
+ # segments, or speaker labels.
32
+ def parse_transcription_response(response, model:)
33
+ data = response.body
34
+ return RubyLLM::Transcription.new(text: data, model: model) if data.is_a?(String)
35
+
36
+ RubyLLM::Transcription.new(text: data['text'], model: model)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The Cohere v2 API, a native wire format shared by Cohere's own host and
6
+ # the platforms that serve Command, Embed, Rerank, and Transcribe models.
7
+ class Cohere < Protocol
8
+ include Cohere::Batches
9
+ include Cohere::Chat
10
+ include Cohere::Embeddings
11
+ include Cohere::Media
12
+ include Cohere::Models
13
+ include Cohere::OCR
14
+ include Cohere::Tokenization
15
+ include Cohere::Rerank
16
+ include Cohere::Streaming
17
+ include Cohere::Tools
18
+ include Cohere::Transcription
19
+ end
20
+ end
21
+ end