language_models: Add provider options for OpenRouter models (#37979)

Umesh Yadav and Aurelien Tollard created

Supersedes: #34500

Also this will allow to fix this: #35386 without the UX changes but
providers can now be control through settings as well within zed.

Just rebased the latest main and docs added. Added @AurelienTollard as
co-author as it was started by him everything else remains the same from
original PR.

Release Notes:

- Added ability to control Provider Routing for OpenRouter models from
settings.

Co-authored-by: Aurelien Tollard <tollard.aurelien1999@gmail.com>

Change summary

Cargo.lock                                         |  1 
crates/language_models/src/provider/open_router.rs |  6 +
crates/open_router/Cargo.toml                      |  1 
crates/open_router/src/open_router.rs              | 42 ++++++++++++++
docs/src/ai/llm-providers.md                       | 47 ++++++++++++++++
5 files changed, 96 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -11244,6 +11244,7 @@ dependencies = [
  "serde_json",
  "strum 0.27.1",
  "thiserror 2.0.12",
+ "util",
  "workspace-hack",
 ]
 

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<bool>,
     pub supports_images: Option<bool>,
     pub mode: Option<ModelMode>,
+    pub provider: Option<Provider>,
 }
 
 #[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(),
     }
 }
 

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

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<Role> 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<Vec<String>>,
+    #[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<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    ignore: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    quantizations: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    sort: Option<String>,
+}
+
 #[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<bool>,
     #[serde(default)]
     pub mode: ModelMode,
+    pub provider: Option<Provider>,
 }
 
 #[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<bool>,
         supports_images: Option<bool>,
         mode: Option<ModelMode>,
+        provider: Option<Provider>,
     ) -> 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<Reasoning>,
     pub usage: RequestUsage,
+    pub provider: Option<Provider>,
 }
 
 #[derive(Debug, Default, Serialize, Deserialize)]
@@ -597,6 +638,7 @@ pub async fn list_models(
                 } else {
                     ModelMode::Default
                 },
+                provider: None,
             })
             .collect();
 

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.