Claude Fable cannot be offered with Zero Data Retention; Anthropic retains inference logs for 30 days. This adds: - A telemetry.anthropic_retention setting (default false), surfaced in the settings UI Privacy section. - A requires_data_retention() query on the LanguageModel trait, true for cloud models with the claude-fable-5 id prefix. - A hard gate in CloudLanguageModel::stream_completion that raises a typed DataRetentionConsentRequiredError when consent is missing, which is non-retryable. - An agent panel callout for that error with Switch to Opus 4.8 / Accept actions; both resume the failed turn after acting, so the user's message continues without retyping.
Unified · Split
| @@ -1471,6 +1471,9 @@ · 1470 unmodified lines | |||
| 1471 | "diagnostics": true, | 1471 | "diagnostics": true, |
| 1472 | // Send anonymized usage data like what languages you're using Zed with. | 1472 | // Send anonymized usage data like what languages you're using Zed with. |
| 1473 | "metrics": true, | 1473 | "metrics": true, |
| 1474 | // Allow sending requests to Anthropic models that cannot be offered with | ||
| 1475 | // Zero Data Retention | ||
| 1476 | "anthropic_retention": false, | ||
| 1474 | }, | 1477 | }, |
| 1475 | // Whether to disable all AI features in Zed. | 1478 | // Whether to disable all AI features in Zed. |
| 1476 | // | 1479 | // |
| @@ -3976,6 +3976,9 @@ impl Thread { · 3975 unmodified lines | |||
| 3976 | // Retrying won't help for Payment Required errors. | 3976 | // Retrying won't help for Payment Required errors. |
| 3977 | None | 3977 | None |
| 3978 | } | 3978 | } |
| 3979 | // Retrying won't help until the user consents to data retention | ||
| 3980 | // or switches models. | ||
| 3981 | DataRetentionConsentRequired { .. } => None, | ||
| 3979 | // Conservatively assume that any other errors are non-retryable | 3982 | // Conservatively assume that any other errors are non-retryable |
| 3980 | HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { | 3983 | HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { |
| 3981 | delay: BASE_RETRY_DELAY, | 3984 | delay: BASE_RETRY_DELAY, |
| @@ -123,6 +123,7 @@ enum ThreadFeedback { · 122 unmodified lines | |||
| 123 | #[derive(Debug)] | 123 | #[derive(Debug)] |
| 124 | pub(crate) enum ThreadError { | 124 | pub(crate) enum ThreadError { |
| 125 | PaymentRequired, | 125 | PaymentRequired, |
| 126 | DataRetentionConsentRequired, | ||
| 126 | Refusal, | 127 | Refusal, |
| 127 | AuthenticationRequired(SharedString), | 128 | AuthenticationRequired(SharedString), |
| 128 | RateLimitExceeded { | 129 | RateLimitExceeded { |
| @@ -196,6 +197,7 @@ impl From<anyhow::Error> for ThreadError { · 67 unmodified lines | |||
| 196 | provider: provider.to_string().into(), | 197 | provider: provider.to_string().into(), |
| 197 | }, | 198 | }, |
| 198 | UpstreamProviderError { .. } => Self::RequestFailed, | 199 | UpstreamProviderError { .. } => Self::RequestFailed, |
| 200 | DataRetentionConsentRequired { .. } => Self::DataRetentionConsentRequired, | ||
| 199 | BadRequestFormat { provider, .. } | 201 | BadRequestFormat { provider, .. } |
| 200 | | HttpResponseError { provider, .. } | 202 | | HttpResponseError { provider, .. } |
| 201 | | ApiEndpointNotFound { provider } => Self::ApiError { | 203 | | ApiEndpointNotFound { provider } => Self::ApiError { |
| @@ -3443,6 +3445,21 @@ pub(crate) mod tests { · 3241 unmodified lines | |||
| 3443 | 3445 | ||
| 3444 | use super::*; | 3446 | use super::*; |
| 3445 | 3447 | ||
| 3448 | #[test] | ||
| 3449 | fn test_data_retention_error_maps_from_provider_error() { | ||
| 3450 | // The agent wraps the provider error in a fresh `anyhow::Error`, so | ||
| 3451 | // the mapping must downcast to `LanguageModelCompletionError` rather | ||
| 3452 | // than matching on the anyhow error directly. | ||
| 3453 | let provider_error = LanguageModelCompletionError::DataRetentionConsentRequired { | ||
| 3454 | model_name: "Claude Fable 5".to_string(), | ||
| 3455 | }; | ||
| 3456 | let error = ThreadError::from(anyhow!(provider_error)); | ||
| 3457 | assert!( | ||
| 3458 | matches!(error, ThreadError::DataRetentionConsentRequired), | ||
| 3459 | "expected ThreadError::DataRetentionConsentRequired, got: {error:?}" | ||
| 3460 | ); | ||
| 3461 | } | ||
| 3462 | |||
| 3446 | #[gpui::test] | 3463 | #[gpui::test] |
| 3447 | async fn test_drop(cx: &mut TestAppContext) { | 3464 | async fn test_drop(cx: &mut TestAppContext) { |
| 3448 | init_test(cx); | 3465 | init_test(cx); |
| @@ -23,10 +23,10 @@ use gpui::Stateful; · 22 unmodified lines | |||
| 23 | use gpui::TaskExt; | 23 | use gpui::TaskExt; |
| 24 | use heapless::Vec as ArrayVec; | 24 | use heapless::Vec as ArrayVec; |
| 25 | use language_model::{ | 25 | use language_model::{ |
| 26 | FastModeConfirmation, LanguageModelEffortLevel, LanguageModelId, LanguageModelProviderId, | 26 | FastModeConfirmation, LanguageModel, LanguageModelEffortLevel, LanguageModelId, |
| 27 | LanguageModelRegistry, Speed, | 27 | LanguageModelProviderId, LanguageModelRegistry, Speed, |
| 28 | }; | 28 | }; |
| 29 | use settings::update_settings_file; | 29 | use settings::{update_settings_file, update_settings_file_with_completion}; |
| 30 | use ui::{ | 30 | use ui::{ |
| 31 | ButtonLike, CalloutBorderPosition, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, | 31 | ButtonLike, CalloutBorderPosition, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, |
| 32 | Tab, | 32 | Tab, |
| @@ -36,6 +36,9 @@ use workspace::{OpenOptions, SERIALIZATION_THROTTLE_TIME}; · 3 unmodified lines | |||
| 36 | 36 | ||
| 37 | use super::*; | 37 | use super::*; |
| 38 | 38 | ||
| 39 | // TODO: Replace with the final link explaining Anthropic's data retention policy. | ||
| 40 | const DATA_RETENTION_LEARN_MORE_URL: &str = "#link-tbd"; | ||
| 41 | |||
| 39 | #[derive(Default)] | 42 | #[derive(Default)] |
| 40 | struct ThreadFeedbackState { | 43 | struct ThreadFeedbackState { |
| 41 | feedback: Option<ThreadFeedback>, | 44 | feedback: Option<ThreadFeedback>, |
| @@ -1714,6 +1717,13 @@ impl ThreadView { · 1672 unmodified lines | |||
| 1714 | ); | 1717 | ); |
| 1715 | ("refusal", None, message.into()) | 1718 | ("refusal", None, message.into()) |
| 1716 | } | 1719 | } |
| 1720 | ThreadError::DataRetentionConsentRequired => { | ||
| 1721 | let message = format!( | ||
| 1722 | "{} is not available with Zero Data Retention.", | ||
| 1723 | self.current_model_name(cx) | ||
| 1724 | ); | ||
| 1725 | ("data_retention_consent_required", None, message.into()) | ||
| 1726 | } | ||
| 1717 | ThreadError::AuthenticationRequired(message) => { | 1727 | ThreadError::AuthenticationRequired(message) => { |
| 1718 | ("authentication_required", None, message.clone()) | 1728 | ("authentication_required", None, message.clone()) |
| 1719 | } | 1729 | } |
| @@ -9348,6 +9358,9 @@ impl ThreadView { · 7628 unmodified lines | |||
| 9348 | self.render_any_thread_error(message.clone(), window, cx) | 9358 | self.render_any_thread_error(message.clone(), window, cx) |
| 9349 | } | 9359 | } |
| 9350 | ThreadError::Refusal => self.render_refusal_error(cx), | 9360 | ThreadError::Refusal => self.render_refusal_error(cx), |
| 9361 | ThreadError::DataRetentionConsentRequired => { | ||
| 9362 | self.render_data_retention_consent_error(cx) | ||
| 9363 | } | ||
| 9351 | ThreadError::AuthenticationRequired(error) => { | 9364 | ThreadError::AuthenticationRequired(error) => { |
| 9352 | self.render_authentication_required_error(error.clone(), cx) | 9365 | self.render_authentication_required_error(error.clone(), cx) |
| 9353 | } | 9366 | } |
| @@ -10119,6 +10132,118 @@ impl ThreadView { · 765 unmodified lines | |||
| 10119 | ) | 10132 | ) |
| 10120 | } | 10133 | } |
| 10121 | 10134 | ||
| 10135 | /// Returns the model to offer as a downgrade target when the current model | ||
| 10136 | /// requires data retention consent (e.g. Opus 4.8 for Fable). | ||
| 10137 | fn data_retention_fallback_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> { | ||
| 10138 | let thread = self.as_native_thread(cx)?; | ||
| 10139 | let model = thread.read(cx).model()?.clone(); | ||
| 10140 | let fallback_id = model.refusal_fallback_model_id()?; | ||
| 10141 | LanguageModelRegistry::read_global(cx) | ||
| 10142 | .available_models(cx) | ||
| 10143 | .find(|fallback| { | ||
| 10144 | fallback.provider_id() == model.provider_id() | ||
| 10145 | && fallback.id().0.as_ref() == fallback_id | ||
| 10146 | }) | ||
| 10147 | } | ||
| 10148 | |||
| 10149 | fn render_data_retention_consent_error(&self, cx: &mut Context<Self>) -> Callout { | ||
| 10150 | let fallback_model = self.data_retention_fallback_model(cx); | ||
| 10151 | |||
| 10152 | Callout::new() | ||
| 10153 | .severity(Severity::Warning) | ||
| 10154 | .icon(IconName::Warning) | ||
| 10155 | .title(format!( | ||
| 10156 | "Note: {} cannot be offered with Zero Data Retention.", | ||
| 10157 | self.current_model_name(cx) | ||
| 10158 | )) | ||
| 10159 | .description_slot( | ||
| 10160 | h_flex() | ||
| 10161 | .gap_1() | ||
| 10162 | .child( | ||
| 10163 | Label::new("Anthropic will retain inference logs.") | ||
| 10164 | .size(LabelSize::Small) | ||
| 10165 | .color(Color::Muted), | ||
| 10166 | ) | ||
| 10167 | .child( | ||
| 10168 | Button::new("data-retention-learn-more", "Learn More") | ||
| 10169 | .label_size(LabelSize::Small) | ||
| 10170 | .on_click(|_, _, cx| { | ||
| 10171 | cx.open_url(DATA_RETENTION_LEARN_MORE_URL); | ||
| 10172 | }), | ||
| 10173 | ), | ||
| 10174 | ) | ||
| 10175 | .actions_slot( | ||
| 10176 | h_flex() | ||
| 10177 | .gap_0p5() | ||
| 10178 | .when_some(fallback_model, |this, fallback| { | ||
| 10179 | this.child( | ||
| 10180 | Button::new( | ||
| 10181 | "switch-data-retention-fallback", | ||
| 10182 | format!("Switch to {}", fallback.name().0), | ||
| 10183 | ) | ||
| 10184 | .label_size(LabelSize::Small) | ||
| 10185 | .on_click(cx.listener(|this, _, _, cx| { | ||
| 10186 | this.switch_to_data_retention_fallback_and_resend(cx); | ||
| 10187 | })), | ||
| 10188 | ) | ||
| 10189 | }) | ||
| 10190 | .child( | ||
| 10191 | Button::new("accept-data-retention", "Accept") | ||
| 10192 | .label_size(LabelSize::Small) | ||
| 10193 | .style(ButtonStyle::Tinted(TintColor::Warning)) | ||
| 10194 | .on_click(cx.listener(|this, _, _, cx| { | ||
| 10195 | this.accept_data_retention_and_resend(cx); | ||
| 10196 | })), | ||
| 10197 | ), | ||
| 10198 | ) | ||
| 10199 | .dismiss_action(self.dismiss_error_button(cx)) | ||
| 10200 | } | ||
| 10201 | |||
| 10202 | fn accept_data_retention_and_resend(&mut self, cx: &mut Context<Self>) { | ||
| 10203 | let fs = self.thread.read(cx).project().read(cx).fs().clone(); | ||
| 10204 | // Resume the failed turn only once the in-memory settings reflect | ||
| 10205 | // consent, otherwise the resent request would be rejected again. | ||
| 10206 | let completion = update_settings_file_with_completion(fs, cx, |settings, _| { | ||
| 10207 | settings | ||
| 10208 | .telemetry | ||
| 10209 | .get_or_insert_default() | ||
| 10210 | .anthropic_retention = Some(true); | ||
| 10211 | }); | ||
| 10212 | cx.spawn(async move |this, cx| { | ||
| 10213 | completion.await??; | ||
| 10214 | this.update(cx, |this, cx| this.retry_generation(cx))?; | ||
| 10215 | anyhow::Ok(()) | ||
| 10216 | }) | ||
| 10217 | .detach_and_log_err(cx); | ||
| 10218 | } | ||
| 10219 | |||
| 10220 | fn switch_to_data_retention_fallback_and_resend(&mut self, cx: &mut Context<Self>) { | ||
| 10221 | let Some(fallback) = self.data_retention_fallback_model(cx) else { | ||
| 10222 | return; | ||
| 10223 | }; | ||
| 10224 | let model_id = acp_thread::AgentModelId::new(format!( | ||
| 10225 | "{}/{}", | ||
| 10226 | fallback.provider_id().0, | ||
| 10227 | fallback.id().0 | ||
| 10228 | )); | ||
| 10229 | let session_id = self.thread.read(cx).session_id().clone(); | ||
| 10230 | let Some(selector) = self | ||
| 10231 | .thread | ||
| 10232 | .read(cx) | ||
| 10233 | .connection() | ||
| 10234 | .model_selector(&session_id) | ||
| 10235 | else { | ||
| 10236 | return; | ||
| 10237 | }; | ||
| 10238 | let select = selector.select_model(model_id, cx); | ||
| 10239 | cx.spawn(async move |this, cx| { | ||
| 10240 | select.await?; | ||
| 10241 | this.update(cx, |this, cx| this.retry_generation(cx))?; | ||
| 10242 | anyhow::Ok(()) | ||
| 10243 | }) | ||
| 10244 | .detach_and_log_err(cx); | ||
| 10245 | } | ||
| 10246 | |||
| 10122 | fn open_permission_dropdown( | 10247 | fn open_permission_dropdown( |
| 10123 | &mut self, | 10248 | &mut self, |
| 10124 | _: &crate::OpenPermissionDropdown, | 10249 | _: &crate::OpenPermissionDropdown, |
| @@ -20,7 +20,7 @@ pub mod completion; · 19 unmodified lines | |||
| 20 | pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com"; | 20 | pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com"; |
| 21 | const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01"; | 21 | const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01"; |
| 22 | 22 | ||
| 23 | pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable"; | 23 | pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable-5"; |
| 24 | pub const FABLE_FALLBACK_MODEL_ID: &str = "claude-opus-4-8"; | 24 | pub const FABLE_FALLBACK_MODEL_ID: &str = "claude-opus-4-8"; |
| 25 | 25 | ||
| 26 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | 26 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
| @@ -540,13 +540,16 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> { · 539 unmodified lines | |||
| 540 | pub struct TelemetrySettings { | 540 | pub struct TelemetrySettings { |
| 541 | pub diagnostics: bool, | 541 | pub diagnostics: bool, |
| 542 | pub metrics: bool, | 542 | pub metrics: bool, |
| 543 | pub anthropic_retention: bool, | ||
| 543 | } | 544 | } |
| 544 | 545 | ||
| 545 | impl settings::Settings for TelemetrySettings { | 546 | impl settings::Settings for TelemetrySettings { |
| 546 | fn from_settings(content: &SettingsContent) -> Self { | 547 | fn from_settings(content: &SettingsContent) -> Self { |
| 548 | let telemetry = content.telemetry.as_ref().unwrap(); | ||
| 547 | Self { | 549 | Self { |
| 548 | diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(), | 550 | diagnostics: telemetry.diagnostics.unwrap(), |
| 549 | metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(), | 551 | metrics: telemetry.metrics.unwrap(), |
| 552 | anthropic_retention: telemetry.anthropic_retention.unwrap(), | ||
| 550 | } | 553 | } |
| 551 | } | 554 | } |
| 552 | } | 555 | } |
| @@ -60,6 +60,18 @@ pub trait LanguageModel: Send + Sync { · 59 unmodified lines | |||
| 60 | false | 60 | false |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | /// Whether requests to this model require the user to consent to the | ||
| 64 | /// upstream provider retaining inference logs (i.e. the model cannot be | ||
| 65 | /// offered with Zero Data Retention). | ||
| 66 | fn requires_data_retention(&self) -> bool { | ||
| 67 | false | ||
| 68 | } | ||
| 69 | |||
| 70 | /// When this model refuses a request, the model ID to fall back to (same provider). | ||
| 71 | fn refusal_fallback_model_id(&self) -> Option<&'static str> { | ||
| 72 | None | ||
| 73 | } | ||
| 74 | |||
| 63 | fn telemetry_id(&self) -> String; | 75 | fn telemetry_id(&self) -> String; |
| 64 | 76 | ||
| 65 | fn api_key(&self, _cx: &App) -> Option<String> { | 77 | fn api_key(&self, _cx: &App) -> Option<String> { |
| @@ -112,11 +124,6 @@ pub trait LanguageModel: Send + Sync { · 46 unmodified lines | |||
| 112 | false | 124 | false |
| 113 | } | 125 | } |
| 114 | 126 | ||
| 115 | /// When this model refuses a request, the model ID to fall back to (same provider). | ||
| 116 | fn refusal_fallback_model_id(&self) -> Option<&'static str> { | ||
| 117 | None | ||
| 118 | } | ||
| 119 | |||
| 120 | fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { | 127 | fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { |
| 121 | LanguageModelToolSchemaFormat::JsonSchema | 128 | LanguageModelToolSchemaFormat::JsonSchema |
| 122 | } | 129 | } |
| @@ -88,6 +88,14 @@ impl LanguageModelCompletionEvent { · 87 unmodified lines | |||
| 88 | pub enum LanguageModelCompletionError { | 88 | pub enum LanguageModelCompletionError { |
| 89 | #[error("prompt too large for context window")] | 89 | #[error("prompt too large for context window")] |
| 90 | PromptTooLarge { tokens: Option<u64> }, | 90 | PromptTooLarge { tokens: Option<u64> }, |
| 91 | /// The model requires the user to consent to the upstream provider | ||
| 92 | /// retaining inference logs (see `LanguageModel::requires_data_retention`) | ||
| 93 | /// and that consent has not been given. | ||
| 94 | #[error( | ||
| 95 | "{model_name} cannot be offered with Zero Data Retention. \ | ||
| 96 | Anthropic will retain inference logs." | ||
| 97 | )] | ||
| 98 | DataRetentionConsentRequired { model_name: String }, | ||
| 91 | #[error("missing {provider} API key")] | 99 | #[error("missing {provider} API key")] |
| 92 | NoApiKey { provider: LanguageModelProviderName }, | 100 | NoApiKey { provider: LanguageModelProviderName }, |
| 93 | #[error("{provider}'s API rate limit exceeded")] | 101 | #[error("{provider}'s API rate limit exceeded")] |
| @@ -1,6 +1,8 @@ | |||
| 1 | use ai_onboarding::YoungAccountBanner; | 1 | use ai_onboarding::YoungAccountBanner; |
| 2 | use anyhow::Result; | 2 | use anyhow::Result; |
| 3 | use client::{Client, RefreshLlmTokenListener, UserStore, global_llm_token, zed_urls}; | 3 | use client::{ |
| 4 | Client, RefreshLlmTokenListener, TelemetrySettings, UserStore, global_llm_token, zed_urls, | ||
| 5 | }; | ||
| 4 | use cloud_api_client::LlmApiToken; | 6 | use cloud_api_client::LlmApiToken; |
| 5 | use cloud_api_types::OrganizationId; | 7 | use cloud_api_types::OrganizationId; |
| 6 | use cloud_api_types::Plan; | 8 | use cloud_api_types::Plan; |
| @@ -70,6 +72,14 @@ impl CloudLlmTokenProvider for ClientTokenProvider { · 63 unmodified lines | |||
| 70 | .await | 72 | .await |
| 71 | }) | 73 | }) |
| 72 | } | 74 | } |
| 75 | |||
| 76 | fn has_data_retention_consent(&self, cx: &impl AppContext) -> bool { | ||
| 77 | cx.read_global(|settings_store: &SettingsStore, _| { | ||
| 78 | settings_store | ||
| 79 | .get::<TelemetrySettings>(None) | ||
| 80 | .anthropic_retention | ||
| 81 | }) | ||
| 82 | } | ||
| 73 | } | 83 | } |
| 74 | 84 | ||
| 75 | #[derive(Default, Clone, Debug, PartialEq)] | 85 | #[derive(Default, Clone, Debug, PartialEq)] |
| @@ -55,6 +55,11 @@ pub trait CloudLlmTokenProvider: Send + Sync { · 54 unmodified lines | |||
| 55 | fn auth_context(&self, cx: &impl AppContext) -> Self::AuthContext; | 55 | fn auth_context(&self, cx: &impl AppContext) -> Self::AuthContext; |
| 56 | fn cached_token(&self, auth_context: Self::AuthContext) -> BoxFuture<'static, Result<String>>; | 56 | fn cached_token(&self, auth_context: Self::AuthContext) -> BoxFuture<'static, Result<String>>; |
| 57 | fn refresh_token(&self, auth_context: Self::AuthContext) -> BoxFuture<'static, Result<String>>; | 57 | fn refresh_token(&self, auth_context: Self::AuthContext) -> BoxFuture<'static, Result<String>>; |
| 58 | |||
| 59 | /// Whether the user has consented to upstream providers retaining | ||
| 60 | /// inference logs for models that require it (see | ||
| 61 | /// [`LanguageModel::requires_data_retention`]). | ||
| 62 | fn has_data_retention_consent(&self, cx: &impl AppContext) -> bool; | ||
| 58 | } | 63 | } |
| 59 | 64 | ||
| 60 | /// Sends an authenticated request to the Zed LLM service, retrying once with | 65 | /// Sends an authenticated request to the Zed LLM service, retrying once with |
| @@ -298,6 +303,27 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T · 237 unmodified lines | |||
| 298 | self.model.is_latest | 303 | self.model.is_latest |
| 299 | } | 304 | } |
| 300 | 305 | ||
| 306 | fn requires_data_retention(&self) -> bool { | ||
| 307 | // Anthropic cannot offer Fable models with Zero Data Retention | ||
| 308 | self.id | ||
| 309 | .0 | ||
| 310 | .as_ref() | ||
| 311 | .starts_with(anthropic::FABLE_MODEL_ID_PREFIX) | ||
| 312 | } | ||
| 313 | |||
| 314 | fn refusal_fallback_model_id(&self) -> Option<&'static str> { | ||
| 315 | if self | ||
| 316 | .id | ||
| 317 | .0 | ||
| 318 | .as_ref() | ||
| 319 | .starts_with(anthropic::FABLE_MODEL_ID_PREFIX) | ||
| 320 | { | ||
| 321 | Some(anthropic::FABLE_FALLBACK_MODEL_ID) | ||
| 322 | } else { | ||
| 323 | None | ||
| 324 | } | ||
| 325 | } | ||
| 326 | |||
| 301 | fn supports_tools(&self) -> bool { | 327 | fn supports_tools(&self) -> bool { |
| 302 | self.model.supports_tools | 328 | self.model.supports_tools |
| 303 | } | 329 | } |
| @@ -343,19 +369,6 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T · 39 unmodified lines | |||
| 343 | matches!(self.model.provider, OpenAi | XAi) | 369 | matches!(self.model.provider, OpenAi | XAi) |
| 344 | } | 370 | } |
| 345 | 371 | ||
| 346 | fn refusal_fallback_model_id(&self) -> Option<&'static str> { | ||
| 347 | if self | ||
| 348 | .id | ||
| 349 | .0 | ||
| 350 | .as_ref() | ||
| 351 | .starts_with(anthropic::FABLE_MODEL_ID_PREFIX) | ||
| 352 | { | ||
| 353 | Some(anthropic::FABLE_FALLBACK_MODEL_ID) | ||
| 354 | } else { | ||
| 355 | None | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | fn telemetry_id(&self) -> String { | 372 | fn telemetry_id(&self) -> String { |
| 360 | format!("zed.dev/{}", self.model.id) | 373 | format!("zed.dev/{}", self.model.id) |
| 361 | } | 374 | } |
| @@ -392,6 +405,14 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T · 30 unmodified lines | |||
| 392 | LanguageModelCompletionError, | 405 | LanguageModelCompletionError, |
| 393 | >, | 406 | >, |
| 394 | > { | 407 | > { |
| 408 | if self.requires_data_retention() && !self.token_provider.has_data_retention_consent(cx) { | ||
| 409 | let model_name = self.model.display_name.clone(); | ||
| 410 | return async move { | ||
| 411 | Err(LanguageModelCompletionError::DataRetentionConsentRequired { model_name }) | ||
| 412 | } | ||
| 413 | .boxed(); | ||
| 414 | } | ||
| 415 | |||
| 395 | let thread_id = request.thread_id.clone(); | 416 | let thread_id = request.thread_id.clone(); |
| 396 | let prompt_id = request.prompt_id.clone(); | 417 | let prompt_id = request.prompt_id.clone(); |
| 397 | let app_version = self.app_version.clone(); | 418 | let app_version = self.app_version.clone(); |
| @@ -862,6 +862,7 @@ impl VsCodeSettings { · 861 unmodified lines | |||
| 862 | Some(TelemetrySettingsContent { | 862 | Some(TelemetrySettingsContent { |
| 863 | metrics: Some(metrics), | 863 | metrics: Some(metrics), |
| 864 | diagnostics: Some(diagnostics), | 864 | diagnostics: Some(diagnostics), |
| 865 | anthropic_retention: None, | ||
| 865 | }) | 866 | }) |
| 866 | }) | 867 | }) |
| 867 | } | 868 | } |
| @@ -521,6 +521,11 @@ pub struct TelemetrySettingsContent { · 520 unmodified lines | |||
| 521 | /// | 521 | /// |
| 522 | /// Default: true | 522 | /// Default: true |
| 523 | pub metrics: Option<bool>, | 523 | pub metrics: Option<bool>, |
| 524 | /// Allow sending requests to Anthropic models that cannot be offered with | ||
| 525 | /// Zero Data Retention. | ||
| 526 | /// | ||
| 527 | /// Default: false | ||
| 528 | pub anthropic_retention: Option<bool>, | ||
| 524 | } | 529 | } |
| 525 | 530 | ||
| 526 | impl Default for TelemetrySettingsContent { | 531 | impl Default for TelemetrySettingsContent { |
| @@ -528,6 +533,7 @@ impl Default for TelemetrySettingsContent { · 1 unmodified line | |||
| 528 | Self { | 533 | Self { |
| 529 | diagnostics: Some(true), | 534 | diagnostics: Some(true), |
| 530 | metrics: Some(true), | 535 | metrics: Some(true), |
| 536 | anthropic_retention: Some(false), | ||
| 531 | } | 537 | } |
| 532 | } | 538 | } |
| 533 | } | 539 | } |
| @@ -365,7 +365,7 @@ fn general_page(cx: &App) -> SettingsPage { · 364 unmodified lines | |||
| 365 | ] | 365 | ] |
| 366 | } | 366 | } |
| 367 | 367 | ||
| 368 | fn privacy_section() -> [SettingsPageItem; 3] { | 368 | fn privacy_section() -> [SettingsPageItem; 4] { |
| 369 | [ | 369 | [ |
| 370 | SettingsPageItem::SectionHeader("Privacy"), | 370 | SettingsPageItem::SectionHeader("Privacy"), |
| 371 | SettingsPageItem::SettingItem(SettingItem { | 371 | SettingsPageItem::SettingItem(SettingItem { |
| @@ -409,6 +409,28 @@ fn general_page(cx: &App) -> SettingsPage { · 37 unmodified lines | |||
| 409 | metadata: None, | 409 | metadata: None, |
| 410 | files: USER, | 410 | files: USER, |
| 411 | }), | 411 | }), |
| 412 | SettingsPageItem::SettingItem(SettingItem { | ||
| 413 | title: "Anthropic Data Retention", | ||
| 414 | description: "Allow sending requests to Anthropic models that cannot be offered with Zero Data Retention.", | ||
| 415 | field: Box::new(SettingField { | ||
| 416 | organization_override: None, | ||
| 417 | json_path: Some("telemetry.anthropic_retention"), | ||
| 418 | pick: |settings_content| { | ||
| 419 | settings_content | ||
| 420 | .telemetry | ||
| 421 | .as_ref() | ||
| 422 | .and_then(|telemetry| telemetry.anthropic_retention.as_ref()) | ||
| 423 | }, | ||
| 424 | write: |settings_content, value, _| { | ||
| 425 | settings_content | ||
| 426 | .telemetry | ||
| 427 | .get_or_insert_default() | ||
| 428 | .anthropic_retention = value; | ||
| 429 | }, | ||
| 430 | }), | ||
| 431 | metadata: None, | ||
| 432 | files: USER, | ||
| 433 | }), | ||
| 412 | ] | 434 | ] |
| 413 | } | 435 | } |
| 414 | 436 | ||