use ai_onboarding::YoungAccountBanner;use anthropic::AnthropicModelMode;use anyhow::{Context as _, Result, anyhow};use chrono::{DateTime, Utc};use client::{Client, UserStore, zed_urls};use cloud_api_types::Plan;use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CLIENT_SUPPORTS_STATUS_STREAM_ENDED_HEADER_NAME, CLIENT_SUPPORTS_X_AI_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, ListModelsResponse, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, ZED_VERSION_HEADER_NAME,};use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::{self, 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, LanguageModelEffortLevel, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, NeedsLlmTokenRefresh, 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::collections::VecDeque;use std::pin::Pin;use std::str::FromStr;use std::sync::Arc;use std::task::Poll;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, OpenAiResponseEventMapper, count_open_ai_tokens, into_open_ai, into_open_ai_response,};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())); } 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>, 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") .header(CLIENT_SUPPORTS_STATUS_STREAM_ENDED_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(); return Ok(PerformLlmCompletionResponse { response, includes_status_messages, }); } if !refreshed_token && response.needs_llm_token_refresh() { token = llm_api_token.refresh(&client).await?; refreshed_token = true; continue; } 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 { code: String, message: String, #[serde(default)] #[serde(deserialize_with = "deserialize_optional_status_code")] upstream_status: Option<StatusCode>, #[serde(default)] retry_after: Option<f64>,} fn deserialize_optional_status_code<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error>where D: serde::Deserializer<'de>,{ let opt: Option<u16> = Option::deserialize(deserializer)?; Ok(opt.and_then(|code| StatusCode::from_u16(code).ok()))} impl From<ApiError> for LanguageModelCompletionError { fn from(error: ApiError) -> Self { if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) { if cloud_error.code.starts_with("upstream_http_") { let status = if let Some(status) = cloud_error.upstream_status { status } else if cloud_error.code.ends_with("_error") { error.status } else { // If there's a status code in the code string (e.g. "upstream_http_429") // then use that; otherwise, see if the JSON contains a status code. cloud_error .code .strip_prefix("upstream_http_") .and_then(|code_str| code_str.parse::<u16>().ok()) .and_then(|code| StatusCode::from_u16(code).ok()) .unwrap_or(error.status) }; return LanguageModelCompletionError::UpstreamProviderError { message: cloud_error.message, status, retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), };Lines 1–500 of 1382. Load more
use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, UserStore, zed_urls}; use cloud_api_types::Plan; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CLIENT_SUPPORTS_STATUS_STREAM_ENDED_HEADER_NAME, CLIENT_SUPPORTS_X_AI_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, ListModelsResponse, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, ZED_VERSION_HEADER_NAME, }; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::{self, 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, LanguageModelEffortLevel, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, NeedsLlmTokenRefresh, 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::collections::VecDeque; use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; use std::task::Poll; 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, OpenAiResponseEventMapper, count_open_ai_tokens, into_open_ai, into_open_ai_response, }; 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())); } 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>, 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") .header(CLIENT_SUPPORTS_STATUS_STREAM_ENDED_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(); return Ok(PerformLlmCompletionResponse { response, includes_status_messages, }); } if !refreshed_token && response.needs_llm_token_refresh() { token = llm_api_token.refresh(&client).await?; refreshed_token = true; continue; } 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 { code: String, message: String, #[serde(default)] #[serde(deserialize_with = "deserialize_optional_status_code")] upstream_status: Option<StatusCode>, #[serde(default)] retry_after: Option<f64>, } fn deserialize_optional_status_code<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error> where D: serde::Deserializer<'de>, { let opt: Option<u16> = Option::deserialize(deserializer)?; Ok(opt.and_then(|code| StatusCode::from_u16(code).ok())) } impl From<ApiError> for LanguageModelCompletionError { fn from(error: ApiError) -> Self { if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) { if cloud_error.code.starts_with("upstream_http_") { let status = if let Some(status) = cloud_error.upstream_status { status } else if cloud_error.code.ends_with("_error") { error.status } else { // If there's a status code in the code string (e.g. "upstream_http_429") // then use that; otherwise, see if the JSON contains a status code. cloud_error .code .strip_prefix("upstream_http_") .and_then(|code_str| code_str.parse::<u16>().ok()) .and_then(|code| StatusCode::from_u16(code).ok()) .unwrap_or(error.status) }; return LanguageModelCompletionError::UpstreamProviderError { message: cloud_error.message, status, retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), };