BigPizzaV3/CodexPlusPlus · error · anyhow::Error
倍率接口缺少观测时间
Error message
倍率接口缺少观测时间
What it means
Thrown by validate_sub2api_billing_response during fetch_sub2api_billing_info when the parsed billing response has an observed_at field that is empty or only whitespace. observed_at is a required String in Sub2ApiBillingResponse, so deserialization itself succeeds only when the key exists — but its content is checked for non-emptiness afterwards. This is the final validation step before the response is accepted as Sub2ApiBillingInfo.
Source
Thrown at crates/codex-plus-core/src/sub2api.rs:125
{
anyhow::bail!("倍率接口返回结构不是 sub2api.key_billing");
}
for value in [
response.group_rate_multiplier,
response.resolved_rate_multiplier,
response.effective_rate_multiplier,
] {
if !value.is_finite() || value < 0.0 {
anyhow::bail!("倍率接口返回了无效倍率");
}
}
if let Some(value) = response.user_rate_multiplier {
if !value.is_finite() || value < 0.0 {
anyhow::bail!("倍率接口返回了无效用户倍率");
}
}
if response.observed_at.trim().is_empty() {
anyhow::bail!("倍率接口缺少观测时间");
}
Ok(())
}
fn sub2api_error_message(body: &str) -> Option<String> {
let payload: Value = serde_json::from_str(body).ok()?;
for path in [&["error", "message"][..], &["message"][..], &["error"][..]] {
let mut current = &payload;
for key in path {
current = current.get(*key)?;
}
if let Some(message) = current
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(message.to_string());
}View on GitHub (pinned to 1f431ae49b)
Solutions
- Check the raw response body for the observed_at field using the same bearer key
- Fix the sub2api server to always emit a non-empty ISO-8601 observation timestamp
- If you control the caller, fall back to the local clock with a warning instead of failing the whole billing view
- Retry after the upstream fix — the field is re-validated on every fetch
Example fix
// before
let billing = fetch_sub2api_billing_info(&profile).await?;
// after
let billing = match fetch_sub2api_billing_info(&profile).await {
Ok(info) => info,
Err(err) if err.to_string().contains("缺少观测时间") => {
let mut info = last_known_billing(&profile);
info.observed_at = chrono::Utc::now().to_rfc3339(); // local fallback
info
}
Err(err) => return Err(err),
}; Defensive patterns
Strategy: try-catch
Try / catch
match fetch_sub2api_billing_info(&profile).await {
Ok(info) => { /* use info */ }
Err(err) if err.to_string().contains("缺少观测时间") => {
// rebuild from last-known values, stamp observed_at with local time, warn
}
Err(err) => return Err(err),
} Prevention
- Contract-test that observed_at is a non-empty ISO-8601 string
- Probe the billing endpoint in monitoring so missing timestamps are caught before users see failures
- Treat the timestamp as display metadata: fall back to local time rather than blocking the billing view
When it happens
Trigger: GET {base}/v1/sub2api/billing returns 200 JSON with "observed_at": "" or "observed_at": " ". All object/schema_version and multiplier checks passed; only the timestamp string is blank.
Common situations: An upstream sub2api deployment that stamps the timestamp in a different layer and omits it on some code paths; a stub server in tests that fills every field except observed_at; an older server version whose schema predates observed_at but still passes the object/schema_version check.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16).
Data as JSON: /api/errors/8270255cb545e8ae.
Report an issue: GitHub.