fix(usage): support CREDIT_LIMIT and multi-interval GLM quotas

GLM quota parsing only accepted TOKENS_LIMIT and wrote every limit to a
single "session" key, so credit-based plans showed nothing and later
intervals overwrote earlier ones. Accept CREDIT_LIMIT too and derive the
quota key from the limit unit (5h session, 7d weekly, tokens, custom).
Moves the parser into its own usage/glm.js, re-exported from misc.js.
This commit is contained in:
Daniel Gonçalves Araujo
2026-08-28 15:34:37 +07:00
parent 56a40765e9
commit fcfcced4ab
4 changed files with 283 additions and 62 deletions

View File

@@ -15,10 +15,10 @@ import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getKimiUsage } from "./usage/kimi.js";
import { getDeepseekUsage } from "./usage/deepseek.js";
import { resolveQoderCredentials } from "./qoderModels.js";
import { getGlmUsage } from "./usage/glm.js";
import {
getIflowUsage,
getOllamaUsage,
getGlmUsage,
getVercelAiGatewayUsage,
getQoderUsage,
} from "./usage/misc.js";

View File

@@ -0,0 +1,88 @@
/**
* GLM Coding Plan usage (international + China regions)
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U } from "./shared.js";
// GLM quota endpoints (region-aware) — url from registry transport.usage
const GLM_QUOTA_URLS = {
international: U("glm").url,
china: U("glm-cn").url,
};
/**
* GLM Coding Plan usage (international + China regions)
* Supports both TOKENS_LIMIT and CREDIT_LIMIT and dynamic intervals (e.g. session 5h, weekly 7d).
*/
export async function getGlmUsage(apiKey, provider, proxyOptions = null) {
if (!apiKey) {
return { message: "GLM API key not available." };
}
const region = provider === "glm-cn" ? "china" : "international";
const quotaUrl = GLM_QUOTA_URLS[region];
try {
const response = await proxyAwareFetch(
quotaUrl,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
},
proxyOptions,
);
if (!response.ok) {
if (response.status === 401) {
return { message: "GLM API key invalid or expired." };
}
return { message: `GLM quota API error (${response.status}).` };
}
const json = await response.json();
const data = json?.data && typeof json.data === "object" ? json.data : {};
const limits = Array.isArray(data.limits) ? data.limits : [];
const quotas = {};
for (const limit of limits) {
// 1. Accept both TOKENS_LIMIT and CREDIT_LIMIT from GLM API
if (!limit || (limit.type !== "TOKENS_LIMIT" && limit.type !== "CREDIT_LIMIT")) continue;
const usedPercent = Number(limit.percentage) || 0;
const resetMs = Number(limit.nextResetTime) || 0;
const remaining = Math.max(0, 100 - usedPercent);
// 2. Map key dynamically based on type and period (unit) to avoid overwriting
let key = "session";
if (limit.unit === 3) {
key = `Session (${limit.number}h)`;
} else if (limit.unit === 6) {
key = "Weekly (7d)";
} else if (limit.type === "TOKENS_LIMIT") {
key = "Tokens";
} else {
key = `Limit (${limit.number})`;
}
quotas[key] = {
used: usedPercent,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null,
unlimited: false,
};
}
const levelRaw = typeof data.level === "string" ? data.level : "";
const plan = levelRaw
? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase()
: "Unknown";
return { plan, quotas };
} catch (error) {
return { message: `GLM error: ${error.message}` };
}
}

View File

