fix(claude-code): 额度查询携带 x-app/claude-cli UA 以获取 cedar_ember,并在无重置机会时显式清空

This commit is contained in:
dalamudx
2026-09-30 00:29:18 +08:00
parent b49f5c0fd7
commit 9cc4018a37
2 changed files with 49 additions and 5 deletions
+46 -4
View File
@@ -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]
@@ -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(),