diff --git a/crates/aether-admin/src/provider/quota.rs b/crates/aether-admin/src/provider/quota.rs index 3778bdccd..7e4b5174b 100644 --- a/crates/aether-admin/src/provider/quota.rs +++ b/crates/aether-admin/src/provider/quota.rs @@ -456,12 +456,15 @@ pub fn parse_claude_code_oauth_usage_response( bucket.insert(format!("{prefix}_reset_at"), serde_json::json!(reset_at)); } } - if let Some(reset_credits) = parse_claude_code_reset_credits(root, updated_at_unix_secs) { - bucket.insert("reset_credits".to_string(), reset_credits); - } if bucket.is_empty() { return None; } + // Explicit null so a refresh overwrites (rather than keeps) credits that were used up. + bucket.insert( + "reset_credits".to_string(), + parse_claude_code_reset_credits(root, updated_at_unix_secs) + .unwrap_or(serde_json::Value::Null), + ); bucket.insert( "updated_at".to_string(), serde_json::json!(updated_at_unix_secs), @@ -7934,6 +7937,45 @@ mod claude_code_quota_tests { assert!(!credits.to_string().contains("launch")); } + #[test] + fn real_cedar_ember_response_survives_metadata_redaction() { + // Shape captured from a live Claude Pro account (unrelated fields trimmed). + let parsed = parse_claude_code_oauth_usage_response( + &json!({ + "five_hour": {"utilization": 32.0, "resets_at": "2026-09-29T19:19:59.933008+00:00"}, + "cedar_ember": { + "eligible": true, + "at_limit": false, + "grants": [{ + "id": "opus55-launch-promax-20260921", + "label": "Claude Opus 5.5 launch", + "resets_total": 1, + "resets_left": 1, + "starts_at": "2026-09-22T16:00:00+00:00", + "ends_at": "2026-10-22T16:00:00+00:00", + "clears": ["five_hour", "seven_day"], + "paused": false, + "usable_now": true, + "use_requires_limit": false + }], + "next_grant_id": "opus55-launch-promax-20260921" + } + }), + 1_790_699_000, + ) + .expect("usage should parse"); + assert_eq!(parsed["reset_credits"]["available_count"], json!(1)); + + let safe = crate::provider::redaction::admin_provider_upstream_metadata_safe_json(Some( + &json!({ "claude_code": parsed }), + )); + assert_eq!( + safe["claude_code"]["reset_credits"]["available_count"], + json!(1), + "redaction dropped reset_credits: {safe}" + ); + } + #[test] fn cedar_ember_null_or_empty_omits_reset_credits() { let parsed = parse_claude_code_oauth_usage_response( @@ -7941,7 +7983,7 @@ mod claude_code_quota_tests { 1, ) .expect("usage should parse"); - assert!(parsed.get("reset_credits").is_none()); + assert!(parsed["reset_credits"].is_null()); } #[test] diff --git a/crates/aether-provider/pool/src/providers/claude_code.rs b/crates/aether-provider/pool/src/providers/claude_code.rs index 27bac2186..195dd40cb 100644 --- a/crates/aether-provider/pool/src/providers/claude_code.rs +++ b/crates/aether-provider/pool/src/providers/claude_code.rs @@ -11,7 +11,7 @@ use crate::quota_refresh::ProviderPoolQuotaRequestSpec; pub const CLAUDE_CODE_OAUTH_USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage?cedar_ember=1&skip_spend=1"; pub const CLAUDE_CODE_OAUTH_BETA: &str = "oauth-2025-04-20"; -pub const CLAUDE_CODE_USAGE_USER_AGENT: &str = "claude-code/2.1.284"; +pub const CLAUDE_CODE_USAGE_USER_AGENT: &str = "claude-cli/2.1.284 (external, cli)"; #[derive(Debug, Clone, Default)] pub struct ClaudeCodeProviderPoolAdapter; @@ -58,6 +58,8 @@ pub fn build_claude_code_pool_quota_request( "anthropic-beta".to_string(), CLAUDE_CODE_OAUTH_BETA.to_string(), ), + // The reset-credit (`cedar_ember`) block is only returned to first-party CLI callers. + ("x-app".to_string(), "cli".to_string()), ( "user-agent".to_string(), CLAUDE_CODE_USAGE_USER_AGENT.to_string(),