feat: add support for Z.AI provider and enhance quota handling

This commit is contained in:
Jan Leon
2026-05-06 20:20:26 +00:00
parent ffde066951
commit 23aa213cef
7 changed files with 90 additions and 22 deletions

View File

@@ -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<string, unknown>) {
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<string,
for (const limit of limits) {
const src = toRecord(limit);
if (src.type !== "TOKENS_LIMIT") continue;
const label = getGlmQuotaLabel(src.type);
if (!label) continue;
const usedPercent = toNumber(src.percentage, 0);
const resetMs = toNumber(src.nextResetTime, 0);
const remaining = Math.max(0, 100 - usedPercent);
quotas["session"] = {
quotas[label] = {
used: usedPercent,
total: 100,
remaining,
@@ -708,6 +728,7 @@ export async function getUsageForProvider(connection, options: { forceRefresh?:
case "qoder":
return await getQoderUsage(accessToken);
case "glm":
case "zai":
case "glmt":
return await getGlmUsage(apiKey, providerSpecificData);
case "minimax":

View File

@@ -3,7 +3,6 @@
import { useTranslations } from "next-intl";
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import Image from "next/image";
import {
parseQuotaData,
calculatePercentage,
@@ -18,6 +17,7 @@ import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
import { pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import ProviderIcon from "@/shared/components/ProviderIcon";
const LS_GROUP_BY = "omniroute:limits:groupBy";
const LS_EXPANDED_GROUPS = "omniroute:limits:expandedGroups";
@@ -36,6 +36,7 @@ const PROVIDER_CONFIG = {
codex: { label: "OpenAI Codex", color: "#10A37F" },
claude: { label: "Claude Code", color: "#D97757" },
glm: { label: "GLM (Z.AI)", color: "#4A90D9" },
zai: { label: "Z.AI", color: "#2563EB" },
glmt: { label: "GLM Thinking", color: "#2563EB" },
"kimi-coding": { label: "Kimi Coding", color: "#1E3A8A" },
minimax: { label: "MiniMax", color: "#7C3AED" },
@@ -298,11 +299,12 @@ export default function ProviderLimits() {
claude: 5,
kiro: 6,
glm: 7,
glmt: 8,
"kimi-coding": 9,
minimax: 10,
"minimax-cn": 11,
nanogpt: 12,
zai: 8,
glmt: 9,
"kimi-coding": 10,
minimax: 11,
"minimax-cn": 12,
nanogpt: 13,
};
return [...filteredConnections].sort(
(a, b) => (priority[a.provider] || 9) - (priority[b.provider] || 9)
@@ -551,14 +553,7 @@ export default function ProviderLimits() {
{/* Account Info */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden shrink-0">
<Image
src={`/providers/${conn.provider}.png`}
alt={conn.provider}
width={32}
height={32}
className="object-contain"
sizes="32px"
/>
<ProviderIcon providerId={conn.provider} size={32} className="object-contain" />
</div>
<div className="min-w-0">
<div className="text-[13px] font-semibold text-text-main truncate">

View File

@@ -22,6 +22,8 @@ const QUOTA_LABEL_MAP: Record<string, string> = {
agentic_request_freetrial: "Agentic (Trial)",
credits: "AI Credits",
models: "Models",
tokens: "Tokens",
time_limit: "Time Limit",
};
function toRecord(value: unknown): Record<string, unknown> {

View File

@@ -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";

View File

@@ -1840,6 +1840,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"claude",
"kimi-coding",
"glm",
"zai",
"glmt",
"minimax",
"minimax-cn",

View File

@@ -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);
});

View File

@@ -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(