mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
Merge origin/main into main
Integrate upstream updates while preserving the local analytics dashboards and schema-only migration changes. Combine user account analysis with upstream user/group usage statistics in separate tabs, retain all migration versions, and keep the deleted audit document removed. Validation: gateway all-target cargo check, frontend type check and 57 focused tests, 48 migration tests, schema composition checks, and diff whitespace checks.
This commit is contained in:
@@ -841,6 +841,53 @@ pub fn build_admin_stats_leaderboard_response(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub fn build_admin_stats_user_group_leaderboard_response(
|
||||
metric: AdminStatsLeaderboardMetric,
|
||||
time_range: Option<&AdminStatsTimeRange>,
|
||||
leaderboard: &[AdminStatsLeaderboardItem],
|
||||
member_counts: &std::collections::BTreeMap<String, usize>,
|
||||
active_member_counts: &std::collections::BTreeMap<String, usize>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Response<Body> {
|
||||
let total = leaderboard.len();
|
||||
let items: Vec<_> = leaderboard
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.map(|(index, item)| {
|
||||
let rank = compute_dense_rank(metric, leaderboard, index);
|
||||
let value = match metric {
|
||||
AdminStatsLeaderboardMetric::Requests => json!(item.requests),
|
||||
AdminStatsLeaderboardMetric::Tokens => json!(item.tokens),
|
||||
AdminStatsLeaderboardMetric::Cost => json!(round_to(item.cost, 6)),
|
||||
};
|
||||
json!({
|
||||
"rank": rank,
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
"value": value,
|
||||
"requests": item.requests,
|
||||
"tokens": item.tokens,
|
||||
"cost": round_to(item.cost, 6),
|
||||
"member_count": member_counts.get(&item.id).copied().unwrap_or(0),
|
||||
"active_member_count": active_member_counts.get(&item.id).copied().unwrap_or(0),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"metric": metric.as_str(),
|
||||
"start_date": time_range.map(|value| value.start_date.to_string()),
|
||||
"end_date": time_range.map(|value| value.end_date.to_string()),
|
||||
"attribution": "current_membership",
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub fn build_admin_stats_comparison_response(
|
||||
current_usage: &[StoredRequestUsageAudit],
|
||||
comparison_usage: &[StoredRequestUsageAudit],
|
||||
|
||||
@@ -1349,6 +1349,7 @@ fn admin_usage_active_request_json(
|
||||
if let Some(target_model) = item.target_model.as_ref() {
|
||||
value["target_model"] = json!(target_model);
|
||||
}
|
||||
value["response_model"] = json!(item.provider_response_model());
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
value["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
@@ -1445,6 +1446,11 @@ pub fn admin_usage_record_json(
|
||||
let object = payload
|
||||
.as_object_mut()
|
||||
.expect("admin usage record payload should be an object");
|
||||
// 大型 json! 宏接近 Rust 的递归展开上限,响应模型在宏展开后补入即可避免编译失败。
|
||||
object.insert(
|
||||
"response_model".to_string(),
|
||||
json!(item.provider_response_model()),
|
||||
);
|
||||
object.insert(
|
||||
"end_to_end_time_ms".to_string(),
|
||||
json!(admin_usage_metadata_u64(item, "end_to_end_time_ms")),
|
||||
@@ -2842,6 +2848,32 @@ mod tests {
|
||||
assert_eq!(record["client_is_stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_payloads_expose_response_model_separately_from_mapping() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
target_model: Some("provider-mapped-model".to_string()),
|
||||
request_metadata: Some(json!({
|
||||
"provider_response_model": "gpt-5.1"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let active = admin_usage_active_request_json(&item, None, None, None);
|
||||
|
||||
for payload in [&record, &active] {
|
||||
assert_eq!(payload["target_model"], "provider-mapped-model");
|
||||
assert_eq!(payload["response_model"], "gpt-5.1");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_payloads_project_end_to_end_timings_from_metadata() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
@@ -169,6 +169,18 @@ pub fn admin_pool_key_account_quota_exhausted(
|
||||
aether_provider_pool::provider_pool_key_account_quota_exhausted(key, provider_type)
|
||||
}
|
||||
|
||||
pub fn admin_pool_key_minimum_quota_reached(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
provider_model_name: Option<&str>,
|
||||
) -> bool {
|
||||
aether_provider_pool::provider_pool_key_minimum_quota_reached(
|
||||
key,
|
||||
provider_type,
|
||||
provider_model_name,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn admin_pool_key_quota_hard_blocked(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
|
||||
@@ -404,6 +404,200 @@ pub fn parse_antigravity_quota_summary_response(
|
||||
(!parsed_groups.is_empty()).then_some(serde_json::Value::Array(parsed_groups))
|
||||
}
|
||||
|
||||
/// Windows reported by `GET /api/oauth/usage`, as `(response key, metadata prefix)`.
|
||||
pub const CLAUDE_CODE_USAGE_WINDOWS: [(&str, &str); 4] = [
|
||||
("five_hour", "five_hour"),
|
||||
("seven_day", "seven_day"),
|
||||
("seven_day_sonnet", "seven_day_sonnet"),
|
||||
("seven_day_overage_included", "seven_day_fable"),
|
||||
];
|
||||
|
||||
/// Parses the Anthropic OAuth usage response into the `claude_code` metadata bucket.
|
||||
///
|
||||
/// Each window carries `utilization` (percent, 0-100) and `resets_at` (RFC 3339).
|
||||
/// Windows that are absent or `null` (e.g. plans without a Sonnet/Fable window) are
|
||||
/// skipped; `None` is returned when no window is present at all.
|
||||
pub fn parse_claude_code_oauth_usage_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let root = value.as_object()?;
|
||||
let mut bucket = serde_json::Map::new();
|
||||
for (response_key, prefix) in CLAUDE_CODE_USAGE_WINDOWS {
|
||||
let Some(window) = root
|
||||
.get(response_key)
|
||||
.and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let utilization = window
|
||||
.get("utilization")
|
||||
.and_then(|value| match value {
|
||||
serde_json::Value::Number(number) => number.as_f64(),
|
||||
serde_json::Value::String(text) => text.trim().parse::<f64>().ok(),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|value| value.is_finite());
|
||||
let reset_at = window
|
||||
.get("resets_at")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|text| chrono::DateTime::parse_from_rfc3339(text.trim()).ok())
|
||||
.and_then(|time| u64::try_from(time.timestamp()).ok());
|
||||
if utilization.is_none() && reset_at.is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(utilization) = utilization {
|
||||
bucket.insert(
|
||||
format!("{prefix}_used_percent"),
|
||||
serde_json::json!(utilization.clamp(0.0, 100.0)),
|
||||
);
|
||||
}
|
||||
if let Some(reset_at) = reset_at {
|
||||
bucket.insert(format!("{prefix}_reset_at"), serde_json::json!(reset_at));
|
||||
}
|
||||
}
|
||||
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),
|
||||
);
|
||||
Some(serde_json::Value::Object(bucket))
|
||||
}
|
||||
|
||||
/// Projects the `cedar_ember` block (returned with `?cedar_ember=1`) into the same
|
||||
/// `reset_credits` shape codex uses. Upstream grant/organization ids are never copied; each
|
||||
/// usable grant becomes one entry, and `available_count` sums their remaining resets.
|
||||
fn parse_claude_code_reset_credits(
|
||||
root: &serde_json::Map<String, serde_json::Value>,
|
||||
now_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let grants = root
|
||||
.get("cedar_ember")
|
||||
.and_then(serde_json::Value::as_object)?
|
||||
.get("grants")
|
||||
.and_then(serde_json::Value::as_array)?;
|
||||
let parse_time = |grant: &serde_json::Map<String, serde_json::Value>, field: &str| {
|
||||
grant
|
||||
.get(field)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|text| chrono::DateTime::parse_from_rfc3339(text.trim()).ok())
|
||||
.and_then(|time| u64::try_from(time.timestamp()).ok())
|
||||
};
|
||||
let mut available_count = 0u64;
|
||||
let mut credits = Vec::new();
|
||||
for grant in grants.iter().filter_map(serde_json::Value::as_object) {
|
||||
let resets_left = grant
|
||||
.get("resets_left")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let has_clears = grant
|
||||
.get("clears")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|clears| !clears.is_empty());
|
||||
let paused = grant
|
||||
.get("paused")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let starts_at = parse_time(grant, "starts_at");
|
||||
let expires_at = parse_time(grant, "ends_at");
|
||||
if resets_left == 0
|
||||
|| !has_clears
|
||||
|| paused
|
||||
|| starts_at.is_some_and(|value| value > now_unix_secs)
|
||||
|| expires_at.is_some_and(|value| value <= now_unix_secs)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
available_count += resets_left;
|
||||
let Some(expires_at) = expires_at else {
|
||||
continue;
|
||||
};
|
||||
credits.push(serde_json::json!({
|
||||
"display_key": format!("Key-{}", credits.len() + 1),
|
||||
"status": "available",
|
||||
"expires_at": expires_at,
|
||||
"remaining_seconds": expires_at - now_unix_secs,
|
||||
}));
|
||||
}
|
||||
if available_count == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(serde_json::json!({
|
||||
"available_count": available_count,
|
||||
"updated_at": now_unix_secs,
|
||||
"detail_source": "claude_oauth_usage",
|
||||
"detail_status": "ok",
|
||||
"credits": credits,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parses the `anthropic-ratelimit-unified-*` response headers into a partial `claude_code`
|
||||
/// metadata bucket (same field names as [`parse_claude_code_oauth_usage_response`]).
|
||||
///
|
||||
/// Utilization headers are 0-1 fractions and are stored as percent; reset headers are Unix
|
||||
/// seconds (millisecond values are normalized). Returns `None` when no window is reported.
|
||||
pub fn parse_claude_code_usage_headers(
|
||||
headers: &BTreeMap<String, String>,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let normalized = headers
|
||||
.iter()
|
||||
.map(|(key, value)| (key.trim().to_ascii_lowercase(), value.trim().to_string()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut bucket = serde_json::Map::new();
|
||||
for (header_window, prefix) in [
|
||||
("5h", "five_hour"),
|
||||
("7d", "seven_day"),
|
||||
("7d_oi", "seven_day_fable"),
|
||||
] {
|
||||
let header = |suffix: &str| {
|
||||
normalized
|
||||
.get(&format!(
|
||||
"anthropic-ratelimit-unified-{header_window}-{suffix}"
|
||||
))
|
||||
.map(String::as_str)
|
||||
};
|
||||
if let Some(utilization) = header("utilization")
|
||||
.and_then(|value| value.parse::<f64>().ok())
|
||||
.filter(|value| value.is_finite())
|
||||
{
|
||||
bucket.insert(
|
||||
format!("{prefix}_used_percent"),
|
||||
serde_json::json!((utilization * 100.0).clamp(0.0, 100.0)),
|
||||
);
|
||||
}
|
||||
if let Some(reset_at) = header("reset")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.map(|value| {
|
||||
if value > 100_000_000_000 {
|
||||
value / 1_000
|
||||
} else {
|
||||
value
|
||||
}
|
||||
})
|
||||
.filter(|value| *value > 0)
|
||||
{
|
||||
bucket.insert(format!("{prefix}_reset_at"), serde_json::json!(reset_at));
|
||||
}
|
||||
}
|
||||
if bucket.is_empty() {
|
||||
return None;
|
||||
}
|
||||
bucket.insert(
|
||||
"updated_at".to_string(),
|
||||
serde_json::json!(updated_at_unix_secs),
|
||||
);
|
||||
Some(serde_json::Value::Object(bucket))
|
||||
}
|
||||
|
||||
pub fn parse_gemini_cli_retrieve_user_quota_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
@@ -3546,6 +3740,258 @@ pub fn parse_kiro_usage_response(
|
||||
Some(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
pub fn parse_xai_billing_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let root = value.as_object()?;
|
||||
let config = root
|
||||
.get("config")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.unwrap_or(root);
|
||||
|
||||
let usage_percentage = coerce_json_f64_from_map(config, "creditUsagePercent")
|
||||
.or_else(|| extract_xai_product_usage_percent(config));
|
||||
let period = config.get("currentPeriod");
|
||||
let period_type = period
|
||||
.and_then(|value| value.get("type").or_else(|| value.get("periodType")))
|
||||
.and_then(normalize_xai_period_type);
|
||||
let next_reset_at = period
|
||||
.and_then(|value| value.get("end"))
|
||||
.and_then(parse_xai_timestamp)
|
||||
.or_else(|| config.get("billingPeriodEnd").and_then(parse_xai_timestamp));
|
||||
let monthly_limit =
|
||||
coerce_xai_cents_dollars(config.get("monthlyLimit")).filter(|value| *value > 0.0);
|
||||
let current_usage = if monthly_limit.is_some() {
|
||||
coerce_xai_cents_dollars(config.get("used"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let remaining = monthly_limit
|
||||
.zip(current_usage)
|
||||
.map(|(limit, used)| (limit - used).max(0.0));
|
||||
let usage_percentage = usage_percentage.or_else(|| {
|
||||
monthly_limit
|
||||
.zip(current_usage)
|
||||
.map(|(limit, used)| ((used / limit) * 100.0).clamp(0.0, 100.0))
|
||||
});
|
||||
let usage_percentage = match usage_percentage {
|
||||
Some(value) => Some(value.clamp(0.0, 100.0)),
|
||||
None if period_type.is_some() || next_reset_at.is_some() => Some(0.0),
|
||||
None => None,
|
||||
};
|
||||
let prepaid_balance = coerce_xai_cents_dollars(config.get("prepaidBalance"));
|
||||
let on_demand_cap = coerce_xai_cents_dollars(config.get("onDemandCap"));
|
||||
let on_demand_used = coerce_xai_cents_dollars(config.get("onDemandUsed"));
|
||||
let on_demand_enabled = coerce_json_bool_from_map(root, "onDemandEnabled")
|
||||
.or_else(|| coerce_json_bool_from_map(config, "onDemandEnabled"));
|
||||
let subscription_title = first_json_string_by_paths(
|
||||
value,
|
||||
&[
|
||||
&["subscriptionTier"],
|
||||
&["subscription_tier"],
|
||||
&["config", "subscriptionTier"],
|
||||
&["config", "subscription_title"],
|
||||
],
|
||||
);
|
||||
|
||||
if usage_percentage.is_none()
|
||||
&& monthly_limit.is_none()
|
||||
&& current_usage.is_none()
|
||||
&& prepaid_balance.is_none()
|
||||
&& on_demand_cap.is_none()
|
||||
&& next_reset_at.is_none()
|
||||
&& subscription_title.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = serde_json::Map::new();
|
||||
result.insert("updated_at".to_string(), json!(updated_at_unix_secs));
|
||||
if let Some(value) = usage_percentage {
|
||||
result.insert("usage_percentage".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = monthly_limit {
|
||||
result.insert("usage_limit".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = current_usage {
|
||||
result.insert("current_usage".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = remaining {
|
||||
result.insert("remaining".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = next_reset_at {
|
||||
result.insert("next_reset_at".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = period_type {
|
||||
result.insert("period_type".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = prepaid_balance {
|
||||
result.insert("prepaid_balance".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = on_demand_cap {
|
||||
result.insert("on_demand_cap".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = on_demand_used {
|
||||
result.insert("on_demand_used".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = on_demand_enabled {
|
||||
result.insert("on_demand_enabled".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = subscription_title {
|
||||
result.insert("subscription_title".to_string(), json!(value));
|
||||
}
|
||||
Some(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
fn coerce_json_f64_from_map(
|
||||
object: &serde_json::Map<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> Option<f64> {
|
||||
object.get(key).and_then(coerce_json_f64)
|
||||
}
|
||||
|
||||
fn coerce_json_bool_from_map(
|
||||
object: &serde_json::Map<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> Option<bool> {
|
||||
object.get(key).and_then(coerce_json_bool)
|
||||
}
|
||||
|
||||
fn extract_xai_product_usage_percent(
|
||||
config: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> Option<f64> {
|
||||
let items = config.get("productUsage")?.as_array()?;
|
||||
let grok_build = items.iter().find(|item| {
|
||||
item.get("product")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|product| product.eq_ignore_ascii_case("GrokBuild"))
|
||||
});
|
||||
grok_build
|
||||
.or(items.first())
|
||||
.and_then(|item| item.get("usagePercent").and_then(coerce_json_f64))
|
||||
}
|
||||
|
||||
fn coerce_xai_cents_dollars(value: Option<&serde_json::Value>) -> Option<f64> {
|
||||
let value = value?;
|
||||
let cents = match value {
|
||||
serde_json::Value::Object(object) => object.get("val").and_then(coerce_json_f64)?,
|
||||
other => coerce_json_f64(other)?,
|
||||
};
|
||||
Some(cents / 100.0)
|
||||
}
|
||||
|
||||
fn normalize_xai_period_type(value: &serde_json::Value) -> Option<String> {
|
||||
let raw = value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let lowered = raw.to_ascii_lowercase();
|
||||
if lowered.contains("week") {
|
||||
Some("weekly".to_string())
|
||||
} else if lowered.contains("month") {
|
||||
Some("monthly".to_string())
|
||||
} else {
|
||||
Some(raw.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_xai_timestamp(value: &serde_json::Value) -> Option<u64> {
|
||||
if let Some(value) = coerce_json_u64(value) {
|
||||
return Some(if value > 1_000_000_000_000 {
|
||||
value / 1000
|
||||
} else {
|
||||
value
|
||||
});
|
||||
}
|
||||
let raw = value.as_str()?.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
chrono::DateTime::parse_from_rfc3339(raw)
|
||||
.ok()
|
||||
.and_then(|timestamp| u64::try_from(timestamp.timestamp()).ok())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod xai_quota_tests {
|
||||
use super::parse_xai_billing_response;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn parse_xai_credits_percent_and_weekly_period() {
|
||||
let metadata = parse_xai_billing_response(
|
||||
&json!({
|
||||
"config": {
|
||||
"currentPeriod": {
|
||||
"type": "USAGE_PERIOD_TYPE_WEEKLY",
|
||||
"start": "2026-08-08T01:53:09.930537+00:00",
|
||||
"end": "2026-08-15T01:53:09.930537+00:00"
|
||||
},
|
||||
"creditUsagePercent": 46.0,
|
||||
"productUsage": [
|
||||
{"product": "GrokBuild", "usagePercent": 41.0},
|
||||
{"product": "GrokChat"}
|
||||
],
|
||||
"onDemandCap": {"val": 0},
|
||||
"onDemandUsed": {"val": 0},
|
||||
"prepaidBalance": {"val": 0}
|
||||
},
|
||||
"subscriptionTier": "SuperGrok"
|
||||
}),
|
||||
1_775_000_000,
|
||||
)
|
||||
.expect("credits payload should parse");
|
||||
|
||||
assert_eq!(metadata["usage_percentage"], json!(46.0));
|
||||
assert_eq!(metadata["period_type"], json!("weekly"));
|
||||
assert_eq!(metadata["next_reset_at"], json!(1_786_758_789u64));
|
||||
assert_eq!(metadata["prepaid_balance"], json!(0.0));
|
||||
assert_eq!(metadata["on_demand_cap"], json!(0.0));
|
||||
assert_eq!(metadata["subscription_title"], json!("SuperGrok"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_xai_omitted_percent_as_fresh_weekly_zero() {
|
||||
let metadata = parse_xai_billing_response(
|
||||
&json!({
|
||||
"config": {
|
||||
"currentPeriod": {
|
||||
"type": "USAGE_PERIOD_TYPE_WEEKLY",
|
||||
"end": "2026-08-15T01:53:09.930537+00:00"
|
||||
},
|
||||
"isUnifiedBillingUser": true
|
||||
}
|
||||
}),
|
||||
1_775_000_000,
|
||||
)
|
||||
.expect("fresh weekly period should parse");
|
||||
|
||||
assert_eq!(metadata["usage_percentage"], json!(0.0));
|
||||
assert_eq!(metadata["period_type"], json!("weekly"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_xai_legacy_monthly_cents() {
|
||||
let metadata = parse_xai_billing_response(
|
||||
&json!({
|
||||
"config": {
|
||||
"monthlyLimit": {"val": 2500},
|
||||
"used": {"val": 1000},
|
||||
"billingPeriodEnd": "2026-09-01T00:00:00Z"
|
||||
}
|
||||
}),
|
||||
1_775_000_000,
|
||||
)
|
||||
.expect("legacy monthly payload should parse");
|
||||
|
||||
assert_eq!(metadata["usage_limit"], json!(25.0));
|
||||
assert_eq!(metadata["current_usage"], json!(10.0));
|
||||
assert_eq!(metadata["remaining"], json!(15.0));
|
||||
assert_eq!(metadata["usage_percentage"], json!(40.0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_windsurf_user_status_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
@@ -7452,3 +7898,163 @@ mod tests {
|
||||
assert!(!serialized.contains("user:password"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod claude_code_quota_tests {
|
||||
use super::{parse_claude_code_oauth_usage_response, parse_claude_code_usage_headers};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn parses_cedar_ember_grants_into_reset_credits() {
|
||||
let parsed = parse_claude_code_oauth_usage_response(
|
||||
&json!({
|
||||
"five_hour": {"utilization": 65.0, "resets_at": "2027-01-15T08:00:00Z"},
|
||||
"cedar_ember": {
|
||||
"eligible": true,
|
||||
"grants": [
|
||||
{"id": "launch", "resets_left": 2, "clears": ["five_hour"],
|
||||
"ends_at": "2027-01-20T00:00:00Z"},
|
||||
{"id": "later", "resets_left": 1, "clears": ["five_hour"],
|
||||
"starts_at": "2027-02-01T00:00:00Z"},
|
||||
{"id": "paused", "resets_left": 1, "clears": ["five_hour"], "paused": true},
|
||||
{"id": "spent", "resets_left": 0, "clears": ["five_hour"]},
|
||||
{"id": "expired", "resets_left": 1, "clears": ["five_hour"],
|
||||
"ends_at": "2027-01-01T00:00:00Z"}
|
||||
]
|
||||
}
|
||||
}),
|
||||
1_800_000_000,
|
||||
)
|
||||
.expect("usage should parse");
|
||||
|
||||
let credits = &parsed["reset_credits"];
|
||||
assert_eq!(credits["available_count"], json!(2));
|
||||
assert_eq!(credits["credits"].as_array().map(Vec::len), Some(1));
|
||||
assert_eq!(credits["credits"][0]["display_key"], json!("Key-1"));
|
||||
assert_eq!(credits["credits"][0]["expires_at"], json!(1_800_403_200u64));
|
||||
assert!(credits["credits"][0].get("id").is_none());
|
||||
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(
|
||||
&json!({"five_hour": {"utilization": 1.0}, "cedar_ember": null}),
|
||||
1,
|
||||
)
|
||||
.expect("usage should parse");
|
||||
assert!(parsed["reset_credits"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_unified_ratelimit_headers_into_percent_and_reset() {
|
||||
let headers = BTreeMap::from([
|
||||
(
|
||||
"Anthropic-Ratelimit-Unified-5h-Utilization".to_string(),
|
||||
"0.42".to_string(),
|
||||
),
|
||||
(
|
||||
"anthropic-ratelimit-unified-5h-reset".to_string(),
|
||||
"1800003600".to_string(),
|
||||
),
|
||||
(
|
||||
"anthropic-ratelimit-unified-7d-utilization".to_string(),
|
||||
"1.0".to_string(),
|
||||
),
|
||||
(
|
||||
"anthropic-ratelimit-unified-7d-reset".to_string(),
|
||||
"1800400000000".to_string(),
|
||||
),
|
||||
(
|
||||
"anthropic-ratelimit-unified-7d_oi-utilization".to_string(),
|
||||
"0.1".to_string(),
|
||||
),
|
||||
]);
|
||||
let parsed =
|
||||
parse_claude_code_usage_headers(&headers, 1_800_000_000).expect("headers should parse");
|
||||
|
||||
assert_eq!(parsed["five_hour_used_percent"], json!(42.0));
|
||||
assert_eq!(parsed["five_hour_reset_at"], json!(1_800_003_600u64));
|
||||
assert_eq!(parsed["seven_day_used_percent"], json!(100.0));
|
||||
// Millisecond timestamps are normalized to seconds.
|
||||
assert_eq!(parsed["seven_day_reset_at"], json!(1_800_400_000u64));
|
||||
assert_eq!(parsed["seven_day_fable_used_percent"], json!(10.0));
|
||||
assert!(parsed.get("seven_day_fable_reset_at").is_none());
|
||||
assert_eq!(parsed["updated_at"], json!(1_800_000_000u64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_ratelimit_headers_absent_yield_none() {
|
||||
let headers =
|
||||
BTreeMap::from([("content-type".to_string(), "application/json".to_string())]);
|
||||
assert!(parse_claude_code_usage_headers(&headers, 1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_windows_and_skips_null_ones() {
|
||||
let parsed = parse_claude_code_oauth_usage_response(
|
||||
&json!({
|
||||
"five_hour": {"utilization": 37.5, "resets_at": "2027-01-15T08:00:00.000000+00:00"},
|
||||
"seven_day": {"utilization": 12, "resets_at": "2027-01-20T00:00:00Z"},
|
||||
"seven_day_sonnet": null,
|
||||
"seven_day_overage_included": {"utilization": 3.0, "resets_at": null}
|
||||
}),
|
||||
1_800_000_000,
|
||||
)
|
||||
.expect("usage windows should parse");
|
||||
|
||||
assert_eq!(parsed["updated_at"], json!(1_800_000_000u64));
|
||||
assert_eq!(parsed["five_hour_used_percent"], json!(37.5));
|
||||
assert_eq!(parsed["five_hour_reset_at"], json!(1_800_000_000u64));
|
||||
assert_eq!(parsed["seven_day_used_percent"], json!(12.0));
|
||||
assert!(parsed.get("seven_day_sonnet_used_percent").is_none());
|
||||
assert_eq!(parsed["seven_day_fable_used_percent"], json!(3.0));
|
||||
assert!(parsed.get("seven_day_fable_reset_at").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_without_any_window() {
|
||||
assert!(parse_claude_code_oauth_usage_response(&json!({}), 1).is_none());
|
||||
assert!(parse_claude_code_oauth_usage_response(&json!({"five_hour": null}), 1).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,23 @@ pub fn enrich_admin_provider_oauth_auth_config(
|
||||
],
|
||||
);
|
||||
|
||||
if provider_type.trim().eq_ignore_ascii_case("xai") {
|
||||
auth_config.insert("auth_method".to_string(), json!("oauth"));
|
||||
auth_config.insert("using_api".to_string(), json!(false));
|
||||
if let Some(id_token) = ["id_token", "idToken"]
|
||||
.iter()
|
||||
.find_map(|field| json_non_empty_string(token_payload.get(field)))
|
||||
{
|
||||
auth_config
|
||||
.entry("id_token".to_string())
|
||||
.or_insert_with(|| json!(id_token.clone()));
|
||||
if let Some(claims) = decode_jwt_claims(&id_token) {
|
||||
merge_missing_auth_config_fields(auth_config, &claims, &["email", "sub"]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if provider_type.trim().eq_ignore_ascii_case("claude_code") {
|
||||
if let Some(organization_uuid) = token_payload_object
|
||||
.get("organization")
|
||||
@@ -554,6 +571,28 @@ mod tests {
|
||||
assert_eq!(auth_config.get("is_fedramp"), Some(&json!(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xai_enrichment_marks_oauth_and_extracts_id_token_identity() {
|
||||
let id_token = sample_unsigned_jwt(json!({
|
||||
"email": "[email protected]",
|
||||
"sub": "user-xai-1",
|
||||
}));
|
||||
let token_payload = json!({
|
||||
"access_token": "access-token",
|
||||
"refresh_token": "refresh-token",
|
||||
"id_token": id_token,
|
||||
});
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
enrich_admin_provider_oauth_auth_config("xai", &mut auth_config, &token_payload);
|
||||
|
||||
assert_eq!(auth_config.get("auth_method"), Some(&json!("oauth")));
|
||||
assert_eq!(auth_config.get("using_api"), Some(&json!(false)));
|
||||
assert_eq!(auth_config.get("email"), Some(&json!("[email protected]")));
|
||||
assert_eq!(auth_config.get("sub"), Some(&json!("user-xai-1")));
|
||||
assert_eq!(auth_config.get("id_token"), Some(&json!(id_token)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_jwt_claims_rejects_oversized_payload_before_decode() {
|
||||
let max_encoded_len = MAX_UNVERIFIED_JWT_CLAIMS_BYTES
|
||||
|
||||
Reference in New Issue
Block a user