language_models: Use `/models/user` for fetching OpenRouter models (#37534)

Umesh Yadav created

This PR switches the OpenRouter integration from fetching all models to
fetching only the models specified in the user's account preferences.
This will help improve the experience

**The Problem**

The previous implementation used the `/models` endpoint, which returned
an exhaustive list of all models supported by OpenRouter. This resulted
in a long and cluttered model selection dropdown in Zed, making it
difficult for users to find the models they actually use.

**The Solution**

We now use the `/models/user` endpoint. This API call returns a curated
list based on the models and providers the user has selected in their
[OpenRouter dashboard](https://openrouter.ai/models).

Ref: [OpenRouter API Docs for User-Filtered
Models](https://openrouter.ai/docs/api-reference/list-models-filtered-by-user-provider-preferences)

Release Notes:

- language_models: Support OpenRouter user preferences for available
models

Change summary

crates/language_models/src/provider/open_router.rs | 25 ++++++++++++---
crates/open_router/src/open_router.rs              |  8 +++-
2 files changed, 26 insertions(+), 7 deletions(-)

Detailed changes

crates/language_models/src/provider/open_router.rs 🔗

@@ -92,7 +92,7 @@ pub struct State {
     api_key_from_env: bool,
     http_client: Arc<dyn HttpClient>,
     available_models: Vec<open_router::Model>,
-    fetch_models_task: Option<Task<Result<()>>>,
+    fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
     settings: OpenRouterSettings,
     _subscription: Subscription,
 }
@@ -178,20 +178,35 @@ impl State {
         })
     }
 
-    fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+    fn fetch_models(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<(), LanguageModelCompletionError>> {
         let settings = &AllLanguageModelSettings::get_global(cx).open_router;
         let http_client = self.http_client.clone();
         let api_url = settings.api_url.clone();
-
+        let Some(api_key) = self.api_key.clone() else {
+            return Task::ready(Err(LanguageModelCompletionError::NoApiKey {
+                provider: PROVIDER_NAME,
+            }));
+        };
         cx.spawn(async move |this, cx| {
-            let models = list_models(http_client.as_ref(), &api_url)
+            let models = list_models(http_client.as_ref(), &api_url, &api_key)
                 .await
-                .map_err(|e| anyhow::anyhow!("OpenRouter error: {:?}", e))?;
+                .map_err(|e| {
+                    LanguageModelCompletionError::Other(anyhow::anyhow!(
+                        "OpenRouter error: {:?}",
+                        e
+                    ))
+                })?;
 
             this.update(cx, |this, cx| {
                 this.available_models = models;
                 cx.notify();
             })
+            .map_err(|e| LanguageModelCompletionError::Other(e))?;
+
+            Ok(())
         })
     }
 

crates/open_router/src/open_router.rs 🔗

@@ -529,12 +529,16 @@ pub async fn stream_completion(
 pub async fn list_models(
     client: &dyn HttpClient,
     api_url: &str,
+    api_key: &str,
 ) -> Result<Vec<Model>, OpenRouterError> {
-    let uri = format!("{api_url}/models");
+    let uri = format!("{api_url}/models/user");
     let request_builder = HttpRequest::builder()
         .method(Method::GET)
         .uri(uri)
-        .header("Accept", "application/json");
+        .header("Accept", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .header("HTTP-Referer", "https://zed.dev")
+        .header("X-Title", "Zed Editor");
 
     let request = request_builder
         .body(AsyncBody::default())