feat(providers): implement GLM usage fetching for all variants

- Refactor fetchZaiUsage to fetchGlmUsage supporting all 4 GLM variants
- Add URL routing for api.z.ai (zai, zai-coding) and bigmodel.cn (zhipu, zhipu-coding)
- Update provider-usage.load.ts to route all GLM providers to fetchGlmUsage
- Add focused tests for GLM-specific behaviors:
  - HTTP error handling
  - API error (non-200 code) handling
  - TIME_LIMIT window format (Monthly label)
  - Combined TOKENS_LIMIT + TIME_LIMIT response
- Keep deprecated fetchZaiUsage as compatibility wrapper

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Kyle Howells 2026-01-29 18:50:28 +00:00
parent 17a4345004
commit 38464465da
4 changed files with 197 additions and 13 deletions

View File

@ -4,4 +4,4 @@ export { fetchCodexUsage } from "./provider-usage.fetch.codex.js";
export { fetchCopilotUsage } from "./provider-usage.fetch.copilot.js";
export { fetchGeminiUsage } from "./provider-usage.fetch.gemini.js";
export { fetchMinimaxUsage } from "./provider-usage.fetch.minimax.js";
export { fetchZaiUsage } from "./provider-usage.fetch.zai.js";
export { fetchGlmUsage, fetchZaiUsage } from "./provider-usage.fetch.zai.js";

View File

