diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index 152b84679d..a33a2e8cc1 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -316,6 +316,10 @@ pub struct LanguageModel { /// Only used by OpenAI and xAI. #[serde(default)] pub supports_parallel_tool_calls: bool, + #[serde(default)] + pub is_disabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disabled_reason: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 52486b30ef..cdd4bb6c8a 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -662,6 +662,31 @@ mod tests { sign_in_task } + fn test_cloud_model( + model_id: cloud_llm_client::LanguageModelId, + ) -> cloud_llm_client::LanguageModel { + cloud_llm_client::LanguageModel { + provider: cloud_llm_client::LanguageModelProvider::Anthropic, + id: model_id, + display_name: "Test Model".to_string(), + is_latest: true, + max_token_count: 200_000, + max_token_count_in_max_mode: None, + max_output_tokens: 8_192, + supports_tools: true, + supports_images: false, + supports_thinking: false, + supports_disabling_thinking: false, + supports_fast_mode: false, + supports_server_side_compaction: false, + supported_effort_levels: Vec::new(), + supports_streaming_tools: false, + supports_parallel_tool_calls: false, + is_disabled: false, + disabled_reason: None, + } + } + #[gpui::test] async fn provider_authenticate_does_not_start_sign_in_when_signed_out(cx: &mut TestAppContext) { let (client, _user_store, provider) = cx.update(init_test); @@ -754,6 +779,41 @@ mod tests { assert!(error.to_string().contains("AuthenticationError")); } + #[gpui::test] + async fn provided_models_surface_disabled_reason(cx: &mut TestAppContext) { + let (_client, _user_store, provider) = cx.update(init_test); + let model_id = cloud_llm_client::LanguageModelId(Arc::from("disabled-model")); + let disabled_reason = "This model is temporarily unavailable."; + + cx.update(|cx| { + let cloud_model_provider = provider.state.read(cx).provider.clone(); + cloud_model_provider.update(cx, |cloud_model_provider, cx| { + let mut model = test_cloud_model(model_id.clone()); + model.is_disabled = true; + model.disabled_reason = Some(disabled_reason.to_string()); + cloud_model_provider.update_models(cloud_llm_client::ListModelsResponse { + models: vec![model], + default_model: Some(model_id.clone()), + default_fast_model: None, + recommended_models: vec![model_id], + }); + cx.notify(); + }); + }); + + let model = cx.read(|cx| { + provider + .provided_models(cx) + .into_iter() + .next() + .expect("disabled model should be provided") + }); + assert_eq!( + model.is_disabled(), + Some(language_model::DisabledReason::new(disabled_reason)) + ); + } + #[gpui::test] async fn sign_out_hides_cached_cloud_models(cx: &mut TestAppContext) { let (client, _user_store, provider) = cx.update(init_test); @@ -780,24 +840,7 @@ mod tests { let cloud_model_provider = provider.state.read(cx).provider.clone(); cloud_model_provider.update(cx, |cloud_model_provider, cx| { cloud_model_provider.update_models(cloud_llm_client::ListModelsResponse { - models: vec![cloud_llm_client::LanguageModel { - provider: cloud_llm_client::LanguageModelProvider::Anthropic, - id: model_id.clone(), - display_name: "Test Model".to_string(), - is_latest: true, - max_token_count: 200_000, - max_token_count_in_max_mode: None, - max_output_tokens: 8_192, - supports_tools: true, - supports_images: false, - supports_thinking: false, - supports_disabling_thinking: false, - supports_fast_mode: false, - supports_server_side_compaction: false, - supported_effort_levels: Vec::new(), - supports_streaming_tools: false, - supports_parallel_tool_calls: false, - }], + models: vec![test_cloud_model(model_id.clone())], default_model: Some(model_id.clone()), default_fast_model: None, recommended_models: vec![model_id], diff --git a/crates/language_models_cloud/src/language_models_cloud.rs b/crates/language_models_cloud/src/language_models_cloud.rs index 0920f41821..07be3150c0 100644 --- a/crates/language_models_cloud/src/language_models_cloud.rs +++ b/crates/language_models_cloud/src/language_models_cloud.rs @@ -19,12 +19,13 @@ use http_client::{ AsyncBody, HttpClient, HttpClientWithUrl, HttpRequestExt, Method, Response, StatusCode, }; use language_model::{ - ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, GOOGLE_PROVIDER_ID, GOOGLE_PROVIDER_NAME, - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelEffortLevel, LanguageModelId, LanguageModelName, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolSchemaFormat, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME, RateLimiter, - X_AI_PROVIDER_ID, X_AI_PROVIDER_NAME, ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME, + ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, DisabledReason, GOOGLE_PROVIDER_ID, + GOOGLE_PROVIDER_NAME, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, OPEN_AI_PROVIDER_ID, + OPEN_AI_PROVIDER_NAME, RateLimiter, X_AI_PROVIDER_ID, X_AI_PROVIDER_NAME, + ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME, }; use schemars::JsonSchema; @@ -322,6 +323,14 @@ impl LanguageModel for CloudLanguageModel Option { + if self.model.is_disabled { + self.model.disabled_reason.clone().map(DisabledReason::new) + } else { + None + } + } + fn requires_data_retention(&self) -> bool { // Anthropic cannot offer Fable models with Zero Data Retention self.id