ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -5,11 +5,9 @@ module RubyLLM
5
5
  class OpenRouter
6
6
  # Streaming methods of the OpenRouter API integration
7
7
  module Streaming
8
- module_function
8
+ ACCUMULATED_REASONING_KEYS = %w[text data summary].freeze
9
9
 
10
- def stream_url
11
- completion_url
12
- end
10
+ module_function
13
11
 
14
12
  def build_chunk(data)
15
13
  usage = data['usage'] || {}
@@ -17,55 +15,50 @@ module RubyLLM
17
15
 
18
16
  Chunk.new(
19
17
  role: :assistant,
20
- model_id: data['model'],
18
+ model: data['model'],
21
19
  content: delta['content'],
22
20
  thinking: Thinking.build(
23
21
  text: extract_thinking_text(delta),
24
22
  signature: extract_thinking_signature(delta)
25
23
  ),
26
- tool_calls: OpenAI::Tools.parse_tool_calls(delta['tool_calls'], parse_arguments: false),
27
- input_tokens: OpenRouter::Chat.input_tokens(usage),
28
- output_tokens: OpenRouter::Chat.output_tokens(usage),
29
- cached_tokens: OpenRouter::Chat.cache_read_tokens(usage),
30
- cache_creation_tokens: OpenRouter::Chat.cache_write_tokens(usage),
31
- thinking_tokens: OpenRouter::Chat.thinking_tokens(usage)
24
+ raw_reasoning: accumulate_raw_reasoning(delta['reasoning_details']),
25
+ tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false, stream_keys: true),
26
+ input_tokens: input_tokens(usage),
27
+ output_tokens: output_tokens(usage),
28
+ cache_read_tokens: cache_read_tokens(usage),
29
+ cache_write_tokens: cache_write_tokens(usage),
30
+ thinking_tokens: thinking_tokens(usage),
31
+ reported_cost: reported_cost(usage),
32
+ finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason'))
32
33
  )
33
34
  end
34
35
 
35
- def parse_streaming_error(data)
36
- OpenAI::Streaming.parse_streaming_error(data)
37
- end
36
+ def accumulate_raw_reasoning(details)
37
+ return @raw_reasoning unless details.is_a?(Array) && !details.empty?
38
38
 
39
- def extract_thinking_text(delta)
40
- candidate = delta['reasoning']
41
- return candidate if candidate.is_a?(String)
39
+ @raw_reasoning ||= []
40
+ details.each { |detail| merge_reasoning_detail(detail) }
41
+ @raw_reasoning
42
+ end
42
43
 
43
- details = delta['reasoning_details']
44
- return nil unless details.is_a?(Array)
44
+ def merge_reasoning_detail(detail)
45
+ target = reasoning_detail_target(detail)
46
+ return @raw_reasoning << detail.dup unless target
45
47
 
46
- text = details.filter_map do |detail|
47
- case detail['type']
48
- when 'reasoning.text'
49
- detail['text']
50
- when 'reasoning.summary'
51
- detail['summary']
48
+ detail.each do |key, value|
49
+ if value.is_a?(String) && target[key].is_a?(String) && ACCUMULATED_REASONING_KEYS.include?(key)
50
+ target[key] += value
51
+ elsif target[key].nil?
52
+ target[key] = value
52
53
  end
53
- end.join
54
-
55
- text.empty? ? nil : text
54
+ end
56
55
  end
57
56
 
58
- def extract_thinking_signature(delta)
59
- details = delta['reasoning_details']
60
- return nil unless details.is_a?(Array)
61
-
62
- signature = details.filter_map do |detail|
63
- detail['signature'] if detail['signature'].is_a?(String)
64
- end.first
65
- return signature if signature
57
+ def reasoning_detail_target(detail)
58
+ index = detail['index']
59
+ return nil unless index
66
60
 
