History

Gate Claude Fable behind Anthropic data retention consent

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.

Mikayla Maki committed 5 months ago

Commit 8b7c1931 parent db3a44c13 files · +252 −26Browse files

Unified · Split

13 files
  1. assets/settings/default.json +3 −0
  2. crates/agent/src/thread.rs +3 −0
  3. crates/agent_ui/src/conversation_view.rs +17 −0
  4. crates/agent_ui/src/conversation_view/thread_view.rs +128 −3
  5. crates/anthropic/src/anthropic.rs +1 −1
  6. crates/client/src/client.rs +5 −2
  7. crates/language_model/src/language_model.rs +12 −5
  8. crates/language_model_core/src/language_model_core.rs +8 −0
  9. crates/language_models/src/provider/cloud.rs +11 −1
  10. crates/language_models_cloud/src/language_models_cloud.rs +34 −13
  11. crates/settings/src/vscode_import.rs +1 −0
  12. crates/settings_content/src/settings_content.rs +6 −0
  13. crates/settings_ui/src/page_data.rs +23 −1
assets/settings/default.json+3−0
@@ -1471,6 +1471,9 @@ · 1470 unmodified lines
14711471 "diagnostics": true,
14721472 // Send anonymized usage data like what languages you're using Zed with.
14731473 "metrics": true,
1474+ // Allow sending requests to Anthropic models that cannot be offered with
1475+ // Zero Data Retention
1476+ "anthropic_retention": false,
14741477 },
14751478 // Whether to disable all AI features in Zed.
14761479 //
crates/agent/src/thread.rs+3−0
@@ -3976,6 +3976,9 @@ impl Thread { · 3975 unmodified lines
39763976 // Retrying won't help for Payment Required errors.
39773977 None
39783978 }
3979+ // Retrying won't help until the user consents to data retention
3980+ // or switches models.
3981+ DataRetentionConsentRequired { .. } => None,
39793982 // Conservatively assume that any other errors are non-retryable
39803983 HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
39813984 delay: BASE_RETRY_DELAY,
crates/agent_ui/src/conversation_view.rs+17−0
@@ -123,6 +123,7 @@ enum ThreadFeedback { · 122 unmodified lines
123123 #[derive(Debug)]
124124 pub(crate) enum ThreadError {
125125 PaymentRequired,
126+ DataRetentionConsentRequired,
126127 Refusal,
127128 AuthenticationRequired(SharedString),
128129 RateLimitExceeded {
@@ -196,6 +197,7 @@ impl From<anyhow::Error> for ThreadError { · 67 unmodified lines
196197 provider: provider.to_string().into(),
197198 },
198199 UpstreamProviderError { .. } => Self::RequestFailed,
200+ DataRetentionConsentRequired { .. } => Self::DataRetentionConsentRequired,
199201 BadRequestFormat { provider, .. }
200202 | HttpResponseError { provider, .. }
201203 | ApiEndpointNotFound { provider } => Self::ApiError {
@@ -3443,6 +3445,21 @@ pub(crate) mod tests { · 3241 unmodified lines
34433445
34443446 use super::*;
34453447
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+
34463463 #[gpui::test]
34473464 async fn test_drop(cx: &mut TestAppContext) {
34483465 init_test(cx);
crates/agent_ui/src/conversation_view/thread_view.rs+128−3
@@ -23,10 +23,10 @@ use gpui::Stateful; · 22 unmodified lines
2323 use gpui::TaskExt;
2424 use heapless::Vec as ArrayVec;
2525 use language_model::{
26− FastModeConfirmation, LanguageModelEffortLevel, LanguageModelId, LanguageModelProviderId,
27− LanguageModelRegistry, Speed,
26+ FastModeConfirmation, LanguageModel, LanguageModelEffortLevel, LanguageModelId,
27+ LanguageModelProviderId, LanguageModelRegistry, Speed,
2828 };
29−use settings::update_settings_file;
29+use settings::{update_settings_file, update_settings_file_with_completion};
3030 use ui::{
3131 ButtonLike, CalloutBorderPosition, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle,
3232 Tab,
@@ -36,6 +36,9 @@ use workspace::{OpenOptions, SERIALIZATION_THROTTLE_TIME}; · 3 unmodified lines
3636
3737 use super::*;
3838
39+// TODO: Replace with the final link explaining Anthropic's data retention policy.
40+const DATA_RETENTION_LEARN_MORE_URL: &str = "#link-tbd";
41+
3942 #[derive(Default)]
4043 struct ThreadFeedbackState {
4144 feedback: Option<ThreadFeedback>,
@@ -1714,6 +1717,13 @@ impl ThreadView { · 1672 unmodified lines
17141717 );
17151718 ("refusal", None, message.into())
17161719 }
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+ }
17171727 ThreadError::AuthenticationRequired(message) => {
17181728 ("authentication_required", None, message.clone())
17191729 }
@@ -9348,6 +9358,9 @@ impl ThreadView { · 7628 unmodified lines
93489358 self.render_any_thread_error(message.clone(), window, cx)
93499359 }
93509360 ThreadError::Refusal => self.render_refusal_error(cx),
9361+ ThreadError::DataRetentionConsentRequired => {
9362+ self.render_data_retention_consent_error(cx)
9363+ }
93519364 ThreadError::AuthenticationRequired(error) => {
93529365 self.render_authentication_required_error(error.clone(), cx)
93539366 }
@@ -10119,6 +10132,118 @@ impl ThreadView { · 765 unmodified lines
1011910132 )
1012010133 }
1012110134
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+
1012210247 fn open_permission_dropdown(
1012310248 &mut self,
1012410249 _: &crate::OpenPermissionDropdown,
crates/anthropic/src/anthropic.rs+1−1
@@ -20,7 +20,7 @@ pub mod completion; · 19 unmodified lines
2020 pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
2121 const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01";
2222
23−pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable";
23+pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable-5";
2424 pub const FABLE_FALLBACK_MODEL_ID: &str = "claude-opus-4-8";
2525
2626 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
crates/client/src/client.rs+5−2
@@ -540,13 +540,16 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> { · 539 unmodified lines
540540 pub struct TelemetrySettings {
541541 pub diagnostics: bool,
542542 pub metrics: bool,
543+ pub anthropic_retention: bool,
543544 }
544545
545546 impl settings::Settings for TelemetrySettings {
546547 fn from_settings(content: &SettingsContent) -> Self {
548+ let telemetry = content.telemetry.as_ref().unwrap();
547549 Self {
548− diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
549− metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),
550+ diagnostics: telemetry.diagnostics.unwrap(),
551+ metrics: telemetry.metrics.unwrap(),
552+ anthropic_retention: telemetry.anthropic_retention.unwrap(),
550553 }
551554 }
552555 }
crates/language_model/src/language_model.rs+12−5
@@ -60,6 +60,18 @@ pub trait LanguageModel: Send + Sync { · 59 unmodified lines
6060 false
6161 }
6262
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+
6375 fn telemetry_id(&self) -> String;
6476
6577 fn api_key(&self, _cx: &App) -> Option<String> {
@@ -112,11 +124,6 @@ pub trait LanguageModel: Send + Sync { · 46 unmodified lines
112124 false
113125 }
114126
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−
120127 fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
121128 LanguageModelToolSchemaFormat::JsonSchema
122129 }
crates/language_model_core/src/language_model_core.rs+8−0
@@ -88,6 +88,14 @@ impl LanguageModelCompletionEvent { · 87 unmodified lines
8888 pub enum LanguageModelCompletionError {
8989 #[error("prompt too large for context window")]
9090 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 },
9199 #[error("missing {provider} API key")]
92100 NoApiKey { provider: LanguageModelProviderName },
93101 #[error("{provider}'s API rate limit exceeded")]
crates/language_models/src/provider/cloud.rs+11−1
@@ -1,6 +1,8 @@
11 use ai_onboarding::YoungAccountBanner;
22 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+};
46 use cloud_api_client::LlmApiToken;
57 use cloud_api_types::OrganizationId;
68 use cloud_api_types::Plan;
@@ -70,6 +72,14 @@ impl CloudLlmTokenProvider for ClientTokenProvider { · 63 unmodified lines
7072 .await
7173 })
7274 }
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+ }
7383 }
7484
7585 #[derive(Default, Clone, Debug, PartialEq)]
crates/language_models_cloud/src/language_models_cloud.rs+34−13
@@ -55,6 +55,11 @@ pub trait CloudLlmTokenProvider: Send + Sync { · 54 unmodified lines
5555 fn auth_context(&self, cx: &impl AppContext) -> Self::AuthContext;
5656 fn cached_token(&self, auth_context: Self::AuthContext) -> BoxFuture<'static, Result<String>>;
5757 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;
5863 }
5964
6065 /// 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
298303 self.model.is_latest
299304 }
300305
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+
301327 fn supports_tools(&self) -> bool {
302328 self.model.supports_tools
303329 }
@@ -343,19 +369,6 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T · 39 unmodified lines
343369 matches!(self.model.provider, OpenAi | XAi)
344370 }
345371
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−
359372 fn telemetry_id(&self) -> String {
360373 format!("zed.dev/{}", self.model.id)
361374 }
@@ -392,6 +405,14 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T · 30 unmodified lines
392405 LanguageModelCompletionError,
393406 >,
394407 > {
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+
395416 let thread_id = request.thread_id.clone();
396417 let prompt_id = request.prompt_id.clone();
397418 let app_version = self.app_version.clone();
crates/settings/src/vscode_import.rs+1−0
@@ -862,6 +862,7 @@ impl VsCodeSettings { · 861 unmodified lines
862862 Some(TelemetrySettingsContent {
863863 metrics: Some(metrics),
864864 diagnostics: Some(diagnostics),
865+ anthropic_retention: None,
865866 })
866867 })
867868 }
crates/settings_content/src/settings_content.rs+6−0
@@ -521,6 +521,11 @@ pub struct TelemetrySettingsContent { · 520 unmodified lines
521521 ///
522522 /// Default: true
523523 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>,
524529 }
525530
526531 impl Default for TelemetrySettingsContent {
@@ -528,6 +533,7 @@ impl Default for TelemetrySettingsContent { · 1 unmodified line
528533 Self {
529534 diagnostics: Some(true),
530535 metrics: Some(true),
536+ anthropic_retention: Some(false),
531537 }
532538 }
533539 }
crates/settings_ui/src/page_data.rs+23−1
@@ -365,7 +365,7 @@ fn general_page(cx: &App) -> SettingsPage { · 364 unmodified lines
365365 ]
366366 }
367367
368− fn privacy_section() -> [SettingsPageItem; 3] {
368+ fn privacy_section() -> [SettingsPageItem; 4] {
369369 [
370370 SettingsPageItem::SectionHeader("Privacy"),
371371 SettingsPageItem::SettingItem(SettingItem {
@@ -409,6 +409,28 @@ fn general_page(cx: &App) -> SettingsPage { · 37 unmodified lines
409409 metadata: None,
410410 files: USER,
411411 }),
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+ }),
412434 ]
413435 }
414436