fix(usage): parse CREDIT_LIMIT rows from z.ai coding-plan quota API (#11378)

Merged via consolidated batch validation. Z.ai's quota API now returns CREDIT_LIMIT rows for GLM Coding Plan subscription keys instead of TOKENS_LIMIT, breaking the dashboard quota card. Own test passes.
This commit is contained in:
Mr White
2026-08-24 23:13:19 +08:00
committed by GitHub
parent 9f30b76057
commit 20de0d9c79
3 changed files with 113 additions and 4 deletions

View File

@@ -0,0 +1 @@
- **fix(usage):** z.ai/GLM coding-plan subscription keys now render their quota cards again, with absolute credits. Z.ai's `/api/monitor/usage/quota/limit` switched these keys from `TOKENS_LIMIT` to `CREDIT_LIMIT` rows (same `unit`/`number` semantics: unit=3/number=5 → 5-hour window, unit=6/number=1 → weekly), and the parser only matched `TOKENS_LIMIT`/`TIME_LIMIT`, so both rows were dropped and the subscription card rendered empty. `CREDIT_LIMIT` is now accepted alongside `TOKENS_LIMIT`, and when the row carries absolute credit fields (`usage`/`currentValue`/`remaining`) they are preferred over the percent-only scale, so the card shows `3341 / 28000` like z.ai's own dashboard instead of `11 / 100`

View File

@@ -155,15 +155,30 @@ export async function getGlmUsage(apiKey: string, providerSpecificData?: Record<
const resetMs = toNumber(src.nextResetTime, 0);
const resetAt = resetMs > 0 ? new Date(resetMs).toISOString() : null;
if (type === "TOKENS_LIMIT") {
// Z.ai coding-plan keys (CREDIT-based, e.g. GLM Coding Max/Lite) report
// CREDIT_LIMIT rows with the same unit/number semantics as TOKENS_LIMIT
// (unit=3/number=5 → 5-hour window, unit=6/number=1 → weekly). Without
// this branch every CREDIT_LIMIT row is dropped and the quota card
// renders empty for subscription keys.
if (type === "TOKENS_LIMIT" || type === "CREDIT_LIMIT") {
const quotaName = getGlmTokenQuotaName(src, quotas);
const usedPercent = toPercentage(src.percentage);
const remaining = Math.max(0, 100 - usedPercent);
// CREDIT_LIMIT rows (z.ai coding-plan keys) carry absolute credits on
// top of the percentage: usage = window total, currentValue = consumed,
// remaining = credits left. Prefer them so the quota card renders
// "3341 / 28000" like z.ai's own dashboard instead of a percent-only
// scale. TOKENS_LIMIT rows without absolute fields keep the percent path.
const totalCredits = toNumber(src.usage, 0);
const usedCredits = totalCredits > 0 ? toNumber(src.currentValue, usedPercent) : usedPercent;
const remainingCredits = totalCredits > 0 ? toNumber(src.remaining, remaining) : remaining;
const total = totalCredits > 0 ? totalCredits : 100;
quotas[quotaName] = {
used: usedPercent,
total: 100,
remaining,
used: usedCredits,
total,
remaining: remainingCredits,
remainingPercentage: remaining,
resetAt,
displayName: getGlmQuotaDisplayName(quotaName),

View File

@@ -317,3 +317,96 @@ describe("getGlmUsage team quota parsing", () => {
}
});
});
describe("getGlmUsage CREDIT_LIMIT (coding-plan subscription keys)", () => {
// Real-world response from https://api.z.ai/api/monitor/usage/quota/limit
// for a GLM Coding Max subscription key (2026-08): limits use CREDIT_LIMIT
// instead of TOKENS_LIMIT, with identical unit/number semantics plus
// absolute credit fields (usage/currentValue/remaining).
const CREDIT_LIMIT_RESPONSE = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "CREDIT_LIMIT",
unit: 3,
number: 5,
usage: 28000,
currentValue: 3341,
remaining: 24658,
percentage: 11,
nextResetTime: 1787563232239,
},
{
type: "CREDIT_LIMIT",
unit: 6,
number: 1,
usage: 140000,
currentValue: 25224,
remaining: 114775,
percentage: 18,
nextResetTime: 1788077327998,
},
],
level: "max",
},
success: true,
};
it("maps CREDIT_LIMIT rows to session/weekly quotas with absolute credits", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify(CREDIT_LIMIT_RESPONSE), { status: 200 });
try {
const usage = await getGlmUsage("zai-subscription-key");
assert.equal(usage.plan, "Max");
assert.ok(usage.quotas.session, "5-hour window quota should render");
assert.ok(usage.quotas.weekly, "weekly quota should render");
// Absolute credits — matches z.ai's own dashboard ("4.1K / 140K" style).
assert.equal(usage.quotas.session.used, 3341);
assert.equal(usage.quotas.session.total, 28000);
assert.equal(usage.quotas.session.remaining, 24658);
assert.equal(usage.quotas.weekly.used, 25224);
assert.equal(usage.quotas.weekly.total, 140000);
assert.equal(usage.quotas.weekly.remaining, 114775);
// Percentages stay derived from the upstream percentage field.
assert.equal(usage.quotas.session.remainingPercentage, 89);
assert.equal(usage.quotas.weekly.remainingPercentage, 82);
assert.equal(usage.quotas.session.displayName, "5 Hours Quota");
assert.equal(usage.quotas.weekly.displayName, "Weekly Quota");
assert.equal(usage.quotas.session.resetAt, new Date(1787563232239).toISOString());
} finally {
globalThis.fetch = originalFetch;
}
});
it("falls back to the percent scale when a CREDIT_LIMIT row has no absolute fields", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
code: 200,
success: true,
data: {
limits: [{ type: "CREDIT_LIMIT", unit: 3, number: 5, percentage: 40 }],
level: "lite",
},
}),
{ status: 200 }
);
try {
const usage = await getGlmUsage("zai-key");
assert.equal(usage.quotas.session.used, 40);
assert.equal(usage.quotas.session.total, 100);
assert.equal(usage.quotas.session.remaining, 60);
assert.equal(usage.quotas.session.remainingPercentage, 60);
} finally {
globalThis.fetch = originalFetch;
}
});
});