67
- encrypted = details.find { |detail| detail['type'] == 'reasoning.encrypted' && detail['data'].is_a?(String) }
68
- encrypted&.dig('data')
61
+ @raw_reasoning.find { |entry| entry['index'] == index && entry['type'] == detail['type'] }
69
62
  end
70
63
  end
71
64
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenRouter
6
+ # OpenRouter's asynchronous video generation API. Reference images
7
+ # become frame_images entries, and finished videos are downloaded
8
+ # from the job's authenticated content endpoint.
9
+ module Videos
10
+ FRAME_TYPES = %w[first_frame last_frame].freeze
11
+
12
+ def video_url
13
+ 'videos'
14
+ end
15
+
16
+ def render_video_payload(prompt, model:, with: [], provider_options: {})
17
+ payload = { model: model, prompt: prompt }
18
+ payload[:frame_images] = frame_images(with) if with.any?
19
+
20
+ payload.merge(provider_options)
21
+ end
22
+
23
+ def parse_video_job(response, model:)
24
+ id = response.body['id']
25
+ raise Error.new('OpenRouter did not return a video job', response:) unless id
26
+
27
+ VideoJob.new(id: id, protocol: self, model: model, status: job_status(response.body), raw: response.body)
28
+ end
29
+
30
+ def video_job_url(job)
31
+ "videos/#{job.id}"
32
+ end
33
+
34
+ def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
35
+ body = response.body
36
+ status = job_status(body)
37
+ state = { status: status, raw: body }
38
+ state[:error] = body['error'] || body['status'] if status == :failed
39
+ state
40
+ end
41
+
42
+ def download_video(job)
43
+ response = @connection.get "videos/#{job.id}/content?index=0"
44
+
45
+ Video.new(
46
+ data: response.body,
47
+ mime_type: response.headers['content-type'] || 'video/mp4',
48
+ model: job.model,
49
+ raw: job.raw
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ def job_status(body)
56
+ case body['status']
57
+ when 'completed' then :completed
58
+ when 'failed' then :failed
59
+ else :pending
60
+ end
61
+ end
62
+
63
+ def validate_animate_inputs!(with:)
64
+ raise Error, 'OpenRouter video generation takes at most first and last frame images' if with.size > 2
65
+
66
+ with.each do |attachment|
67
+ next if attachment.url?
68
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
69
+ end
70
+ end
71
+
72
+ def frame_images(attachments)
73
+ attachments.zip(FRAME_TYPES).map do |attachment, frame_type|
74
+ url = attachment.url? ? attachment.source.to_s : attachment.for_llm
75
+ { type: 'image_url', image_url: { url: url }, frame_type: frame_type }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -3,58 +3,116 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # OpenRouter API integration.
6
- class OpenRouter < OpenAI
7
- include OpenRouter::Chat
8
- include OpenRouter::Models
9
- include OpenRouter::Streaming
10
- include OpenRouter::Images
6
+ class OpenRouter < Provider
7
+ # OpenRouter's dialect of the Chat Completions API.
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include OpenRouter::Chat
10
+ include OpenRouter::Embeddings
11
+ include OpenRouter::Images
12
+ include OpenRouter::Models
13
+ include OpenRouter::Speech
14
+ include OpenRouter::Streaming
15
+ include Protocols::ChatCompletions::Rerank
16
+ include OpenRouter::Videos
17
+ include Protocols::OpenRouter::Transcription
18
+
19
+ # OpenRouter runs its server tools transparently: results surface as
20
+ # citations and usage counters rather than discrete content blocks.
21
+ SERVER_TOOL_ALIASES = %i[
22
+ web_search web_fetch datetime image_generation apply_patch shell
23
+ ].to_h { |name| [name, { tool: { type: "openrouter:#{name}" } }] }.merge(
24
+ url_context: { tool: { type: 'openrouter:web_fetch' } },
25
+ code_execution: { tool: { type: 'openrouter:shell' } }
26
+ ).freeze
27
+
28
+ def server_tool_aliases
29
+ SERVER_TOOL_ALIASES
30
+ end
31
+ end
32
+
33
+ protocol :chat_completions, ChatCompletions, batches: Protocols::OpenRouter::Batches
34
+ protocol :responses, Protocols::OpenRouter::Responses
35
+ protocol :files, Protocols::OpenRouter::Files
36
+
37
+ def resolve_protocol(name, model, **request)
38
+ return fetch_protocol(:chat_completions) if !name && request[:operation]
39
+
40
+ super
41
+ end
11
42
 
12
43
  def api_base
13
44
  @config.openrouter_api_base || 'https://openrouter.ai/api/v1'
14
45
  end
15
46
 
47
+ def batch_api_base
48
+ "#{api_base.sub(%r{/v1/?\z}, '')}/beta/batches"
49
+ end
50
+
16
51
  def headers
17
52
  {
18
- 'Authorization' => "Bearer #{@config.openrouter_api_key}"
53
+ 'Authorization' => "Bearer #{@config.openrouter_api_key}",
54
+ 'HTTP-Referer' => @config.openrouter_app_url || 'https://rubyllm.com',
55
+ 'X-OpenRouter-Title' => @config.openrouter_app_name || 'RubyLLM'
19
56
  }
20
57
  end
21
58
 
22
59
  def parse_error(response)
23
- return if response.body.empty?
60
+ body = parse_error_body(response)
61
+ return unless body
24
62
 
25
- body = try_parse_json(response.body)
26
63
  case body
27
64
  when Hash
28
65
  parse_error_part_message body
29
66
  when Array
30
- body.map do |part|
67
+ messages = body.filter_map do |part|
31
68
  parse_error_part_message part
32
- end.join('. ')
69
+ end.reject(&:empty?)
70
+ messages.join('. ') unless messages.empty?
33
71
  else
34
72
  body
35
73
  end
36
74
  end
37
75
 
76
+ class << self
77
+ def configuration_options
78
+ %i[openrouter_api_key openrouter_api_base openrouter_app_url openrouter_app_name]
79
+ end
80
+
81
+ def configuration_requirements
82
+ %i[openrouter_api_key]
83
+ end
84
+ end
85
+
38
86
  private
39
87
 
40
88
  def parse_error_part_message(part)
41
- message = part.dig('error', 'message')
42
- raw = try_parse_json(part.dig('error', 'metadata', 'raw'))
89
+ return error_message(part) unless part.is_a?(Hash)
90
+
91
+ error = part['error']
92
+ message = error_message(error)
93
+ metadata = error['metadata'] if error.is_a?(Hash)
94
+ return message unless metadata.is_a?(Hash)
95
+
96
+ raw = try_parse_json(metadata['raw'])
43
97
  return message unless raw.is_a?(Hash)
44
98
 
45
- raw_message = raw.dig('error', 'message')
99
+ raw_message = error_message(raw['error'])
46
100
  return [message, raw_message].compact.join(' - ') if raw_message
47
101
 
48
102
  message
49
103
  end
50
104
 
51
- class << self
52
- def configuration_options
53
- %i[openrouter_api_key openrouter_api_base]
54
- end
55
-
56
- def configuration_requirements
57
- %i[openrouter_api_key]
105
+ def error_message(value)
106
+ case value
107
+ when Hash
108
+ value['message']
109
+ when Array
110
+ messages = value.filter_map { |part| error_message(part) }
111
+ messages.join('. ') unless messages.empty?
112
+ when nil
113
+ nil
114
+ else
115
+ value.to_s
58
116
  end
59
117
  end
60
118
  end
@@ -11,15 +11,8 @@ module RubyLLM
11
11
  role.to_s
12
12
  end
13
13
 
14
- def format_messages(messages)
15
- messages.map do |msg|
16
- {
17
- role: format_role(msg.role),
18
- content: Perplexity::Media.format_content(msg.content),
19
- tool_calls: OpenAI::Tools.format_tool_calls(msg.tool_calls),
20
- tool_call_id: msg.tool_call_id
21
- }.compact.merge(OpenAI::Chat.format_thinking(msg))
22
- end
14
+ def format_content(content, attachments = [])
15
+ Perplexity::Media.format_content(content, attachments)
23
16
  end
24
17
  end
25
18
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Embeddings methods of the Perplexity API integration
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(...)
11
+ 'v1/embeddings'
12
+ end
13
+
14
+ # Perplexity serves base64-encoded signed int8 vectors.
15
+ def parse_embedding_response(response, model:, text:)
16
+ data = response.body
17
+ input_tokens = data.dig('usage', 'prompt_tokens')
18
+ vectors = Array(data['data']).map { |vector| decode_embedding(vector['embedding']) }
19
+ vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
20
+
21
+ Embedding.new(vectors:, model:, input_tokens:)
22
+ end
23
+
24
+ def decode_embedding(embedding)
25
+ return embedding unless embedding.is_a?(String)
26
+
27
+ Base64.decode64(embedding).unpack('c*')
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -9,31 +9,19 @@ module RubyLLM
9
9
 
10
10
  SUPPORTED_DOCUMENT_EXTENSIONS = %w[pdf doc docx txt rtf].freeze
11
11
 
12
- def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
13
- if content.is_a?(RubyLLM::Content::Raw)
14
- value = content.value
15
- return value.is_a?(Hash) ? value.to_json : value
16
- end
17
- return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
18
- return content unless content.is_a?(Content)
19
-
20
- parts = []
21
- parts << OpenAI::Media.format_text(content.text) if content.text
22
-
23
- content.attachments.each do |attachment|
12
+ def format_content(content, attachments = [])
13
+ Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
24
14
  case attachment.type
25
15
  when :image
26
- parts << OpenAI::Media.format_image(attachment)
16
+ Protocols::ChatCompletions::Media.format_image(attachment)
27
17
  when :pdf, :document
28
- parts << format_document(attachment)
18
+ format_document(attachment)
29
19
  when :text
30
- parts << format_text_attachment(attachment)
20
+ Protocols::ChatCompletions::Media.format_text_file(attachment)
31
21
  else
32
22
  raise UnsupportedAttachmentError, attachment.mime_type
33
23
  end
34
24
  end
35
-
36
- parts
37
25
  end
38
26
 
39
27
  def format_document(attachment)
@@ -47,10 +35,6 @@ module RubyLLM
47
35
  }
