From b396c635c51f133e6aa77b2def724cecad66fc91 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 12:28:16 +0900 Subject: [PATCH 1/3] fix(quota): correct GLM Coding Plan auth and stop reporting MCP calls as model quota MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1168. Two defects in the BigModel Coding Plan quota probe: 1. open.bigmodel.cn expects the API key directly in Authorization with no scheme prefix and answers a Bearer header with an auth error, so BigModel Coding Plan quota never rendered at all. api.z.ai keeps Bearer. The host is already canonicalized by isCanonicalZaiBaseUrl and redirect stays 'error', so the bare key cannot reach a lookalike host. 2. TIME_LIMIT rows were mapped to monthlyPercent, but they are the shared monthly MCP *call* allowance for search-prime / web-reader / zread, not a model-token window. This was not merely a mislabelled bar: headroomOf() in src/oauth/account-quota-rank.ts takes the MAX across every window, so a user who spent their web-search allowance had that read as exhausted model capacity and a healthy account was demoted in quota-aware ranking. Row type is now gated before a percentage is derived, so an MCP row cannot contribute a value at all, and a payload carrying only TIME_LIMIT rows reports no quota rather than a fabricated one. Absent windows stay absent instead of being synthesized as 0. Tests: existing Z.AI/BigModel cases updated to assert the raw-key header and absent monthly window, plus two new regressions — a fully-spent MCP allowance yields no report, and the real V1 Lite shape keeps weekly/monthly absent rather than 0. --- .../src/content/docs/guides/providers.md | 27 ++++-- src/providers/quota.ts | 54 +++++++---- tests/provider-quota.test.ts | 93 ++++++++++++++++--- 3 files changed, 130 insertions(+), 44 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index f8d13cbe87..3a3276c602 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -655,13 +655,22 @@ their matching bars. OpenCodex does not reconstruct dollar caps from local usage provider using a non-canonical `baseUrl` is never sent the key for this probe. **Z.AI GLM Coding Plan quota.** The `zai`, `glm`, `glm-cn`, and `zhipu-bigmodel-coding` -presets read `GET /api/monitor/usage/quota/limit` with the configured key as a Bearer token -and do not follow redirects. The probe runs against the region the provider points at: +presets read `GET /api/monitor/usage/quota/limit` and do not follow redirects. The probe +runs against the region the provider points at: `api.z.ai` (bare or `/api/coding/paas/v4`) or `open.bigmodel.cn` (bare, -`/api/coding/paas/v4`, or the OpenAI Responses endpoint `/api/v1`). The response's `limits` -rows fill the utilization bars: `TOKENS_LIMIT` / `CREDIT_LIMIT` rows with `unit` 3 / -`number` 5 fill the 5-hour bar and `unit` 6 / `number` 1 the weekly bar, while -`TIME_LIMIT` rows fill the monthly MCP bar. The v2 coding-plan protocol reports the -monthly MCP row; the newer protocol does not, so the monthly bar renders only when that -row is present. A provider using a non-canonical `baseUrl` is never sent the key for this -probe. +`/api/coding/paas/v4`, or the OpenAI Responses endpoint `/api/v1`). + +Authentication differs by region: `api.z.ai` takes the key as a Bearer token, while +`open.bigmodel.cn` expects the key directly in `Authorization` with no scheme prefix and +rejects a Bearer header. The response's `limits` rows fill the utilization bars: +`TOKENS_LIMIT` / `CREDIT_LIMIT` rows with `unit` 3 / `number` 5 fill the 5-hour bar and +`unit` 6 / `number` 1 the weekly bar. + +`TIME_LIMIT` rows are **not** model quota and are ignored. They are the shared monthly +MCP call allowance for Web Search, Web Reader, and Zread, so treating them as a model +window would let a spent web-search budget read as exhausted model capacity in +quota-aware account ranking. A plan that reports only `TIME_LIMIT` rows therefore shows +no quota bars rather than a fabricated one, and windows the plan does not report stay +absent instead of rendering as 0%. + +A provider using a non-canonical `baseUrl` is never sent the key for this probe. diff --git a/src/providers/quota.ts b/src/providers/quota.ts index e27bb29a31..19009bb298 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -636,10 +636,19 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT` * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 → * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly - * window). `TIME_LIMIT` rows are the monthly MCP tool budget (Web Search / Web - * Reader / Zread). Every row's `percentage` is the consumed share (falling + * window). Every row's `percentage` is the consumed share (falling * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms) * the window reset. + * + * `TIME_LIMIT` rows are deliberately ignored (issue #1168). They are the shared + * monthly MCP *call* allowance for Web Search / Web Reader / Zread — not a + * model-token budget — and `ProviderQuota.monthlyPercent` is consumed as a + * model-capacity signal: `headroomOf()` in `src/oauth/account-quota-rank.ts` + * takes the MAX across every window, so a user who spent their MCP search + * allowance would be ranked as having no model capacity left, and the dashboard + * would draw a full monthly bar for a plan whose model tokens are untouched. + * A payload carrying only `TIME_LIMIT` rows therefore reports no quota at all, + * which is the honest answer rather than a fabricated one. */ export function parseZaiQuotaLimits(data: Record | null): ProviderQuota | null { const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null; @@ -649,6 +658,9 @@ export function parseZaiQuotaLimits(data: Record | null): Provi for (const raw of limits) { const row = asRecord(raw); if (!row) continue; + // Gate on row type before deriving a percentage: an MCP row must not even + // contribute a parsed value to a model-quota report. + if (row.type !== "TOKENS_LIMIT" && row.type !== "CREDIT_LIMIT") continue; const resetAt = normalizeResetAt(row.nextResetTime); let percent = normalizePercent(row.percentage); if (percent === undefined) { @@ -659,21 +671,15 @@ export function parseZaiQuotaLimits(data: Record | null): Provi } } if (percent === undefined) continue; - if (row.type === "TOKENS_LIMIT" || row.type === "CREDIT_LIMIT") { - const unit = toFiniteNumber(row.unit); - const number = toFiniteNumber(row.number); - if (unit === 3 && number === 5) { - quota.fiveHourPercent = percent; - if (resetAt !== undefined) quota.fiveHourResetAt = resetAt; - windows += 1; - } else if (unit === 6 && number === 1) { - quota.weeklyPercent = percent; - if (resetAt !== undefined) quota.weeklyResetAt = resetAt; - windows += 1; - } - } else if (row.type === "TIME_LIMIT") { - quota.monthlyPercent = percent; - if (resetAt !== undefined) quota.monthlyResetAt = resetAt; + const unit = toFiniteNumber(row.unit); + const number = toFiniteNumber(row.number); + if (unit === 3 && number === 5) { + quota.fiveHourPercent = percent; + if (resetAt !== undefined) quota.fiveHourResetAt = resetAt; + windows += 1; + } else if (unit === 6 && number === 1) { + quota.weeklyPercent = percent; + if (resetAt !== undefined) quota.weeklyResetAt = resetAt; windows += 1; } } @@ -715,9 +721,16 @@ function parseZaiQuotaLegacyFields(data: Record | null): Provid /** * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider - * points at (api.z.ai or open.bigmodel.cn). Authenticates with the API key as - * a Bearer token per Z.AI's API reference. The `limits` array shape is + * points at (api.z.ai or open.bigmodel.cn). The `limits` array shape is * preferred; older field-name payloads fall back to the legacy parser. + * + * Authentication differs by host (issue #1168). `api.z.ai` takes the API key as + * a Bearer token per Z.AI's API reference; `open.bigmodel.cn` expects the key + * directly in `Authorization` with no scheme prefix and answers a Bearer header + * with an auth error, which is why BigModel Coding Plan quota never rendered. + * The host is already canonicalized by `isCanonicalZaiBaseUrl` above and + * `redirect: "error"` stays set, so the bare key cannot travel to a lookalike + * host or follow a redirect off-origin. */ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise { if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null; @@ -727,8 +740,9 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4` ? ZAI_BASE_URL : ZAI_CN_BASE_URL; + const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`; const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, { - headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + headers: { Accept: "application/json", Authorization: authorization }, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 5eef964306..633abb0349 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -979,9 +979,11 @@ describe("fetchProviderQuotaReports", () => { fiveHourResetAt: 1789000000000, weeklyPercent: 52, weeklyResetAt: 1789600000000, - monthlyPercent: 12.3, - monthlyResetAt: 1789000000000, }); + // The TIME_LIMIT row is the MCP call allowance, not a model-token window, so it + // must not surface as monthly model quota (issue #1168). + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); + expect(result.reports[0]?.quota.monthlyResetAt).toBeUndefined(); expect(seen).toHaveLength(1); expect(seen[0]?.url).toBe("https://api.z.ai/api/monitor/usage/quota/limit"); expect(seen[0]?.authorization).toBe("Bearer zai-secret"); @@ -1017,11 +1019,12 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports[0]?.quota).toMatchObject({ fiveHourPercent: 20, weeklyPercent: 52, - monthlyPercent: 7.5, }); + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); expect(seen).toHaveLength(1); expect(seen[0]?.url).toBe("https://open.bigmodel.cn/api/monitor/usage/quota/limit"); - expect(seen[0]?.authorization).toBe("Bearer zai-secret"); + // BigModel takes the raw key; a Bearer prefix is rejected upstream (issue #1168). + expect(seen[0]?.authorization).toBe("zai-secret"); expect(seen[0]?.redirect).toBe("error"); }); @@ -1044,8 +1047,8 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports[0]?.quota).toMatchObject({ fiveHourPercent: 40.5, weeklyPercent: 52, - monthlyPercent: 12.3, }); + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); expect(seen).toHaveLength(1); expect(seen[0]?.url).toBe("https://api.z.ai/api/monitor/usage/quota/limit"); }); @@ -1078,11 +1081,11 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports[0]?.quota).toMatchObject({ fiveHourPercent: 30, weeklyPercent: 60, - monthlyPercent: 9.5, }); + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); expect(seen).toHaveLength(1); expect(seen[0]?.url).toBe("https://open.bigmodel.cn/api/monitor/usage/quota/limit"); - expect(seen[0]?.authorization).toBe("Bearer zai-secret"); + expect(seen[0]?.authorization).toBe("zai-secret"); }); test("Z.AI quota treats an unsuccessful payload as a no-report", async () => { @@ -1127,7 +1130,7 @@ describe("fetchProviderQuotaReports", () => { expect(seen).toEqual([]); }); - test("Z.AI quota ignores token rows whose window length does not match", async () => { + test("Z.AI quota reports nothing when only unmatched token rows and an MCP row remain", async () => { globalThis.fetch = (async () => new Response(JSON.stringify({ success: true, data: { @@ -1141,10 +1144,10 @@ describe("fetchProviderQuotaReports", () => { const result = await fetchProviderQuotaReports(keyQuotaConfig("zai", "https://api.z.ai/api/coding/paas/v4"), true); - expect(result.reports).toHaveLength(1); - expect(result.reports[0]?.quota).toMatchObject({ monthlyPercent: 12.3 }); - expect(result.reports[0]?.quota.fiveHourPercent).toBeUndefined(); - expect(result.reports[0]?.quota.weeklyPercent).toBeUndefined(); + // Neither token row matches a known window length and the TIME_LIMIT row is the MCP + // allowance, so there is no model-quota evidence at all. Reporting no quota is the + // honest outcome; previously the MCP row alone produced a monthly model bar. + expect(result.reports).toEqual([]); }); test("Z.AI quota does not fall back to legacy fields when limits is present but empty", async () => { @@ -1158,7 +1161,7 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports).toEqual([]); }); - test("Z.AI quota renders a real v2 coding-plan response (monthly MCP TIME_LIMIT)", async () => { + test("Z.AI quota ignores the monthly MCP TIME_LIMIT row in a real v2 response", async () => { // Sanitized live response captured from the /api/monitor/usage/quota/limit probe // (level=max, v2 protocol): the TIME_LIMIT row is the 30-day MCP tool budget // (search-prime / web-reader / zread), independent of the token windows. @@ -1181,9 +1184,69 @@ describe("fetchProviderQuotaReports", () => { fiveHourResetAt: 1787056863927, weeklyPercent: 20, weeklyResetAt: 1787641095989, - monthlyPercent: 0, - monthlyResetAt: 1788073095998, }); + // The MCP allowance is untouched here (percentage 0) while the five-hour model + // window is fully consumed. Reporting the MCP row as monthly model quota is what + // issue #1168 removes: headroomOf() takes the MAX across windows, so an exhausted + // MCP budget would otherwise be read as exhausted model capacity. + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); + expect(result.reports[0]?.quota.monthlyResetAt).toBeUndefined(); + }); + + test("Z.AI quota reports no model window when the payload carries only an MCP TIME_LIMIT row", async () => { + // A BigModel V1 Lite plan whose MCP allowance is fully spent but whose model tokens + // are untouched. Before issue #1168 this produced monthlyPercent: 100, and because + // headroomOf() in src/oauth/account-quota-rank.ts takes the MAX across every window, + // the account ranked as having ZERO model headroom — a healthy account demoted, or + // skipped, over a spent web-search budget. + globalThis.fetch = (async () => new Response(JSON.stringify({ + success: true, + data: { + limits: [ + { type: "TIME_LIMIT", unit: 5, number: 1, usage: 100, currentValue: 100, remaining: 0, percentage: 100, nextResetTime: 1788921262994, + usageDetails: [{ modelCode: "search-prime", usage: 60 }, { modelCode: "web-reader", usage: 40 }, { modelCode: "zread", usage: 0 }] }, + ], + level: "lite", + }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("zhipu-bigmodel-coding", "https://open.bigmodel.cn/api/coding/paas/v4", "zai-secret"), + true, + ); + + expect(result.reports).toEqual([]); + }); + + test("Z.AI quota omits absent windows rather than reporting them as zero", async () => { + // Real V1 Lite shape from issue #1168: one five-hour token window plus the MCP row. + // Weekly and monthly must be ABSENT, not 0 — a synthesized 0 would draw a + // full-capacity weekly bar for a plan that never reported one. + globalThis.fetch = (async () => new Response(JSON.stringify({ + code: 200, + msg: "操作成功", + success: true, + data: { + limits: [ + { type: "TIME_LIMIT", unit: 5, number: 1, usage: 100, currentValue: 0, remaining: 100, percentage: 0, nextResetTime: 1788921262994, + usageDetails: [{ modelCode: "search-prime", usage: 0 }, { modelCode: "web-reader", usage: 0 }, { modelCode: "zread", usage: 0 }] }, + { type: "TOKENS_LIMIT", unit: 3, number: 5, percentage: 1, nextResetTime: 1786626122911 }, + ], + level: "lite", + }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("zhipu-bigmodel-coding", "https://open.bigmodel.cn/api/coding/paas/v4", "zai-secret"), + true, + ); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota).toMatchObject({ fiveHourPercent: 1, fiveHourResetAt: 1786626122911 }); + expect(result.reports[0]?.quota.weeklyPercent).toBeUndefined(); + expect(result.reports[0]?.quota.weeklyResetAt).toBeUndefined(); + expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); + expect(result.reports[0]?.quota.monthlyResetAt).toBeUndefined(); }); test("Z.AI quota renders a real new-protocol response without the monthly MCP row", async () => { From 1f33ddc94c73d07527997344517914f4c1bacbad Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 12:29:05 +0900 Subject: [PATCH 2/3] test(quota): keep the legacy Z.AI monthlyMCPUsage mapping intact The legacy field-name fallback is a different contract from the limits[] parser: it reads an explicit monthlyMCPUsage field from an api.z.ai payload, where mapping to monthlyPercent is the pre-existing documented behavior. Issue #1168 scopes the TIME_LIMIT exclusion to the limits[] parser only, so parseZaiQuotaLegacyFields stays untouched to avoid regressing older responses. --- tests/provider-quota.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 633abb0349..32ea4dab2b 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -1047,8 +1047,8 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports[0]?.quota).toMatchObject({ fiveHourPercent: 40.5, weeklyPercent: 52, + monthlyPercent: 12.3, }); - expect(result.reports[0]?.quota.monthlyPercent).toBeUndefined(); expect(seen).toHaveLength(1); expect(seen[0]?.url).toBe("https://api.z.ai/api/monitor/usage/quota/limit"); }); From ef5377dcb720604b41db6196127d8ecf0cea7c44 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 13:12:40 +0900 Subject: [PATCH 3/3] fix(quota): distinguish an authoritative-empty GLM response from a probe failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding on 1f33ddc94: after the TIME_LIMIT exclusion, a SUCCESSFUL limits[] response carrying only MCP rows returned null, which fetchProviderQuotaReports treats as a transient probe failure and answers by preserving the last-good report for up to 30 minutes. The dashboard and quota-aware routing therefore kept using stale model-token windows even though the new authoritative response said there are none. Adds an AUTHORITATIVE_EMPTY_QUOTA sentinel beside the existing TERMINAL_QUOTA_FAILURE and routes it through the same suppression path, so the provider's last-good row is dropped. The distinction is the point: null means 'this probe told us nothing, keep the old row'; the new sentinel means 'the provider answered, and the answer is no model windows'. Malformed, transport, and 5xx outcomes stay on the transient-preservation path, and the legacy flattened Z.AI parser keeps returning null when it cannot read a payload. Regression runs two sequential forced refreshes — token windows, then MCP-only — and asserts the cached report is cleared rather than preserved. --- src/providers/quota.ts | 45 ++++++++++++++++++++++++++++++------ tests/provider-quota.test.ts | 30 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 19009bb298..3335679e63 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -104,7 +104,23 @@ export function setProviderQuotaBeforePublishForTests( providerQuotaBeforePublishForTests = hook; } const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure"); -type ProviderQuotaProbeResult = ProviderQuotaReport | null | typeof TERMINAL_QUOTA_FAILURE; +/** + * The probe succeeded and the upstream authoritatively reported NO model-quota windows. + * + * Distinct from `null`, which means "this probe told us nothing" and deliberately preserves + * the last-good row for up to 30 minutes. Collapsing the two would let a stale report outlive + * the authoritative answer that replaced it: a GLM plan whose payload carries only MCP + * `TIME_LIMIT` rows has no model windows, and the dashboard and quota-aware routing must stop + * showing the previous token windows rather than keep them for another half hour. + * + * Suppression is shared with `TERMINAL_QUOTA_FAILURE`; only the reason differs. + */ +const AUTHORITATIVE_EMPTY_QUOTA = Symbol("authoritative-empty-quota"); +type ProviderQuotaProbeResult = + | ProviderQuotaReport + | null + | typeof TERMINAL_QUOTA_FAILURE + | typeof AUTHORITATIVE_EMPTY_QUOTA; export interface ProviderQuotaReport { provider: string; @@ -754,10 +770,16 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi const body = asRecord(await readQuotaJson(response)); if (!body || body.success === false) return null; const data = asRecord(body.data) ?? body; - const quota = Array.isArray(data?.limits) - ? parseZaiQuotaLimits(data) - : parseZaiQuotaLegacyFields(data); - return quota ? report(provider, "zai:quota-limit", quota) : null; + if (Array.isArray(data?.limits)) { + const quota = parseZaiQuotaLimits(data); + // A well-formed `limits[]` we fully understood is authoritative even when it yields no + // model window — for example a plan reporting only the monthly MCP `TIME_LIMIT` row. + // Returning `null` here would preserve the previous token windows for up to 30 minutes + // and keep quota-aware routing acting on a report the provider has already superseded. + return quota ? report(provider, "zai:quota-limit", quota) : AUTHORITATIVE_EMPTY_QUOTA; + } + const legacy = parseZaiQuotaLegacyFields(data); + return legacy ? report(provider, "zai:quota-limit", legacy) : null; } /** @@ -2322,9 +2344,18 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot) )), ); - const fresh = probeResults.filter((item): item is ProviderQuotaReport => item !== null && item !== TERMINAL_QUOTA_FAILURE); + const fresh = probeResults.filter((item): item is ProviderQuotaReport => ( + item !== null && item !== TERMINAL_QUOTA_FAILURE && item !== AUTHORITATIVE_EMPTY_QUOTA + )); + // Both sentinels suppress the previous row. A terminal failure means the response was + // invalid; an authoritative empty means the response was valid and said there are no + // model windows. Either way the old row is no longer true, which is what separates them + // from `null` (told us nothing — keep the last-good row). const terminalFailures = new Set( - Object.keys(config.providers).filter((_, index) => probeResults[index] === TERMINAL_QUOTA_FAILURE), + Object.keys(config.providers).filter((_, index) => ( + probeResults[index] === TERMINAL_QUOTA_FAILURE + || probeResults[index] === AUTHORITATIVE_EMPTY_QUOTA + )), ); await providerQuotaBeforePublishForTests?.(); let commitKey: string | null = null; diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 32ea4dab2b..ad3a6edbdd 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -1193,6 +1193,36 @@ describe("fetchProviderQuotaReports", () => { expect(result.reports[0]?.quota.monthlyResetAt).toBeUndefined(); }); + test("a later MCP-only refresh clears the cached Z.AI model windows", async () => { + // Sequential forced refreshes. The first returns real token windows, so a last-good row + // exists; the second is a SUCCESSFUL response that authoritatively reports no model + // windows. Treating that as a transient failure would preserve the stale token windows + // for up to 30 minutes, so the dashboard and quota-aware routing would keep acting on a + // report the provider has already superseded. + let call = 0; + globalThis.fetch = (async () => { + call += 1; + const limits = call === 1 + ? [ + { type: "TOKENS_LIMIT", unit: 3, number: 5, percentage: 40, nextResetTime: 1789000000000 }, + { type: "TOKENS_LIMIT", unit: 6, number: 1, percentage: 52, nextResetTime: 1789600000000 }, + ] + : [ + { type: "TIME_LIMIT", unit: 5, number: 1, usage: 100, currentValue: 20, remaining: 80, percentage: 20, nextResetTime: 1788921262994 }, + ]; + return new Response(JSON.stringify({ success: true, data: { limits, level: "lite" } }), { status: 200 }); + }) as typeof fetch; + + const cfg = keyQuotaConfig("zhipu-bigmodel-coding", "https://open.bigmodel.cn/api/coding/paas/v4", "zai-secret"); + + const first = await fetchProviderQuotaReports(cfg, true); + expect(first.reports).toHaveLength(1); + expect(first.reports[0]?.quota).toMatchObject({ fiveHourPercent: 40, weeklyPercent: 52 }); + + const second = await fetchProviderQuotaReports(cfg, true); + expect(second.reports).toEqual([]); + }); + test("Z.AI quota reports no model window when the payload carries only an MCP TIME_LIMIT row", async () => { // A BigModel V1 Lite plan whose MCP allowance is fully spent but whose model tokens // are untouched. Before issue #1168 this produced monthlyPercent: 100, and because