language_models: Surface disabled cloud models (#59781)

Extends the support for disabled model state introduced in
https://github.com/zed-industries/zed/pull/59606 to Zed Cloud hosted
models.

Release Notes:

- N/A
This commit is contained in:
Tom Houlé authored and GitHub committed 2026-06-26 11:07:29 +00:00
1 parent 4306847359
commit 8d21284fbb
3 files changed
+80 -24

No files matched your search

@@ -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<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+61 -18
View File
@@ -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],
@@ -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<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
self.model.is_latest
}
fn is_disabled(&self) -> Option<DisabledReason> {
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