@ -1,6 +1,10 @@
import { fetchJson } from "./provider-usage.fetch.shared.js";
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
import type {
ProviderUsageSnapshot,
UsageProviderId,
UsageWindow,
} from "./provider-usage.types.js";
type ZaiUsageResponse = {
success?: boolean;
@ -19,13 +23,24 @@ type ZaiUsageResponse = {
};
};
export async function fetchZaiUsage(
type GlmUsageProviderId = "zai" | "zai-coding" | "zhipu" | "zhipu-coding";
const GLM_USAGE_URLS: Record<GlmUsageProviderId, string> = {
zai: "https://api.z.ai/api/monitor/usage/quota/limit",
"zai-coding": "https://api.z.ai/api/monitor/usage/quota/limit",
zhipu: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
"zhipu-coding": "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
};
export async function fetchGlmUsage(
provider: GlmUsageProviderId,
apiKey: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
const url = GLM_USAGE_URLS[provider];
const res = await fetchJson(
"https://api.z.ai/api/monitor/usage/quota/limit",
url,
{
method: "GET",
headers: {
@ -39,8 +54,8 @@ export async function fetchZaiUsage(
if (!res.ok) {
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
provider: provider as UsageProviderId,
displayName: PROVIDER_LABELS[provider],
windows: [],
error: `HTTP ${res.status}`,
};
@ -49,8 +64,8 @@ export async function fetchZaiUsage(
const data = (await res.json()) as ZaiUsageResponse;
if (!data.success || data.code !== 200) {
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
provider: provider as UsageProviderId,
displayName: PROVIDER_LABELS[provider],
windows: [],
error: data.msg || "API error",
};
@ -84,9 +99,18 @@ export async function fetchZaiUsage(
const planName = data.data?.planName || data.data?.plan || undefined;
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
provider: provider as UsageProviderId,
displayName: PROVIDER_LABELS[provider],
windows,
plan: planName,
};
}
/** @deprecated Use fetchGlmUsage instead */
export async function fetchZaiUsage(
apiKey: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
return fetchGlmUsage("zai", apiKey, timeoutMs, fetchFn);
}

View File

@ -5,8 +5,8 @@ import {
fetchCodexUsage,
fetchCopilotUsage,
fetchGeminiUsage,
fetchGlmUsage,
fetchMinimaxUsage,
fetchZaiUsage,
} from "./provider-usage.fetch.js";
import {
DEFAULT_TIMEOUT_MS,
@ -67,7 +67,10 @@ export async function loadProviderUsageSummary(
case "minimax":
return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn);
case "zai":
return await fetchZaiUsage(auth.token, timeoutMs, fetchFn);
case "zai-coding":
case "zhipu":
case "zhipu-coding":
return await fetchGlmUsage(auth.provider, auth.token, timeoutMs, fetchFn);
default:
return {
provider: auth.provider,

View File

@ -137,7 +137,6 @@ describe("provider usage loading", () => {
expect(mockFetch).toHaveBeenCalled();
});
// TODO: Implement fetchZhipuUsage in provider-usage.load.ts
it("loads zhipu usage from bigmodel.cn endpoint", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
@ -180,6 +179,164 @@ describe("provider usage loading", () => {
expect(zhipu?.windows[0]?.usedPercent).toBe(30);
});
it("returns error snapshot when GLM API returns HTTP error", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<Parameters<typeof fetch>, ReturnType<typeof fetch>>(async (input) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("api.z.ai")) {
return makeResponse(500, "Internal Server Error");
}
return makeResponse(404, "not found");
});
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
auth: [{ provider: "zai", token: "token-1" }],
fetch: mockFetch,
});
const zai = summary.providers.find((p) => p.provider === "zai");
expect(zai?.error).toBe("HTTP 500");
expect(zai?.windows).toHaveLength(0);
});
it("returns error snapshot when GLM API returns non-200 code", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<Parameters<typeof fetch>, ReturnType<typeof fetch>>(async (input) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("api.z.ai")) {
return makeResponse(200, {
success: false,
code: 401,
msg: "Invalid API key",
});
}
return makeResponse(404, "not found");
});
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
auth: [{ provider: "zai", token: "invalid-token" }],
fetch: mockFetch,
});
const zai = summary.providers.find((p) => p.provider === "zai");
expect(zai?.error).toBe("Invalid API key");
expect(zai?.windows).toHaveLength(0);
});
it("handles GLM TIME_LIMIT as Monthly window", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<Parameters<typeof fetch>, ReturnType<typeof fetch>>(async (input) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("api.z.ai")) {
return makeResponse(200, {
success: true,
code: 200,
data: {
planName: "Enterprise",
limits: [
{
type: "TIME_LIMIT",
percentage: 45,
unit: 1,
number: 30,
nextResetTime: "2026-02-01T00:00:00Z",
},
],
},
});
}
return makeResponse(404, "not found");
});
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
auth: [{ provider: "zai", token: "token-1" }],
fetch: mockFetch,
});
const zai = summary.providers.find((p) => p.provider === "zai");
expect(zai?.plan).toBe("Enterprise");
expect(zai?.windows).toHaveLength(1);
expect(zai?.windows[0]?.label).toBe("Monthly");
expect(zai?.windows[0]?.usedPercent).toBe(45);
});
it("handles GLM response with both TOKENS_LIMIT and TIME_LIMIT", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<Parameters<typeof fetch>, ReturnType<typeof fetch>>(async (input) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("api.z.ai")) {
return makeResponse(200, {
success: true,
code: 200,
data: {
planName: "Pro",
limits: [
{
type: "TOKENS_LIMIT",
percentage: 25,
unit: 3,
number: 24,
nextResetTime: "2026-01-08T00:00:00Z",
},
{
type: "TIME_LIMIT",
percentage: 10,
unit: 1,
number: 30,
nextResetTime: "2026-02-01T00:00:00Z",
},
],
},
});
}
return makeResponse(404, "not found");
});
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
auth: [{ provider: "zai", token: "token-1" }],
fetch: mockFetch,
});
const zai = summary.providers.find((p) => p.provider === "zai");
expect(zai?.plan).toBe("Pro");
expect(zai?.windows).toHaveLength(2);
const tokensWindow = zai?.windows.find((w) => w.label.startsWith("Tokens"));
expect(tokensWindow?.label).toBe("Tokens (24h)");
expect(tokensWindow?.usedPercent).toBe(25);
const monthlyWindow = zai?.windows.find((w) => w.label === "Monthly");
expect(monthlyWindow?.usedPercent).toBe(10);
});
it("handles nested MiniMax usage payloads", async () => {
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);