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

  1. Check the raw response body for the observed_at field using the same bearer key
  2. Fix the sub2api server to always emit a non-empty ISO-8601 observation timestamp
  3. If you control the caller, fall back to the local clock with a warning instead of failing the whole billing view
  4. 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

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.