BigPizzaV3/CodexPlusPlus · error · anyhow::Error

倍率接口返回了无效用户倍率

Error message

倍率接口返回了无效用户倍率

What it means

Thrown by validate_sub2api_billing_response inside fetch_sub2api_billing_info (crates/codex-plus-core/src/sub2api.rs). After GET {base}/v1/sub2api/billing returns HTTP 200 with parseable JSON, every multiplier is sanity-checked with !value.is_finite() || value < 0.0. This variant fires only for the optional user_rate_multiplier field (Option<f64> with #[serde(default)]): the field was present in the response as a number, but that number is NaN, infinite, or negative. The three mandatory multipliers (group/resolved/effective) passed the identical check just before it.

Source

Thrown at crates/codex-plus-core/src/sub2api.rs:121

fn validate_sub2api_billing_response(response: &Sub2ApiBillingResponse) -> anyhow::Result<()> {
    if response.object != "sub2api.key_billing"
        || response.schema_version != 1
        || response.billing_scope != "token"
    {
        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)

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Replay the request with the same credentials (curl -H "Authorization: Bearer <key>" <base>/v1/sub2api/billing) and inspect the actual user_rate_multiplier value
  2. Fix the value on the sub2api server so it is a finite number >= 0, or remove the per-user override so the field is absent/null
  3. If you own the calling code, degrade instead of aborting: fall back to resolved_rate_multiplier or 1.0 and surface a warning
  4. Retry after the upstream fix — validation runs on every fetch, so no client-side cache needs clearing

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("无效用户倍率") => {
        tracing::warn!(?err, "invalid user multiplier, using defaults");
        default_billing_info(&profile)
    }
    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("无效用户倍率") => {
        // fall back to cached/default multipliers and warn
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Calling fetch_sub2api_billing_info(&profile) where the upstream JSON contains "user_rate_multiplier": -0.5 (negative) or "user_rate_multiplier": 1e999 (serde_json parses this to f64 infinity). A JSON null or an absent field does NOT trigger it — serde maps those to None and the branch is skipped.

Common situations: A sub2api server with a per-user discount override misconfigured to a negative value; schema drift after an upstream version change; a relay/gateway in front of the endpoint corrupting numeric fields; mock servers in tests returning placeholder values like -1.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/03afa8bb174e8990. Report an issue: GitHub.