diff --git a/Cargo.lock b/Cargo.lock index c8bde05dd820147181cbb51e4abc67b29f852cc2..227e4127b6ad25501c0af1161d790caf7db0425e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11244,6 +11244,7 @@ dependencies = [ "serde_json", "strum 0.27.1", "thiserror 2.0.12", + "util", "workspace-hack", ] diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 698e9d23cc74c56b00daa48359b663ff034c1abe..0ebf379b393791558cba6ff36ab31d278162386e 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -15,7 +15,8 @@ use language_model::{ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; use open_router::{ - Model, ModelMode as OpenRouterModelMode, ResponseStreamEvent, list_models, stream_completion, + Model, ModelMode as OpenRouterModelMode, Provider, ResponseStreamEvent, list_models, + stream_completion, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -48,6 +49,7 @@ pub struct AvailableModel { pub supports_tools: Option, pub supports_images: Option, pub mode: Option, + pub provider: Option, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -296,6 +298,7 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { supports_tools: model.supports_tools, supports_images: model.supports_images, mode: model.mode.clone().unwrap_or_default().into(), + provider: model.provider.clone(), }); } @@ -584,6 +587,7 @@ pub fn into_open_router( LanguageModelToolChoice::Any => open_router::ToolChoice::Required, LanguageModelToolChoice::None => open_router::ToolChoice::None, }), + provider: model.provider.clone(), } } diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml index 8920c157dc3d6ea0974bd978816eb58cde19919d..54348ea5a598d5c92a30ddd60ea68d3fa7b6046f 100644 --- a/crates/open_router/Cargo.toml +++ b/crates/open_router/Cargo.toml @@ -24,4 +24,5 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true strum.workspace = true +util.workspace = true workspace-hack.workspace = true diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index cbc6c243d87c8f9ea3d0186dbecb8f0ac2e10a90..2e5f33b161b5947eceb64c9b2c2c4ae81116b813 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -6,6 +6,7 @@ use serde_json::Value; use std::{convert::TryFrom, io, time::Duration}; use strum::EnumString; use thiserror::Error; +use util::serde::default_true; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; @@ -64,6 +65,41 @@ impl From for String { } } +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DataCollection { + Allow, + Disallow, +} + +impl Default for DataCollection { + fn default() -> Self { + Self::Allow + } +} + +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Provider { + #[serde(skip_serializing_if = "Option::is_none")] + order: Option>, + #[serde(default = "default_true")] + allow_fallbacks: bool, + #[serde(default)] + require_parameters: bool, + #[serde(default)] + data_collection: DataCollection, + #[serde(skip_serializing_if = "Option::is_none")] + only: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + ignore: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + quantizations: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + sort: Option, +} + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Model { @@ -74,6 +110,7 @@ pub struct Model { pub supports_images: Option, #[serde(default)] pub mode: ModelMode, + pub provider: Option, } #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -95,6 +132,7 @@ impl Model { Some(true), Some(false), Some(ModelMode::Default), + None, ) } @@ -109,6 +147,7 @@ impl Model { supports_tools: Option, supports_images: Option, mode: Option, + provider: Option, ) -> Self { Self { name: name.to_owned(), @@ -117,6 +156,7 @@ impl Model { supports_tools, supports_images, mode: mode.unwrap_or(ModelMode::Default), + provider, } } @@ -164,6 +204,7 @@ pub struct Request { #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning: Option, pub usage: RequestUsage, + pub provider: Option, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -597,6 +638,7 @@ pub async fn list_models( } else { ModelMode::Default }, + provider: None, }) .collect(); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 2846ab2f2ffda3645dea07e3c7e51803b6177018..98aaeef2126d559efa7696143faca13d39e11e62 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -524,6 +524,53 @@ You can find available models and their specifications on the [OpenRouter models Custom models will be listed in the model dropdown in the Agent Panel. +#### Provider Routing + +You can optionally control how OpenRouter routes a given custom model request among underlying upstream providers via the `provider` object on each model entry. + +Supported fields (all optional): + +- `order`: Array of provider slugs to try first, in order (e.g. `["anthropic", "openai"]`) +- `allow_fallbacks` (default: `true`): Whether fallback providers may be used if preferred ones are unavailable +- `require_parameters` (default: `false`): Only use providers that support every parameter you supplied +- `data_collection` (default: `allow`): `"allow"` or `"disallow"` (controls use of providers that may store data) +- `only`: Whitelist of provider slugs allowed for this request +- `ignore`: Provider slugs to skip +- `quantizations`: Restrict to specific quantization variants (e.g. `["int4","int8"]`) +- `sort`: Sort strategy for candidate providers (e.g. `"price"` or `"throughput"`) + +Example adding routing preferences to a model: + +```json +{ + "language_models": { + "open_router": { + "api_url": "https://openrouter.ai/api/v1", + "available_models": [ + { + "name": "openrouter/auto", + "display_name": "Auto Router (Tools Preferred)", + "max_tokens": 2000000, + "supports_tools": true, + "provider": { + "order": ["anthropic", "openai"], + "allow_fallbacks": true, + "require_parameters": true, + "only": ["anthropic", "openai", "google"], + "ignore": ["cohere"], + "quantizations": ["int8"], + "sort": "price", + "data_collection": "allow" + } + } + ] + } + } +} +``` + +These routing controls let you fine‑tune cost, capability, and reliability trade‑offs without changing the model name you select in the UI. + ### Vercel v0 {#vercel-v0} [Vercel v0](https://vercel.com/docs/v0/api) is an expert model for generating full-stack apps, with framework-aware completions optimized for modern stacks like Next.js and Vercel.