mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix: correct quota sync and cache metrics
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -156,7 +156,7 @@
|
||||
- `free`:常见只看到 monthly/30d,是因为短周期窗口可能不存在,或者月限额由 spend control 提供。
|
||||
- model/additional:例如 `gpt-5.3-codex-spark` 这类 additional limit 会按 `limit_name` 单独组成 snapshot,和 core 限额不是同一行。
|
||||
|
||||
官方阻断判断也不是单看 `rate_limit.limit_reached`:`rate_limit.allowed === false`、`credits` 不可用、`spend_control.reached === true` 都会进入“额度不可用”的判断路径。additional limit 的阻断是按对应 `limit_name` 独立判断,不能简单等同于整个账号不可用。
|
||||
官方阻断判断也不是单看 `rate_limit.limit_reached`:`rate_limit.allowed === false`、明确的 credit overage、`spend_control.reached === true` 都会进入“额度不可用”的判断路径。free 账号可出现 `credits.has_credits = false` 且 `rate_limit.allowed = true`,不能把 `has_credits = false` 单独当成账号不可用。additional limit 的阻断是按对应 `limit_name` 独立判断,不能简单等同于整个账号不可用。
|
||||
|
||||
### 官方响应事件
|
||||
|
||||
@@ -419,6 +419,15 @@ Usage 请求路径按 base URL 选择:
|
||||
|
||||
本地 `account_cookies` 当前没有该 active 账号的 `cf_clearance`,所以这次真实链路只验证 usage 主刷新路径未被 cookie 改动破坏;cookie 头携带行为由 `tests/admin/accounts/quota.rs` 和 `tests/upstream/accounts/quota_refresh.rs` 的 route/mock 请求头测试覆盖。
|
||||
|
||||
2026-06-26 使用当前本地 8080 实例验证修复后的真实请求链路。先调用管理端 `GET /api/admin/accounts/quota?id=acct_fbed55cb574b4aee81760d0e8ac84ba6` 刷新真实官方 usage,返回 `allowed = true`、`limit_reached = false`、`credits.has_credits = false`、月窗口 `used_percent = 6`;账号保持 `active`,没有被写入本地 quota lock。
|
||||
|
||||
随后使用同一 active free 账号发起真实 Responses 请求:
|
||||
|
||||
- HTTP SSE:`/v1/responses`、`use_websocket = false` 返回 200,`responseId = resp_030daafe01e3005a016a3d723972348194bfd3508fd170d13f`,输出 `ok`,日志 `transport = http_sse`,`total_tokens = 14`。
|
||||
- WebSocket:`/v1/responses`、`use_websocket = true` 返回 200,`responseId = resp_08f5f7aa3db3c93d016a3d724cd85c819a8d41afa2221047c0`,输出 `ws-ok`,日志 `transport = websocket`,`total_tokens = 16`。
|
||||
- 连续 WS 对话:同一 `prompt_cache_key = ws-stability-20260626` 下连续 5 轮自然语言请求,第二轮起携带上一轮 `previous_response_id` 续链;5 条日志全部为 `statusCode = 200`、`transport = websocket`,没有 fallback 到 HTTP。5 轮输出均正常完成,最新账号状态仍为 `active`,管理端账号列表显示请求数增长到 11,月限额仍为 6.0%。
|
||||
- 今日缓存命中:同一 `prompt_cache_key = ws-cache-hit-20260626` 下发送两轮长前缀自然语言对话,第一轮 `cached_tokens = 0`,第二轮 `cached_tokens = 1792`,对应 dashboard `todayHitRate = 0.23938017632914774`、`totalCachedTokens = 7936`。这说明今天的缓存命中路径是通的,之前显示 0 只是因为短对话没有触发缓存。
|
||||
|
||||
结论:本地真实 free 账号确认为 30d/月限额单独展示。当前本地没有 active 的 5h+7d 账号可实时验证组合返回;该组合路径由官方 bundle 证据和项目测试覆盖。
|
||||
|
||||
## 当前差距
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn quota_snapshot_limit_reached(quota: &Value) -> bool {
|
||||
core_snapshot_blocked(quota)
|
||||
|| spend_control_limit_reached(quota)
|
||||
|| monthly_limit_reached(quota)
|
||||
|| credits_exhausted(quota)
|
||||
|| credits_overage_limit_reached(quota)
|
||||
}
|
||||
|
||||
/// 从配额快照读取 UTC 重置时间。
|
||||
@@ -306,15 +306,11 @@ fn bucket_window_minutes(bucket: &Value) -> Option<u64> {
|
||||
.filter(|value| *value > 0)
|
||||
}
|
||||
|
||||
fn credits_exhausted(quota: &Value) -> bool {
|
||||
fn credits_overage_limit_reached(quota: &Value) -> bool {
|
||||
quota
|
||||
.pointer("/credits/unlimited")
|
||||
.pointer("/credits/overage_limit_reached")
|
||||
.and_then(Value::as_bool)
|
||||
.is_some_and(|unlimited| !unlimited)
|
||||
&& quota
|
||||
.pointer("/credits/has_credits")
|
||||
.and_then(Value::as_bool)
|
||||
.is_some_and(|has_credits| !has_credits)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn number_value(value: &Value) -> Option<f64> {
|
||||
|
||||
@@ -5,7 +5,9 @@ use codex_proxy_rs::infra::crypto::SecretBox;
|
||||
use codex_proxy_rs::infra::database::connect_sqlite;
|
||||
use codex_proxy_rs::upstream::accounts::cookies::SqliteCookieStore;
|
||||
use codex_proxy_rs::upstream::accounts::model::AccountStatus;
|
||||
use codex_proxy_rs::upstream::accounts::quota::{quota_from_usage, RuntimeQuotaRefreshService};
|
||||
use codex_proxy_rs::upstream::accounts::quota::{
|
||||
quota_from_usage, quota_snapshot_limit_reached, RuntimeQuotaRefreshService,
|
||||
};
|
||||
use codex_proxy_rs::upstream::accounts::store::{NewAccount, SqliteAccountStore};
|
||||
use codex_proxy_rs::upstream::transport::CodexBackendClient;
|
||||
use secrecy::SecretString;
|
||||
@@ -138,6 +140,45 @@ fn quota_from_usage_should_preserve_additional_limit_name_and_metered_feature()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quota_snapshot_limit_reached_should_keep_allowed_free_account_without_reset_credits_available() {
|
||||
let usage = json!({
|
||||
"plan_type": "free",
|
||||
"rate_limit": {
|
||||
"allowed": true,
|
||||
"limit_reached": false,
|
||||
"primary_window": {
|
||||
"used_percent": 6,
|
||||
"reset_at": 1_806_364_800,
|
||||
"limit_window_seconds": 2_592_000
|
||||
}
|
||||
},
|
||||
"credits": {
|
||||
"has_credits": false,
|
||||
"unlimited": false,
|
||||
"overage_limit_reached": false,
|
||||
"balance": 0
|
||||
}
|
||||
});
|
||||
let quota = quota_from_usage(&usage);
|
||||
|
||||
assert!(!quota_snapshot_limit_reached("a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quota_snapshot_limit_reached_should_block_explicit_credit_overage_limit() {
|
||||
let quota = json!({
|
||||
"credits": {
|
||||
"has_credits": false,
|
||||
"unlimited": false,
|
||||
"overage_limit_reached": true,
|
||||
"balance": 0
|
||||
}
|
||||
});
|
||||
|
||||
assert!(quota_snapshot_limit_reached("a));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn quota_refresh_service_should_send_usage_cookie_when_cookie_store_is_configured() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -243,7 +243,7 @@ export function useDashboard(): {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '缓存命中',
|
||||
title: '今日缓存命中',
|
||||
value: formatRate(cache.todayHitRate),
|
||||
icon: Timer,
|
||||
tone: cache.todayHitRate && cache.todayHitRate > 0 ? 'warning' : 'normal',
|
||||
@@ -255,8 +255,8 @@ export function useDashboard(): {
|
||||
details: [
|
||||
{ label: '总缓存命中', value: formatRate(cache.totalHitRate), tone: 'warning' },
|
||||
{
|
||||
label: '首字 / 完成',
|
||||
value: `${formatLatency(cache.firstTokenLatencyMs)} / ${formatLatency(cache.completionLatencyMs)}`,
|
||||
label: '缓存 Token',
|
||||
value: formatTokens(cache.totalCachedTokens),
|
||||
tone: 'warning',
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user