From 877cfa00719fcf5923bc8bf80b11f37b62879276 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 28 Mar 2026 17:39:24 -0600 Subject: [PATCH] feat: add GLM Coding usage/quota tracking with Z.AI session quota (#698) * feat: add GLM Coding usage/quota tracking with Z.AI session quota Add GLM to the usage tracking pipeline: usage API route, Z.AI quota fetcher (TOKENS_LIMIT percentage-based), quota parser, and Provider Limits UI. Adds API region dropdown (International/China) to Add/Edit connection modals. Displays session quota with plan level. * fix: address PR review feedback for GLM usage tracking - Remove explicit `any` types from getGlmUsage (fix lint budget) - Fix empty string fallback for plan level - Remove duplicate `case "glm"` in quota parser (identical to default) - Skip OAuth refresh flow for GLM (API key auth) in usage route * fix: upgrade path-to-regexp to fix ReDoS vulnerability (GHSA-j3q9-mxjg-w52f, GHSA-27v5-c462-wpq7) --------- Co-authored-by: Chris Staley --- open-sse/services/usage.ts | 57 ++++++++++++++++++- package-lock.json | 12 ++-- .../dashboard/providers/[id]/page.tsx | 44 ++++++++++++++ .../usage/components/ProviderLimits/index.tsx | 2 +- .../usage/components/ProviderLimits/utils.tsx | 1 - src/app/api/usage/[connectionId]/route.ts | 16 +++++- src/shared/constants/providers.ts | 1 + 7 files changed, 122 insertions(+), 11 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index f9d502e8d0..c1f1114a2e 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -100,13 +100,66 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota return quota.total > 0 || quota.remainingPercentage !== undefined; } +// GLM (Z.AI) quota API config +const GLM_QUOTA_URLS: Record = { + international: "https://api.z.ai/api/monitor/usage/quota/limit", + china: "https://open.bigmodel.cn/api/monitor/usage/quota/limit", +}; + +async function getGlmUsage(apiKey: string, providerSpecificData?: Record) { + const region = providerSpecificData?.apiRegion || "international"; + const quotaUrl = GLM_QUOTA_URLS[region] || GLM_QUOTA_URLS.international; + + const res = await fetch(quotaUrl, { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + }, + }); + + if (!res.ok) { + if (res.status === 401) throw new Error("Invalid API key"); + throw new Error(`GLM quota API error (${res.status})`); + } + + const json = await res.json(); + const data = toRecord(json.data); + const limits: unknown[] = Array.isArray(data.limits) ? data.limits : []; + const quotas: Record = {}; + + for (const limit of limits) { + const src = toRecord(limit); + if (src.type !== "TOKENS_LIMIT") continue; + + const usedPercent = toNumber(src.percentage, 0); + const resetMs = toNumber(src.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 }; +} + /** * Get usage data for a provider connection * @param {Object} connection - Provider connection with accessToken * @returns {Promise} Usage data with quotas */ export async function getUsageForProvider(connection) { - const { provider, accessToken, providerSpecificData } = connection; + const { provider, accessToken, apiKey, providerSpecificData } = connection; switch (provider) { case "github": @@ -127,6 +180,8 @@ export async function getUsageForProvider(connection) { return await getQwenUsage(accessToken, providerSpecificData); case "iflow": return await getIflowUsage(accessToken); + case "glm": + return await getGlmUsage(apiKey, providerSpecificData); default: return { message: `Usage API not implemented for ${provider}` }; } diff --git a/package-lock.json b/package-lock.json index 0e90a983ba..792dcfc98f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6346,9 +6346,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7574,9 +7574,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 0e63a5f6af..4844e1d31c 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -4289,6 +4289,7 @@ function AddApiKeyModal({ const defaultBailianUrl = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1"; const isVertex = provider === "vertex"; const defaultRegion = "us-central1"; + const isGlm = provider === "glm"; const [formData, setFormData] = useState({ name: "", @@ -4296,6 +4297,7 @@ function AddApiKeyModal({ priority: 1, baseUrl: isBailian ? defaultBailianUrl : "", region: isVertex ? defaultRegion : "", + apiRegion: "international", validationModelId: "", }); const [validating, setValidating] = useState(false); @@ -4385,6 +4387,10 @@ function AddApiKeyModal({ payload.providerSpecificData = { region: formData.region, }; + } else if (isGlm) { + payload.providerSpecificData = { + apiRegion: formData.apiRegion, + }; } const error = await onSave(payload); @@ -4484,6 +4490,22 @@ function AddApiKeyModal({ hint="ex: us-central1 ou europe-west4. Partner models usam a região global automaticamente." /> )} + {isGlm && ( +
+ + +

+ Select the endpoint region for API access and quota tracking. +

+
+ )}