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
- Replay the request with the same credentials (curl -H "Authorization: Bearer <key>" <base>/v1/sub2api/billing) and inspect the actual user_rate_multiplier value
- 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
- If you own the calling code, degrade instead of aborting: fall back to resolved_rate_multiplier or 1.0 and surface a warning
- 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
- Cache the last valid Sub2ApiBillingInfo so a bad upstream response degrades instead of breaking the UI
- If you operate the sub2api server, validate multipliers (finite, >= 0) at write time
- Pin schema_version 1 in contract tests so upstream schema drift surfaces in CI, not as runtime NaN/infinity
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.