48
36
  end
49
37
 
50
- def format_text_attachment(attachment)
51
- OpenAI::Media.format_text_file(attachment)
52
- end
53
-
54
38
  def supported_file?(attachment)
55
39
  return true if attachment.pdf?
56
40
 
@@ -5,35 +5,102 @@ module RubyLLM
5
5
  class Perplexity
6
6
  # Models methods of the Perplexity API integration
7
7
  module Models
8
- MODEL_IDS = %w[
8
+ SEARCH_MODEL_IDS = %w[
9
9
  sonar
10
10
  sonar-pro
11
- sonar-reasoning
12
11
  sonar-reasoning-pro
13
12
  sonar-deep-research
14
13
  ].freeze
14
+ EMBEDDING_MODEL_IDS = %w[
15
+ pplx-embed-v1-0.6b
16
+ pplx-embed-v1-4b
17
+ ].freeze
18
+ STATIC_MODEL_IDS = (SEARCH_MODEL_IDS + EMBEDDING_MODEL_IDS).freeze
19
+ STATIC_MODEL_DATA = {
20
+ 'sonar' => {
21
+ context_window: 128_000, input_price: 1.0, output_price: 1.0,
22
+ capabilities: %w[streaming structured_output citations]
23
+ },
24
+ 'sonar-pro' => {
25
+ context_window: 200_000, input_price: 3.0, output_price: 15.0,
26
+ capabilities: %w[streaming structured_output citations]
27
+ },
28
+ 'sonar-reasoning-pro' => {
29
+ context_window: 128_000, input_price: 2.0, output_price: 8.0,
30
+ capabilities: %w[streaming structured_output citations reasoning]
31
+ },
32
+ 'sonar-deep-research' => {
33
+ context_window: 128_000, input_price: 2.0, output_price: 8.0, reasoning_price: 3.0,
34
+ capabilities: %w[streaming structured_output citations reasoning]
35
+ },
36
+ 'pplx-embed-v1-0.6b' => { context_window: 32_768, input_price: 0.004 },
37
+ 'pplx-embed-v1-4b' => { context_window: 32_768, input_price: 0.03 }
38
+ }.freeze
39
+
40
+ def models_url
41
+ 'v1/models'
42
+ end
43
+
44
+ def list_models
45
+ super
46
+ rescue Error => e
47
+ RubyLLM.logger.warn "Perplexity models endpoint failed (#{e.message}). Using the static model list."
48
+ static_models(@provider.slug)
49
+ end
50
+
51
+ # The models endpoint lists the multi-model catalog but not the search
52
+ # or embedding models, so those ride along statically.
53
+ def parse_list_models_response(response, slug)
54
+ listed = Array(response.body['data']).map do |model_data|
55
+ create_model_info(model_data['id'], slug, pricing: endpoint_pricing(model_data['pricing']))
56
+ end
15
57
 
16
- def list_models(**)
17
- slug = 'perplexity'
18
- parse_list_models_response(nil, slug, Perplexity::Capabilities)
58
+ static_models(slug, ids: STATIC_MODEL_IDS - listed.map(&:id)) + listed
19
59
  end
20
60
 
21
- def parse_list_models_response(_response, slug, capabilities)
22
- MODEL_IDS.map { |id| create_model_info(id, slug, capabilities) }
61
+ def static_models(slug, ids: STATIC_MODEL_IDS)
62
+ ids.map { |id| create_model_info(id, slug) }
23
63
  end
24
64
 
25
- def create_model_info(id, slug, capabilities)
26
- Model::Info.new(
65
+ def create_model_info(id, slug, pricing: nil)
66
+ static = STATIC_MODEL_DATA.fetch(id, {})
67
+
68
+ Model.new(
27
69
  id: id,
28
70
  name: id,
29
71
  provider: slug,
30
- context_window: capabilities.context_window_for(id),
31
- max_output_tokens: capabilities.max_tokens_for(id),
32
- capabilities: capabilities.critical_capabilities_for(id),
33
- pricing: capabilities.pricing_for(id),
72
+ context_window: static[:context_window],
73
+ capabilities: static.fetch(:capabilities, []),
74
+ pricing: pricing || static_pricing(static),
75
+ modalities: modalities_for(id),
34
76
  metadata: {}
35
77
  )
36
78
  end
79
+
80
+ def modalities_for(id)
81
+ { input: %w[text], output: %w[embeddings] } if EMBEDDING_MODEL_IDS.include?(id)
82
+ end
83
+
84
+ def endpoint_pricing(pricing)
85
+ return nil unless pricing.is_a?(Hash)
86
+
87
+ standard = {
88
+ input_per_million: pricing['input'],
89
+ output_per_million: pricing['output'],
90
+ cache_read_input_per_million: pricing['cache_read'],
91
+ cache_write_input_per_million: pricing['cache_write']
92
+ }.compact
93
+ { text_tokens: { standard: standard } } unless standard.empty?
94
+ end
95
+
96
+ def static_pricing(data)
97
+ return {} unless data[:input_price]
98
+
99
+ standard = { input_per_million: data[:input_price] }
100
+ standard[:output_per_million] = data[:output_price] if data[:output_price]
101
+ standard[:reasoning_output_per_million] = data[:reasoning_price] if data[:reasoning_price]
102
+ { text_tokens: { standard: standard } }
103
+ end
37
104
  end
38
105
  end
39
106
  end
@@ -3,14 +3,26 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # Perplexity API integration.
6
- class Perplexity < OpenAI
7
- include Perplexity::Chat
8
- include Perplexity::Models
6
+ class Perplexity < Provider
7
+ # Perplexity's dialect of the Chat Completions API.
8
+ class ChatCompletions < Protocols::ChatCompletions
9
+ include Perplexity::Chat
10
+ include Perplexity::Embeddings
11
+ include Perplexity::Models
12
+ end
13
+
14
+ protocol :chat_completions, ChatCompletions
15
+ protocol :router_chat_completions, Protocols::Perplexity::Router
16
+ protocol :files, Protocols::Perplexity::Files
9
17
 
10
18
  def api_base
11
19
  @config.perplexity_api_base || 'https://api.perplexity.ai'
12
20
  end
13
21
 
22
+ def router_url(operation) # :nodoc:
23
+ "#{api_base.delete_suffix('/').delete_suffix('/router/v1')}/router/v1/#{operation}"
24
+ end
25
+
14
26
  def headers
15
27
  {
16
28
  'Authorization' => "Bearer #{@config.perplexity_api_key}",
@@ -18,26 +30,12 @@ module RubyLLM
18
30
  }
19
31
  end
20
32
 
21
- class << self
22
- def capabilities
23
- Perplexity::Capabilities
24
- end
25
-
26
- def configuration_options
27
- %i[perplexity_api_key perplexity_api_base]
28
- end
29
-
30
- def configuration_requirements
31
- %i[perplexity_api_key]
32
- end
33
- end
34
-
35
33
  def parse_error(response)
36
- body = response.body
37
- return if body.empty?
34
+ body = parse_error_body(response)
35
+ return unless body
38
36
 
39
37
  # If response is HTML (Perplexity returns HTML for auth errors)
40
- if body.include?('<html>') && body.include?('<title>')
38
+ if body.is_a?(String) && body.include?('<html>') && body.include?('<title>')
41
39
  title_match = body.match(%r{<title>(.+?)</title>})
42
40
  if title_match
43
41
  message = title_match[1]
@@ -47,6 +45,16 @@ module RubyLLM
47
45
  end
48
46
  super
49
47
  end
48
+
49
+ class << self
50
+ def configuration_options
51
+ %i[perplexity_api_key perplexity_api_base]
52
+ end
53
+
54
+ def configuration_requirements
55
+ %i[perplexity_api_key]
56
+ end
57
+ end
50
58
  end
51
59
  end
52
60
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI < Provider
6
+ class Anthropic
7
+ # Vertex AI Claude batch prediction rows.
8
+ module Batches
9
+ include Protocols::VertexAI::BatchPrediction
10
+
11
+ private
12
+
13
+ def vertex_batch_request(request)
14
+ {
15
+ custom_id: request[:custom_id],
16
+ request: batch_payload(request)
17
+ }
18
+ end
19
+
20
+ def validate_batch_requests!(requests)
21
+ if @config.vertexai_location.to_s == 'global'
22
+ raise ConfigurationError, 'vertexai Anthropic batches require a regional vertexai_location'
23
+ end
24
+
25
+ return if requests.all? { |request| anthropic_batch_payload?(request.fetch(:payload)) }
26
+
27
+ raise Error, 'vertexai Anthropic batch requests require Anthropic message payloads'
28
+ end
29
+
30
+ def anthropic_batch_payload?(payload)
31
+ payload.key?(:messages) || payload.key?('messages')
32
+ end
33
+
34
+ def vertex_batch_model_path(model)
35
+ @provider.model_path(model, publisher: 'anthropic')
36
+ end
37
+
38
+ def parse_vertex_batch_result(line, fallback_index)
39
+ index = vertex_batch_result_index(line, fallback_index)
40
+ body = line.dig('response', 'body') || line['response']
41
+
42
+ if body
43
+ [index, parse_completion_body(body, raw: body)]
44
+ else
45
+ [index, nil, batch_failure(index, line.dig('status', 'message') || batch_error_message(line))]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end