mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
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:
1
changelog.d/fixes/glm-credit-limit-quota.md
Normal file
1
changelog.d/fixes/glm-credit-limit-quota.md
Normal 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`
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user