@@ -5,11 +5,8 @@
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U } from "./shared.js";
// GLM quota endpoints (region-aware) — url from registry transport.usage
const GLM_QUOTA_URLS = {
international: U("glm").url,
china: U("glm-cn").url,
};
export { getGlmUsage } from "./glm.js";
// Vercel AI Gateway credits endpoint
// Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings).
@@ -112,63 +109,7 @@ export async function getOllamaUsage(apiKey, providerSpecificData, proxyOptions
}
}
/**
* GLM Coding Plan usage (international + China regions)
*/
export async function getGlmUsage(apiKey, provider, proxyOptions = null) {
if (!apiKey) {
return { message: "GLM API key not available." };
}
const region = provider === "glm-cn" ? "china" : "international";
const quotaUrl = GLM_QUOTA_URLS[region];
try {
const response = await proxyAwareFetch(quotaUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
}, proxyOptions);
if (!response.ok) {
if (response.status === 401) {
return { message: "GLM API key invalid or expired." };
}
return { message: `GLM quota API error (${response.status}).` };
}
const json = await response.json();
const data = json?.data && typeof json.data === "object" ? json.data : {};
const limits = Array.isArray(data.limits) ? data.limits : [];
const quotas = {};
for (const limit of limits) {
if (!limit || limit.type !== "TOKENS_LIMIT") continue;
const usedPercent = Number(limit.percentage) || 0;
const resetMs = Number(limit.nextResetTime) || 0;
const remaining = Math.max(0, 100 - usedPercent);
quotas["session"] = {
used: usedPercent,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null,
unlimited: false,
};
}
const levelRaw = typeof data.level === "string" ? data.level : "";
const plan = levelRaw
? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase()
: "Unknown";
return { plan, quotas };
} catch (error) {
return { message: `GLM error: ${error.message}` };
}
}
/**
* Vercel AI Gateway usage — credit balance for the API key

View File

@@ -0,0 +1,192 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
import { getGlmUsage } from "../../open-sse/services/usage/glm.js";
import {
USAGE_SUPPORTED_PROVIDERS,
USAGE_APIKEY_PROVIDERS,
} from "../../src/shared/constants/providers.js";
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
const SAMPLE_GLM_CREDIT_USAGE = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "CREDIT_LIMIT",
unit: 3,
number: 5,
usage: 2000,
currentValue: 0,
remaining: 1999,
percentage: 25,
nextResetTime: 1787905548392,
},
{
type: "CREDIT_LIMIT",
unit: 6,
number: 1,
usage: 10000,
currentValue: 0,
remaining: 9999,
percentage: 10,
nextResetTime: 1788492142997,
},
],
level: "lite",
},
success: true,
};
const SAMPLE_GLM_TOKENS_USAGE = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 40,
nextResetTime: 1787905548392,
},
],
level: "standard",
},
success: true,
};
describe("glm registry usage flags", () => {
it("is listed for apikey quota dashboard", () => {
expect(USAGE_SUPPORTED_PROVIDERS).toContain("glm");
expect(USAGE_SUPPORTED_PROVIDERS).toContain("glm-cn");
expect(USAGE_APIKEY_PROVIDERS).toContain("glm");
expect(USAGE_APIKEY_PROVIDERS).toContain("glm-cn");
});
});
describe("getGlmUsage and getUsageForProvider(glm)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("handles CREDIT_LIMIT with session 5h and weekly 7d quotas", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(SAMPLE_GLM_CREDIT_USAGE));
const usage = await getUsageForProvider({
provider: "glm",
apiKey: "glm-key-123",
});
expect(usage.message).toBeUndefined();
expect(usage.plan).toBe("Lite");
expect(usage.quotas["Session (5h)"]).toEqual({
used: 25,
total: 100,
remaining: 75,
remainingPercentage: 75,
resetAt: new Date(1787905548392).toISOString(),
unlimited: false,
});
expect(usage.quotas["Weekly (7d)"]).toEqual({
used: 100 ? 10 : 10,
total: 100,
remaining: 90,
remainingPercentage: 90,
resetAt: new Date(1788492142997).toISOString(),
unlimited: false,
});
});
it("handles TOKENS_LIMIT quotas", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(SAMPLE_GLM_TOKENS_USAGE));
const usage = await getUsageForProvider({
provider: "glm-cn",
apiKey: "glm-cn-key",
});
expect(usage.message).toBeUndefined();
expect(usage.plan).toBe("Standard");
expect(usage.quotas["Tokens"]).toEqual({
used: 40,
total: 100,
remaining: 60,
remainingPercentage: 60,
resetAt: new Date(1787905548392).toISOString(),
unlimited: false,
});
});
it("handles fallback key for custom limit units", async () => {
proxyAwareFetch.mockResolvedValueOnce(
jsonResponse({
code: 200,
data: {
limits: [
{
type: "CREDIT_LIMIT",
unit: 99,
number: 12,
percentage: 5,
nextResetTime: 0,
},
],
level: "pro",
},
})
);
const usage = await getGlmUsage("glm-key", "glm");
expect(usage.plan).toBe("Pro");
expect(usage.quotas["Limit (12)"]).toEqual({
used: 5,
total: 100,
remaining: 95,
remainingPercentage: 95,
resetAt: null,
unlimited: false,
});
});
it("surfaces invalid key message on 401", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
const usage = await getUsageForProvider({
provider: "glm",
apiKey: "invalid-key",
});
expect(usage.message).toMatch(/invalid or expired/i);
});
it("handles non-200 error response", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "server error" }, 500));
const usage = await getUsageForProvider({
provider: "glm",
apiKey: "valid-key",
});
expect(usage.message).toMatch(/GLM quota API error \(500\)/);
});
it("returns message when apiKey is missing", async () => {
const usage = await getUsageForProvider({
provider: "glm",
apiKey: "",
});
expect(usage.message).toBe("GLM API key not available.");
});
});