BigPizzaV3/CodexPlusPlus · error

倍率接口缺少观测时间

Error message

倍率接口缺少观测时间

What it means

validate_sub2api_billing_response checks that a sub2api billing/user-info API response carries all required fields before the app trusts it. Besides validating the user rate multiplier, it requires a non-empty `observed_at` field: without an observation timestamp the billing data cannot be presented as current, so the library bails instead of displaying stale or untraceable rate info.

Solutions

  1. Upgrade the sub2api server to a version whose billing endpoint returns `observed_at`.
  2. Inspect the raw JSON response (curl the billing endpoint) and confirm the field name is exactly `observed_at` and non-empty.
  3. Remove any proxy/mock/fixture layer that strips the field, or update test fixtures to include a timestamp.
  4. If the upstream truly has no timestamp, display a placeholder instead of calling this validation path.

Example fix

// before
let response: Sub2apiBillingResponse = serde_json::from_str(&body)?;
fetch_sub2api_billing_info(...)?;
// after
// ensure server returns observed_at, or guard before calling:
if body.contains("observed_at") {
    fetch_sub2api_billing_info(...)?;
} else {
    // handle legacy server: show multiplier without timestamp
}
Defensive patterns

Strategy: validation

Validate before calling

let resp: serde_json::Value = serde_json::from_str(&body)?;
let observed_at = resp.get("observed_at").and_then(|v| v.as_str()).unwrap_or("");
if observed_at.trim().is_empty() {
    // legacy server: skip billing display or fetch from another endpoint
}

Type guard

fn has_observed_at(v: &serde_json::Value) -> bool {
    v.get("observed_at").and_then(|x| x.as_str()).map_or(false, |s| !s.trim().is_empty())
}

Try / catch

match fetch_sub2api_billing_info(...) {
    Ok(info) => render(info),
    Err(e) if e.to_string().contains("观测时间") => render_stale_banner(),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling fetch_sub2api_billing_info against a sub2api endpoint whose JSON response omits `observed_at` or returns it as an empty/whitespace-only string. The multiplier check may pass (field optional) but the missing timestamp still aborts.

Common situations: Pointing the app at an older sub2api server version that predates the `observed_at` field; a reverse proxy or mock that strips/renames fields; hand-written test fixtures that only include `user_rate_multiplier`.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/8270255cb545e8ae. Report an issue: GitHub.

Appendix: 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 b1ed92e5e4)