use ai_onboarding::YoungAccountBanner;use anthropic::AnthropicModelMode;use anyhow::{Context as _, Result, anyhow};use chrono::{DateTime, Utc};use client::{Client, ModelRequestUsage, UserStore, zed_urls};use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CLIENT_SUPPORTS_X_AI_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan, PlanV1, PlanV2, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,};use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,};use google_ai::GoogleModelMode;use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Task};use http_client::http::{HeaderMap, HeaderValue};use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};use language_model::{ AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,};use release_channel::AppVersion;use schemars::JsonSchema;use semver::Version;use serde::{Deserialize, Serialize, de::DeserializeOwned};use settings::SettingsStore;pub use settings::ZedDotDevAvailableModel as AvailableModel;pub use settings::ZedDotDevAvailableProvider as AvailableProvider;use smol::io::{AsyncReadExt, BufReader};use std::pin::Pin;use std::str::FromStr as _;use std::sync::Arc;use std::time::Duration;use thiserror::Error;use ui::{TintColor, prelude::*};use util::{ResultExt as _, maybe}; use crate::provider::anthropic::{ AnthropicEventMapper, count_anthropic_tokens_with_tiktoken, into_anthropic,};use crate::provider::google::{GoogleEventMapper, into_google};use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};use crate::provider::x_ai::count_xai_tokens; const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)]pub struct ZedDotDevSettings { pub available_models: Vec<AvailableModel>,}#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]#[serde(tag = "type", rename_all = "lowercase")]pub enum ModelMode { #[default] Default, Thinking { /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. budget_tokens: Option<u32>, },} impl From<ModelMode> for AnthropicModelMode { fn from(value: ModelMode) -> Self { match value { ModelMode::Default => AnthropicModelMode::Default, ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens }, } }} pub struct CloudLanguageModelProvider { client: Arc<Client>, state: Entity<State>, _maintain_client_status: Task<()>,} pub struct State { client: Arc<Client>, llm_api_token: LlmApiToken, user_store: Entity<UserStore>, status: client::Status, models: Vec<Arc<cloud_llm_client::LanguageModel>>, default_model: Option<Arc<cloud_llm_client::LanguageModel>>, default_fast_model: Option<Arc<cloud_llm_client::LanguageModel>>, recommended_models: Vec<Arc<cloud_llm_client::LanguageModel>>, _fetch_models_task: Task<()>, _settings_subscription: Subscription, _llm_token_subscription: Subscription,} impl State { fn new( client: Arc<Client>, user_store: Entity<UserStore>, status: client::Status, cx: &mut Context<Self>, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); let mut current_user = user_store.read(cx).watch_current_user(); Self { client: client.clone(), llm_api_token: LlmApiToken::default(), user_store, status, models: Vec::new(), default_model: None, default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; while current_user.borrow().is_none() { current_user.next().await; } let response = Self::fetch_models(client.clone(), llm_api_token.clone()).await?; this.update(cx, |this, cx| this.update_models(response, cx))?; anyhow::Ok(()) }) .await .context("failed to fetch Zed models") .log_err(); }), _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| { cx.notify(); }), _llm_token_subscription: cx.subscribe( &refresh_llm_token_listener, move |this, _listener, _event, cx| { let client = this.client.clone(); let llm_api_token = this.llm_api_token.clone(); cx.spawn(async move |this, cx| { llm_api_token.refresh(&client).await?; let response = Self::fetch_models(client, llm_api_token).await?; this.update(cx, |this, cx| { this.update_models(response, cx); }) }) .detach_and_log_err(cx); }, ), } } fn is_signed_out(&self, cx: &App) -> bool { self.user_store.read(cx).current_user().is_none() } fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let client = self.client.clone(); cx.spawn(async move |state, cx| { client.sign_in_with_optional_connect(true, cx).await?; state.update(cx, |_, cx| cx.notify()) }) } fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context<Self>) { let mut models = Vec::new(); for model in response.models { models.push(Arc::new(model.clone())); // Right now we represent thinking variants of models as separate models on the client, // so we need to insert variants for any model that supports thinking. if model.supports_thinking { models.push(Arc::new(cloud_llm_client::LanguageModel { id: cloud_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()), display_name: format!("{} Thinking", model.display_name), ..model })); } } self.default_model = models .iter() .find(|model| { response .default_model .as_ref() .is_some_and(|default_model_id| &model.id == default_model_id) }) .cloned(); self.default_fast_model = models .iter() .find(|model| { response .default_fast_model .as_ref() .is_some_and(|default_fast_model_id| &model.id == default_fast_model_id) }) .cloned(); self.recommended_models = response .recommended_models .iter() .filter_map(|id| models.iter().find(|model| &model.id == id)) .cloned() .collect(); self.models = models; cx.notify(); } async fn fetch_models( client: Arc<Client>, llm_api_token: LlmApiToken, ) -> Result<ListModelsResponse> { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; let request = http_client::Request::builder() .method(Method::GET) .header(CLIENT_SUPPORTS_X_AI_HEADER_NAME, "true") .uri(http_client.build_zed_llm_url("/models", &[])?.as_ref()) .header("Authorization", format!("Bearer {token}")) .body(AsyncBody::empty())?; let mut response = http_client .send(request) .await .context("failed to send list models request")?; if response.status().is_success() { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; Ok(serde_json::from_str(&body)?) } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; anyhow::bail!( "error listing models.\nStatus: {:?}\nBody: {body}", response.status(), ); } }} impl CloudLanguageModelProvider { pub fn new(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) -> Self { let mut status_rx = client.status(); let status = *status_rx.borrow(); let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx)); let state_ref = state.downgrade(); let maintain_client_status = cx.spawn(async move |cx| { while let Some(status) = status_rx.next().await { if let Some(this) = state_ref.upgrade() { _ = this.update(cx, |this, cx| { if this.status != status { this.status = status; cx.notify(); } }); } else { break; } } }); Self { client, state, _maintain_client_status: maintain_client_status, } } fn create_language_model( &self, model: Arc<cloud_llm_client::LanguageModel>, llm_api_token: LlmApiToken, ) -> Arc<dyn LanguageModel> { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, llm_api_token, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) }} impl LanguageModelProviderState for CloudLanguageModelProvider { type ObservableEntity = State; fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> { Some(self.state.clone()) }} impl LanguageModelProvider for CloudLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn icon(&self) -> IconOrSvg { IconOrSvg::Icon(IconName::AiZed) } fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> { let default_model = self.state.read(cx).default_model.clone()?; let llm_api_token = self.state.read(cx).llm_api_token.clone(); Some(self.create_language_model(default_model, llm_api_token)) } fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> { let default_fast_model = self.state.read(cx).default_fast_model.clone()?; let llm_api_token = self.state.read(cx).llm_api_token.clone(); Some(self.create_language_model(default_fast_model, llm_api_token)) } fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let llm_api_token = self.state.read(cx).llm_api_token.clone(); self.state .read(cx) .recommended_models .iter() .cloned() .map(|model| self.create_language_model(model, llm_api_token.clone())) .collect() } fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let llm_api_token = self.state.read(cx).llm_api_token.clone(); self.state .read(cx) .models .iter() .cloned() .map(|model| self.create_language_model(model, llm_api_token.clone())) .collect() } fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); !state.is_signed_out(cx) } fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> { Task::ready(Ok(())) } fn configuration_view( &self, _target_agent: language_model::ConfigurationViewTargetAgent, _: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|_| ConfigurationView::new(self.state.clone())) .into() } fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> { Task::ready(Ok(())) }} pub struct CloudLanguageModel { id: LanguageModelId, model: Arc<cloud_llm_client::LanguageModel>, llm_api_token: LlmApiToken, client: Arc<Client>, request_limiter: RateLimiter,} struct PerformLlmCompletionResponse { response: Response<AsyncBody>, usage: Option<ModelRequestUsage>, tool_use_limit_reached: bool, includes_status_messages: bool,} impl CloudLanguageModel { async fn perform_llm_completion( client: Arc<Client>, llm_api_token: LlmApiToken, app_version: Option<Version>, body: CompletionBody, ) -> Result<PerformLlmCompletionResponse> { let http_client = &client.http_client(); let mut token = llm_api_token.acquire(&client).await?; let mut refreshed_token = false; loop { let request = http_client::Request::builder() .method(Method::POST) .uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref()) .when_some(app_version.as_ref(), |builder, app_version| { builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string()) }) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {token}")) .header(CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, "true") .body(serde_json::to_string(&body)?.into())?; let mut response = http_client.send(request).await?; let status = response.status(); if status.is_success() { let includes_status_messages = response .headers() .get(SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME) .is_some(); let tool_use_limit_reached = response .headers() .get(TOOL_USE_LIMIT_REACHED_HEADER_NAME) .is_some(); let usage = if includes_status_messages { None } else { ModelRequestUsage::from_headers(response.headers()).ok() }; return Ok(PerformLlmCompletionResponse { response, usage, includes_status_messages, tool_use_limit_reached, }); } if !refreshed_token && response .headers() .get(EXPIRED_LLM_TOKEN_HEADER_NAME) .is_some() { token = llm_api_token.refresh(&client).await?; refreshed_token = true; continue; } if status == StatusCode::FORBIDDEN && response .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .is_some() { if let Some(MODEL_REQUESTS_RESOURCE_HEADER_VALUE) = response .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .and_then(|resource| resource.to_str().ok()) && let Some(plan) = response .headers() .get(CURRENT_PLAN_HEADER_NAME) .and_then(|plan| plan.to_str().ok()) .and_then(|plan| cloud_llm_client::PlanV1::from_str(plan).ok()) .map(Plan::V1) { return Err(anyhow!(ModelRequestLimitReachedError { plan })); } } else if status == StatusCode::PAYMENT_REQUIRED { return Err(anyhow!(PaymentRequiredError)); } let mut body = String::new(); let headers = response.headers().clone(); response.body_mut().read_to_string(&mut body).await?; return Err(anyhow!(ApiError { status, body, headers })); } }} #[derive(Debug, Error)]#[error("cloud language model request failed with status {status}: {body}")]struct ApiError { status: StatusCode, body: String, headers: HeaderMap<HeaderValue>,} /// Represents error responses from Zed's cloud API.////// Example JSON for an upstream HTTP error:/// ```json/// {/// "code": "upstream_http_error",/// "message": "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout",/// "upstream_status": 503/// }/// ```#[derive(Debug, serde::Deserialize)]struct CloudApiError {Lines 1–500 of 1402. Load more
use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, ModelRequestUsage, UserStore, zed_urls}; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CLIENT_SUPPORTS_X_AI_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan, PlanV1, PlanV2, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, }; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; use google_ai::GoogleModelMode; use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Task}; use http_client::http::{HeaderMap, HeaderValue}; use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode}; use language_model::{ AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; use semver::Version; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use settings::SettingsStore; pub use settings::ZedDotDevAvailableModel as AvailableModel; pub use settings::ZedDotDevAvailableProvider as AvailableProvider; use smol::io::{AsyncReadExt, BufReader}; use std::pin::Pin; use std::str::FromStr as _; use std::sync::Arc; use std::time::Duration; use thiserror::Error; use ui::{TintColor, prelude::*}; use util::{ResultExt as _, maybe}; use crate::provider::anthropic::{ AnthropicEventMapper, count_anthropic_tokens_with_tiktoken, into_anthropic, }; use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; use crate::provider::x_ai::count_xai_tokens; const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { pub available_models: Vec<AvailableModel>, } #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ModelMode { #[default] Default, Thinking { /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. budget_tokens: Option<u32>, }, } impl From<ModelMode> for AnthropicModelMode { fn from(value: ModelMode) -> Self { match value { ModelMode::Default => AnthropicModelMode::Default, ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens }, } } } pub struct CloudLanguageModelProvider { client: Arc<Client>, state: Entity<State>, _maintain_client_status: Task<()>, } pub struct State { client: Arc<Client>, llm_api_token: LlmApiToken, user_store: Entity<UserStore>, status: client::Status, models: Vec<Arc<cloud_llm_client::LanguageModel>>, default_model: Option<Arc<cloud_llm_client::LanguageModel>>, default_fast_model: Option<Arc<cloud_llm_client::LanguageModel>>, recommended_models: Vec<Arc<cloud_llm_client::LanguageModel>>, _fetch_models_task: Task<()>, _settings_subscription: Subscription, _llm_token_subscription: Subscription, } impl State { fn new( client: Arc<Client>, user_store: Entity<UserStore>, status: client::Status, cx: &mut Context<Self>, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); let mut current_user = user_store.read(cx).watch_current_user(); Self { client: client.clone(), llm_api_token: LlmApiToken::default(), user_store, status, models: Vec::new(), default_model: None, default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; while current_user.borrow().is_none() { current_user.next().await; } let response = Self::fetch_models(client.clone(), llm_api_token.clone()).await?; this.update(cx, |this, cx| this.update_models(response, cx))?; anyhow::Ok(()) }) .await .context("failed to fetch Zed models") .log_err(); }), _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| { cx.notify(); }), _llm_token_subscription: cx.subscribe( &refresh_llm_token_listener, move |this, _listener, _event, cx| { let client = this.client.clone(); let llm_api_token = this.llm_api_token.clone(); cx.spawn(async move |this, cx| { llm_api_token.refresh(&client).await?; let response = Self::fetch_models(client, llm_api_token).await?; this.update(cx, |this, cx| { this.update_models(response, cx); }) }) .detach_and_log_err(cx); }, ), } } fn is_signed_out(&self, cx: &App) -> bool { self.user_store.read(cx).current_user().is_none() } fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let client = self.client.clone(); cx.spawn(async move |state, cx| { client.sign_in_with_optional_connect(true, cx).await?; state.update(cx, |_, cx| cx.notify()) }) } fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context<Self>) { let mut models = Vec::new(); for model in response.models { models.push(Arc::new(model.clone())); // Right now we represent thinking variants of models as separate models on the client, // so we need to insert variants for any model that supports thinking. if model.supports_thinking { models.push(Arc::new(cloud_llm_client::LanguageModel { id: cloud_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()), display_name: format!("{} Thinking", model.display_name), ..model })); } } self.default_model = models .iter() .find(|model| { response .default_model .as_ref() .is_some_and(|default_model_id| &model.id == default_model_id) }) .cloned(); self.default_fast_model = models .iter() .find(|model| { response .default_fast_model .as_ref() .is_some_and(|default_fast_model_id| &model.id == default_fast_model_id) }) .cloned(); self.recommended_models = response .recommended_models .iter() .filter_map(|id| models.iter().find(|model| &model.id == id)) .cloned() .collect(); self.models = models; cx.notify(); } async fn fetch_models( client: Arc<Client>, llm_api_token: LlmApiToken, ) -> Result<ListModelsResponse> { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; let request = http_client::Request::builder() .method(Method::GET) .header(CLIENT_SUPPORTS_X_AI_HEADER_NAME, "true") .uri(http_client.build_zed_llm_url("/models", &[])?.as_ref()) .header("Authorization", format!("Bearer {token}")) .body(AsyncBody::empty())?; let mut response = http_client .send(request) .await .context("failed to send list models request")?; if response.status().is_success() { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; Ok(serde_json::from_str(&body)?) } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; anyhow::bail!( "error listing models.\nStatus: {:?}\nBody: {body}", response.status(), ); } } } impl CloudLanguageModelProvider { pub fn new(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) -> Self { let mut status_rx = client.status(); let status = *status_rx.borrow(); let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx)); let state_ref = state.downgrade(); let maintain_client_status = cx.spawn(async move |cx| { while let Some(status) = status_rx.next().await { if let Some(this) = state_ref.upgrade() { _ = this.update(cx, |this, cx| { if this.status != status { this.status = status; cx.notify(); } }); } else { break; } } }); Self { client, state, _maintain_client_status: maintain_client_status, } } fn create_language_model( &self, model: Arc<cloud_llm_client::LanguageModel>, llm_api_token: LlmApiToken, ) -> Arc<dyn LanguageModel> { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, llm_api_token, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) } } impl LanguageModelProviderState for CloudLanguageModelProvider { type ObservableEntity = State; fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> { Some(self.state.clone()) } } impl LanguageModelProvider for CloudLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn icon(&self) -> IconOrSvg { IconOrSvg::Icon(IconName::AiZed) } fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> { let default_model = self.state.read(cx).default_model.clone()?; let llm_api_token = self.state.read(cx).llm_api_token.clone(); Some(self.create_language_model(default_model, llm_api_token)) } fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> { let default_fast_model = self.state.read(cx).default_fast_model.clone()?; let llm_api_token = self.state.read(cx).llm_api_token.clone(); Some(self.create_language_model(default_fast_model, llm_api_token)) } fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let llm_api_token = self.state.read(cx).llm_api_token.clone(); self.state .read(cx) .recommended_models .iter() .cloned() .map(|model| self.create_language_model(model, llm_api_token.clone())) .collect() } fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let llm_api_token = self.state.read(cx).llm_api_token.clone(); self.state .read(cx) .models .iter() .cloned() .map(|model| self.create_language_model(model, llm_api_token.clone())) .collect() } fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); !state.is_signed_out(cx) } fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> { Task::ready(Ok(())) } fn configuration_view( &self, _target_agent: language_model::ConfigurationViewTargetAgent, _: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|_| ConfigurationView::new(self.state.clone())) .into() } fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> { Task::ready(Ok(())) } } pub struct CloudLanguageModel { id: LanguageModelId, model: Arc<cloud_llm_client::LanguageModel>, llm_api_token: LlmApiToken, client: Arc<Client>, request_limiter: RateLimiter, } struct PerformLlmCompletionResponse { response: Response<AsyncBody>, usage: Option<ModelRequestUsage>, tool_use_limit_reached: bool, includes_status_messages: bool, } impl CloudLanguageModel { async fn perform_llm_completion( client: Arc<Client>, llm_api_token: LlmApiToken, app_version: Option<Version>, body: CompletionBody, ) -> Result<PerformLlmCompletionResponse> { let http_client = &client.http_client(); let mut token = llm_api_token.acquire(&client).await?; let mut refreshed_token = false; loop { let request = http_client::Request::builder() .method(Method::POST) .uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref()) .when_some(app_version.as_ref(), |builder, app_version| { builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string()) }) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {token}")) .header(CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, "true") .body(serde_json::to_string(&body)?.into())?; let mut response = http_client.send(request).await?; let status = response.status(); if status.is_success() { let includes_status_messages = response .headers() .get(SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME) .is_some(); let tool_use_limit_reached = response .headers() .get(TOOL_USE_LIMIT_REACHED_HEADER_NAME) .is_some(); let usage = if includes_status_messages { None } else { ModelRequestUsage::from_headers(response.headers()).ok() }; return Ok(PerformLlmCompletionResponse { response, usage, includes_status_messages, tool_use_limit_reached, }); } if !refreshed_token && response .headers() .get(EXPIRED_LLM_TOKEN_HEADER_NAME) .is_some() { token = llm_api_token.refresh(&client).await?; refreshed_token = true; continue; } if status == StatusCode::FORBIDDEN && response .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .is_some() { if let Some(MODEL_REQUESTS_RESOURCE_HEADER_VALUE) = response .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .and_then(|resource| resource.to_str().ok()) && let Some(plan) = response .headers() .get(CURRENT_PLAN_HEADER_NAME) .and_then(|plan| plan.to_str().ok()) .and_then(|plan| cloud_llm_client::PlanV1::from_str(plan).ok()) .map(Plan::V1) { return Err(anyhow!(ModelRequestLimitReachedError { plan })); } } else if status == StatusCode::PAYMENT_REQUIRED { return Err(anyhow!(PaymentRequiredError)); } let mut body = String::new(); let headers = response.headers().clone(); response.body_mut().read_to_string(&mut body).await?; return Err(anyhow!(ApiError { status, body, headers })); } } } #[derive(Debug, Error)] #[error("cloud language model request failed with status {status}: {body}")] struct ApiError { status: StatusCode, body: String, headers: HeaderMap<HeaderValue>, } /// Represents error responses from Zed's cloud API. /// /// Example JSON for an upstream HTTP error: /// ```json /// { /// "code": "upstream_http_error", /// "message": "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout", /// "upstream_status": 503 /// } /// ``` #[derive(Debug, serde::Deserialize)] struct CloudApiError {