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 { fetchCopilotUsage } from "./provider-usage.fetch.copilot.js";
|
||||||
export { fetchGeminiUsage } from "./provider-usage.fetch.gemini.js";
|
export { fetchGeminiUsage } from "./provider-usage.fetch.gemini.js";
|
||||||
export { fetchMinimaxUsage } from "./provider-usage.fetch.minimax.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 { fetchJson } from "./provider-usage.fetch.shared.js";
|
||||||
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.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 = {
|
type ZaiUsageResponse = {
|
||||||
success?: boolean;
|
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,
|
apiKey: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
fetchFn: typeof fetch,
|
fetchFn: typeof fetch,
|
||||||
): Promise<ProviderUsageSnapshot> {
|
): Promise<ProviderUsageSnapshot> {
|
||||||
|
const url = GLM_USAGE_URLS[provider];
|
||||||
const res = await fetchJson(
|
const res = await fetchJson(
|
||||||
"https://api.z.ai/api/monitor/usage/quota/limit",
|
url,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@ -39,8 +54,8 @@ export async function fetchZaiUsage(
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return {
|
return {
|
||||||
provider: "zai",
|
provider: provider as UsageProviderId,
|
||||||
displayName: PROVIDER_LABELS.zai,
|
displayName: PROVIDER_LABELS[provider],
|
||||||
windows: [],
|
windows: [],
|
||||||
error: `HTTP ${res.status}`,
|
error: `HTTP ${res.status}`,
|
||||||
};
|
};
|
||||||
@ -49,8 +64,8 @@ export async function fetchZaiUsage(
|
|||||||
const data = (await res.json()) as ZaiUsageResponse;
|
const data = (await res.json()) as ZaiUsageResponse;
|
||||||
if (!data.success || data.code !== 200) {
|
if (!data.success || data.code !== 200) {
|
||||||
return {
|
return {
|
||||||
provider: "zai",
|
provider: provider as UsageProviderId,
|
||||||
displayName: PROVIDER_LABELS.zai,
|
displayName: PROVIDER_LABELS[provider],
|
||||||
windows: [],
|
windows: [],
|
||||||
error: data.msg || "API error",
|
error: data.msg || "API error",
|
||||||
};
|
};
|
||||||
@ -84,9 +99,18 @@ export async function fetchZaiUsage(
|
|||||||
|
|
||||||
const planName = data.data?.planName || data.data?.plan || undefined;
|
const planName = data.data?.planName || data.data?.plan || undefined;
|
||||||
return {
|
return {
|
||||||
provider: "zai",
|
provider: provider as UsageProviderId,
|
||||||
displayName: PROVIDER_LABELS.zai,
|
displayName: PROVIDER_LABELS[provider],
|
||||||
windows,
|
windows,
|
||||||
plan: planName,
|
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,
|
fetchCodexUsage,
|
||||||
fetchCopilotUsage,
|
fetchCopilotUsage,
|
||||||
fetchGeminiUsage,
|
fetchGeminiUsage,
|
||||||
|
fetchGlmUsage,
|
||||||
fetchMinimaxUsage,
|
fetchMinimaxUsage,
|
||||||
fetchZaiUsage,
|
|
||||||
} from "./provider-usage.fetch.js";
|
} from "./provider-usage.fetch.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_TIMEOUT_MS,
|
DEFAULT_TIMEOUT_MS,
|
||||||
@ -67,7 +67,10 @@ export async function loadProviderUsageSummary(
|
|||||||
case "minimax":
|
case "minimax":
|
||||||
return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn);
|
return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn);
|
||||||
case "zai":
|
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:
|
default:
|
||||||
return {
|
return {
|
||||||
provider: auth.provider,
|
provider: auth.provider,
|
||||||
|
|||||||
@ -137,7 +137,6 @@ describe("provider usage loading", () => {
|
|||||||
expect(mockFetch).toHaveBeenCalled();
|
expect(mockFetch).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement fetchZhipuUsage in provider-usage.load.ts
|
|
||||||
it("loads zhipu usage from bigmodel.cn endpoint", async () => {
|
it("loads zhipu usage from bigmodel.cn endpoint", async () => {
|
||||||
const makeResponse = (status: number, body: unknown): Response => {
|
const makeResponse = (status: number, body: unknown): Response => {
|
||||||
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
||||||
@ -180,6 +179,164 @@ describe("provider usage loading", () => {
|
|||||||
expect(zhipu?.windows[0]?.usedPercent).toBe(30);
|
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 () => {
|
it("handles nested MiniMax usage payloads", async () => {
|
||||||
const makeResponse = (status: number, body: unknown): Response => {
|
const makeResponse = (status: number, body: unknown): Response => {
|
||||||
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user