mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
feat: add support for Z.AI provider and enhance quota handling
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1840,6 +1840,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"claude",
|
||||
"kimi-coding",
|
||||
"glm",
|
||||
"zai",
|
||||
"glmt",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user