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:
parent
17a4345004
commit
38464465da
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user