From 23aa213cefa072916e7ced7646f086e27efe85a3 Mon Sep 17 00:00:00 2001 From: Jan Leon Date: Wed, 6 May 2026 20:20:26 +0000 Subject: [PATCH] feat: add support for Z.AI provider and enhance quota handling --- open-sse/services/usage.ts | 25 ++++++++++++++++-- .../usage/components/ProviderLimits/index.tsx | 23 +++++++--------- .../usage/components/ProviderLimits/utils.tsx | 2 ++ src/lib/usage/providerLimits.ts | 10 ++++++- src/shared/constants/providers.ts | 1 + tests/unit/provider-limits-ui.test.ts | 26 +++++++++++++++++++ tests/unit/usage-service-hardening.test.ts | 25 ++++++++++++++---- 7 files changed, 90 insertions(+), 22 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 2bf2edec5f..a7f01440f1 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -515,7 +515,26 @@ async function getCrofUsage(apiKey: string) { return { quotas }; } +function getGlmQuotaLabel(type: unknown): string | null { + const normalized = typeof type === "string" ? type.trim().toUpperCase() : ""; + + switch (normalized) { + case "TOKENS_LIMIT": + case "TOKEN_LIMIT": + return "tokens"; + case "TIME_LIMIT": + case "TIME_USAGE_LIMIT": + return "time_limit"; + default: + return null; + } +} + async function getGlmUsage(apiKey: string, providerSpecificData?: Record) { + if (!apiKey) { + return { message: "Z.AI API key not available. Add a coding plan API key to view usage." }; + } + const quotaUrl = getGlmQuotaUrl(providerSpecificData); const res = await fetch(quotaUrl, { @@ -537,13 +556,14 @@ async function getGlmUsage(apiKey: string, providerSpecificData?: Record (priority[a.provider] || 9) - (priority[b.provider] || 9) @@ -551,14 +553,7 @@ export default function ProviderLimits() { {/* Account Info */}
- {conn.provider} +
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index ddce524a9f..afd2ab1715 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -22,6 +22,8 @@ const QUOTA_LABEL_MAP: Record = { agentic_request_freetrial: "Agentic (Trial)", credits: "AI Credits", models: "Models", + tokens: "Tokens", + time_limit: "Time Limit", }; function toRecord(value: unknown): Record { diff --git a/src/lib/usage/providerLimits.ts b/src/lib/usage/providerLimits.ts index 5855c04bab..80c3b31e17 100644 --- a/src/lib/usage/providerLimits.ts +++ b/src/lib/usage/providerLimits.ts @@ -43,7 +43,15 @@ interface ProviderConnectionLike { backoffLevel?: number; } -const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set(["glm", "glmt", "minimax", "minimax-cn", "crof", "nanogpt"]); +const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([ + "glm", + "zai", + "glmt", + "minimax", + "minimax-cn", + "crof", + "nanogpt", +]); const DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES = 70; const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_run"; diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 69b1f43091..ee49035bef 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1840,6 +1840,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [ "claude", "kimi-coding", "glm", + "zai", "glmt", "minimax", "minimax-cn", diff --git a/tests/unit/provider-limits-ui.test.ts b/tests/unit/provider-limits-ui.test.ts index ae40f8b868..48c365d40a 100644 --- a/tests/unit/provider-limits-ui.test.ts +++ b/tests/unit/provider-limits-ui.test.ts @@ -56,6 +56,7 @@ test("quota labels normalize session and weekly windows while preserving readabl }); test("MiniMax providers are exposed to the limits dashboard support list", () => { + assert.ok(providerConstants.USAGE_SUPPORTED_PROVIDERS.includes("zai")); assert.ok(providerConstants.USAGE_SUPPORTED_PROVIDERS.includes("minimax")); assert.ok(providerConstants.USAGE_SUPPORTED_PROVIDERS.includes("minimax-cn")); }); @@ -93,3 +94,28 @@ test("MiniMax quota payloads use generic provider parsing and stale resets still assert.equal(providerLimitUtils.formatQuotaLabel(parsed[0].name), "Session"); assert.equal(providerLimitUtils.formatQuotaLabel(parsed[1].name), "Weekly"); }); + +test("Z.AI quota labels render token and time limit usage", () => { + assert.equal(providerLimitUtils.formatQuotaLabel("tokens"), "Tokens"); + assert.equal(providerLimitUtils.formatQuotaLabel("time_limit"), "Time Limit"); + + const future = new Date(Date.now() + 5 * 60_000).toISOString(); + const parsed = providerLimitUtils.parseQuotaData("zai", { + quotas: { + tokens: { used: 18, total: 100, remaining: 82, remainingPercentage: 82, resetAt: future }, + time_limit: { + used: 0, + total: 100, + remaining: 100, + remainingPercentage: 100, + resetAt: future, + }, + }, + }); + + assert.equal(parsed.length, 2); + assert.equal(parsed[0].name, "tokens"); + assert.equal(parsed[0].remainingPercentage, 82); + assert.equal(parsed[1].name, "time_limit"); + assert.equal(parsed[1].remainingPercentage, 100); +}); diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 8642c6564b..442dc83f0e 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -862,7 +862,7 @@ test("usage service covers Codex auth failures, Kiro hard failures, Kimi no-quot assert.equal(qwenCatch.message, "Unable to fetch Qwen usage."); }); -test("usage service covers Qwen, Qoder, GLM and GLMT branches", async () => { +test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () => { const qwenMissingUrl: any = await usageService.getUsageForProvider({ provider: "qwen", accessToken: "qwen-token", @@ -896,6 +896,11 @@ test("usage service covers Qwen, Qoder, GLM and GLMT branches", async () => { percentage: "64", nextResetTime: Date.now() + 120_000, }, + { + type: "TIME_LIMIT", + percentage: "7", + nextResetTime: Date.now() + 300_000, + }, { type: "OTHER_LIMIT", percentage: "10", @@ -916,8 +921,18 @@ test("usage service covers Qwen, Qoder, GLM and GLMT branches", async () => { providerSpecificData: { apiRegion: "invalid-region" }, }); assert.equal(glm.plan, "Pro"); - assert.equal(glm.quotas.session.used, 64); - assert.equal(glm.quotas.session.remaining, 36); + assert.equal(glm.quotas.tokens.used, 64); + assert.equal(glm.quotas.tokens.remaining, 36); + assert.equal(glm.quotas.time_limit.used, 7); + assert.equal(glm.quotas.time_limit.remaining, 93); + + const zai: any = await usageService.getUsageForProvider({ + provider: "zai", + apiKey: "glm-key", + }); + assert.equal(zai.plan, "Pro"); + assert.equal(zai.quotas.tokens.used, 64); + assert.equal(zai.quotas.time_limit.remaining, 93); const glmt: any = await usageService.getUsageForProvider({ provider: "glmt", @@ -925,8 +940,8 @@ test("usage service covers Qwen, Qoder, GLM and GLMT branches", async () => { providerSpecificData: { apiRegion: "international" }, }); assert.equal(glmt.plan, "Pro"); - assert.equal(glmt.quotas.session.used, 64); - assert.equal(glmt.quotas.session.remaining, 36); + assert.equal(glmt.quotas.tokens.used, 64); + assert.equal(glmt.quotas.tokens.remaining, 36); globalThis.fetch = async () => new Response("nope", { status: 401 }); await assert.rejects(