fix(glm): add dedicated coding transport (#2087)

Integrated into release/v3.8.0
This commit is contained in:
Raxxoor
2026-05-10 04:52:00 +01:00
committed by GitHub
parent 73fc6e3ca6
commit a8106bbadd
51 changed files with 2326 additions and 398 deletions

View File

@@ -187,14 +187,17 @@ export default function ApiManagerPageClient() {
for (const key of apiKeys) {
// Match analytics entry by unique API Key ID (isolates usage to this specific key instance)
const matches = byApiKey.filter((entry: any) => entry.apiKeyId === key.id);
const totalRequests = matches.reduce((sum: number, entry: any) => sum + (Number(entry.requests) || 0), 0);
const totalRequests = matches.reduce(
(sum: number, entry: any) => sum + (Number(entry.requests) || 0),
0
);
// Match call logs by unique ID as well for the lastUsed timestamp
const lastUsed =
(logs || []).find((log: any) => log.apiKeyId === key.id)?.timestamp || null;
(logs || []).find(
(log: any) => log.apiKeyId === key.id || (!log.apiKeyId && log.apiKeyName === key.name)
)?.timestamp || null;
(logs || []).find(
(log: any) => log.apiKeyId === key.id || (!log.apiKeyId && log.apiKeyName === key.name)
)?.timestamp || null;
stats[key.id] = {
totalRequests,
@@ -358,7 +361,7 @@ export default function ApiManagerPageClient() {
expiresAt: string | null,
maxSessions: number,
accessSchedule: AccessSchedule | null,
rateLimits: Array<{ limit: number; window: number }> | null
rateLimits: Array<{ limit: number; window: number }> | null,
scopes: string[]
) => {
if (!editingKey || !editingKey.id) return;
@@ -971,7 +974,7 @@ const PermissionsModal = memo(function PermissionsModal({
expiresAt: string | null,
maxSessions: number,
accessSchedule: AccessSchedule | null,
rateLimits: Array<{ limit: number; window: number }> | null
rateLimits: Array<{ limit: number; window: number }> | null,
scopes: string[]
) => void;
}) {
@@ -1141,7 +1144,7 @@ const PermissionsModal = memo(function PermissionsModal({
expiresAt || null,
maxSessions,
schedule,
rateLimits.length > 0 ? rateLimits : null
rateLimits.length > 0 ? rateLimits : null,
manageEnabled ? ["manage"] : []
);
}, [
@@ -1329,7 +1332,7 @@ const PermissionsModal = memo(function PermissionsModal({
value={String(rl.limit)}
onChange={(e) => {
const val = parseInt(e.target.value) || 0;
setRateLimits(prev => {
setRateLimits((prev) => {
const next = [...prev];
next[index].limit = val;
return next;
@@ -1344,7 +1347,7 @@ const PermissionsModal = memo(function PermissionsModal({
value={String(rl.window)}
onChange={(e) => {
const val = parseInt(e.target.value) || 0;
setRateLimits(prev => {
setRateLimits((prev) => {
const next = [...prev];
next[index].window = val;
return next;
@@ -1355,7 +1358,7 @@ const PermissionsModal = memo(function PermissionsModal({
<span className="text-sm text-text-muted shrink-0">sec</span>
<button
type="button"
onClick={() => setRateLimits(prev => prev.filter((_, i) => i !== index))}
onClick={() => setRateLimits((prev) => prev.filter((_, i) => i !== index))}
className="p-2 text-red-500 hover:bg-red-500/10 rounded transition-colors shrink-0"
title="Remove limit"
>
@@ -1522,6 +1525,24 @@ const PermissionsModal = memo(function PermissionsModal({
<p className="text-sm font-bold text-red-700 dark:text-red-400">Banned Status</p>
<p className="text-xs text-red-600 dark:text-red-300">
Immediately revoke all access. Used for suspected abuse or compromised keys.
</p>
</div>
<button
role="switch"
aria-checked={keyIsBanned}
onClick={() => setKeyIsBanned((prev) => !prev)}
className={`inline-flex shrink-0 items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-bold transition-colors ${
keyIsBanned
? "bg-red-500 text-white shadow-sm"
: "bg-black/5 dark:bg-white/5 text-text-muted hover:bg-black/10 dark:hover:bg-white/10"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{keyIsBanned ? "block" : "check_circle"}
</span>
{keyIsBanned ? "Banned" : "Active"}
</button>
</div>
{/* Management API Access Toggle */}
<div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-border bg-surface/40">
<div className="flex flex-col gap-1">
@@ -1551,7 +1572,9 @@ const PermissionsModal = memo(function PermissionsModal({
<div className="flex flex-col gap-2 p-3 rounded-lg border border-border bg-surface/40">
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-text-main">Expiration Date</p>
<p className="text-xs text-text-muted">Key will automatically stop working after this date.</p>
<p className="text-xs text-text-muted">
Key will automatically stop working after this date.
</p>
</div>
<input
type="datetime-local"
@@ -1563,7 +1586,16 @@ const PermissionsModal = memo(function PermissionsModal({
className="w-full px-2 py-1.5 text-sm border border-border rounded-md bg-background text-text-main"
/>
</div>
{/* Management Access */}
<div className="flex flex-col gap-2 p-3 rounded-lg border border-border bg-surface/40">
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-text-main">Management Access</p>
<p className="text-xs text-text-muted">
Allow this API key to manage OmniRoute configuration.
</p>
</div>
<button
role="switch"
aria-checked={manageEnabled}
onClick={() => setManageEnabled((prev) => !prev)}
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-semibold transition-colors ${

View File

@@ -3080,7 +3080,9 @@ export default function ProviderDetailPage() {
: undefined
}
onRefreshToken={
conn.authType === "oauth" ? () => handleRefreshToken(conn.id) : undefined
conn.authType === "oauth"
? () => handleRefreshToken(conn.id)
: undefined
}
isRefreshing={refreshingId === conn.id}
onApplyCodexAuthLocal={
@@ -3164,122 +3166,124 @@ export default function ProviderDetailPage() {
</div>
) : null}
<div className="flex flex-col gap-0">
{groupKeys.map((tag, gi) => {
const groupConns = groupMap.get(tag)!;
return (
<div
key={tag || "__untagged__"}
className={
gi > 0
? "border-t border-black/[0.06] dark:border-white/[0.06] mt-1 pt-1"
: ""
}
>
{tag && (
<div className="flex items-center gap-2 px-3 pt-2 pb-1">
<span className="material-symbols-outlined text-[13px] text-text-muted/50">
label
</span>
<span className="text-[11px] font-semibold uppercase tracking-widest text-text-muted/60 select-none">
{tag}
</span>
<div className="flex-1 h-px bg-black/[0.04] dark:bg-white/[0.04]" />
<span className="text-[10px] text-text-muted/40">
{groupConns.length}
</span>
{groupKeys.map((tag, gi) => {
const groupConns = groupMap.get(tag)!;
return (
<div
key={tag || "__untagged__"}
className={
gi > 0
? "border-t border-black/[0.06] dark:border-white/[0.06] mt-1 pt-1"
: ""
}
>
{tag && (
<div className="flex items-center gap-2 px-3 pt-2 pb-1">
<span className="material-symbols-outlined text-[13px] text-text-muted/50">
label
</span>
<span className="text-[11px] font-semibold uppercase tracking-widest text-text-muted/60 select-none">
{tag}
</span>
<div className="flex-1 h-px bg-black/[0.04] dark:bg-white/[0.04]" />
<span className="text-[10px] text-text-muted/40">
{groupConns.length}
</span>
</div>
)}
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
{groupConns.map((conn, index) => (
<ConnectionRow
key={conn.id}
connection={conn}
isOAuth={conn.authType === "oauth"}
isClaude={providerId === "claude"}
codexFastGlobalEnabled={codexGlobalFastServiceTier}
isFirst={gi === 0 && index === 0}
isLast={
gi === groupKeys.length - 1 && index === groupConns.length - 1
}
isSelected={selectedIds.has(conn.id)}
onToggleSelect={() => handleToggleSelectOne(conn.id)}
onMoveUp={() =>
handleSwapPriority(conn, sorted[sorted.indexOf(conn) - 1])
}
onMoveDown={() =>
handleSwapPriority(conn, sorted[sorted.indexOf(conn) + 1])
}
onToggleActive={(isActive) =>
handleUpdateConnectionStatus(conn.id, isActive)
}
onToggleRateLimit={(enabled) =>
handleToggleRateLimit(conn.id, enabled)
}
onToggleClaudeExtraUsage={(enabled) =>
handleToggleClaudeExtraUsage(conn.id, enabled)
}
isCodex={providerId === "codex"}
isCcCompatible={isCcCompatible}
cliproxyapiEnabled={cpaProviderEnabled}
onToggleCodex5h={(enabled) =>
handleToggleCodexLimit(conn.id, "use5h", enabled)
}
onToggleCodexWeekly={(enabled) =>
handleToggleCodexLimit(conn.id, "useWeekly", enabled)
}
onRetest={() => handleRetestConnection(conn.id)}
isRetesting={retestingId === conn.id}
onEdit={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
onDelete={() => handleDelete(conn.id)}
onReauth={
conn.authType === "oauth"
? () => setShowOAuthModal(true, conn)
: undefined
}
onRefreshToken={
conn.authType === "oauth"
? () => handleRefreshToken(conn.id)
: undefined
}
isRefreshing={refreshingId === conn.id}
onApplyCodexAuthLocal={
providerId === "codex"
? () => handleApplyCodexAuthLocal(conn.id)
: undefined
}
isApplyingCodexAuthLocal={applyingCodexAuthId === conn.id}
onExportCodexAuthFile={
providerId === "codex"
? () => handleExportCodexAuthFile(conn.id)
: undefined
}
isExportingCodexAuthFile={exportingCodexAuthId === conn.id}
onProxy={() =>
setProxyTarget({
level: "key",
id: conn.id,
label: pickDisplayValue(
[conn.name, conn.email],
emailsVisible,
conn.id
),
})
}
hasProxy={!!connProxyMap[conn.id]?.proxy}
proxySource={connProxyMap[conn.id]?.level || null}
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
/>
))}
</div>
)}
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
{groupConns.map((conn, index) => (
<ConnectionRow
key={conn.id}
connection={conn}
isOAuth={conn.authType === "oauth"}
isClaude={providerId === "claude"}
codexFastGlobalEnabled={codexGlobalFastServiceTier}
isFirst={gi === 0 && index === 0}
isLast={
gi === groupKeys.length - 1 && index === groupConns.length - 1
}
isSelected={selectedIds.has(conn.id)}
onToggleSelect={() => handleToggleSelectOne(conn.id)}
onMoveUp={() =>
handleSwapPriority(conn, sorted[sorted.indexOf(conn) - 1])
}
onMoveDown={() =>
handleSwapPriority(conn, sorted[sorted.indexOf(conn) + 1])
}
onToggleActive={(isActive) =>
handleUpdateConnectionStatus(conn.id, isActive)
}
onToggleRateLimit={(enabled) =>
handleToggleRateLimit(conn.id, enabled)
}
onToggleClaudeExtraUsage={(enabled) =>
handleToggleClaudeExtraUsage(conn.id, enabled)
}
isCodex={providerId === "codex"}
isCcCompatible={isCcCompatible}
cliproxyapiEnabled={cpaProviderEnabled}
onToggleCodex5h={(enabled) =>
handleToggleCodexLimit(conn.id, "use5h", enabled)
}
onToggleCodexWeekly={(enabled) =>
handleToggleCodexLimit(conn.id, "useWeekly", enabled)
}
onRetest={() => handleRetestConnection(conn.id)}
isRetesting={retestingId === conn.id}
onEdit={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
onDelete={() => handleDelete(conn.id)}
onReauth={
conn.authType === "oauth"
? () => setShowOAuthModal(true, conn)
: undefined
}
onRefreshToken={
conn.authType === "oauth"
? () => handleRefreshToken(conn.id)
: undefined
}
isRefreshing={refreshingId === conn.id}
onApplyCodexAuthLocal={
providerId === "codex"
? () => handleApplyCodexAuthLocal(conn.id)
: undefined
}
isApplyingCodexAuthLocal={applyingCodexAuthId === conn.id}
onExportCodexAuthFile={
providerId === "codex"
? () => handleExportCodexAuthFile(conn.id)
: undefined
}
isExportingCodexAuthFile={exportingCodexAuthId === conn.id}
onProxy={() =>
setProxyTarget({
level: "key",
id: conn.id,
label: pickDisplayValue(
[conn.name, conn.email],
emailsVisible,
conn.id
),
})
}
hasProxy={!!connProxyMap[conn.id]?.proxy}
proxySource={connProxyMap[conn.id]?.level || null}
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
/>
))}
</div>
</div>
</>
);
}
})()}
</div>
);
})}
</div>
</>
);
})()
)}
</Card>
)}
@@ -5668,6 +5672,10 @@ function getProviderBaseUrlPlaceholder(providerId?: string | null) {
}
}
function isGlmProvider(providerId?: string | null) {
return providerId === "glm" || providerId === "glm-cn" || providerId === "glmt";
}
function parseRoutingTagsInput(value: string): string[] | undefined {
const tags = Array.from(
new Set(
@@ -5723,7 +5731,7 @@ function AddApiKeyModal({
const defaultBaseUrl = getProviderBaseUrlDefault(provider);
const isVertex = provider === "vertex" || provider === "vertex-partner";
const defaultRegion = "us-central1";
const isGlm = provider === "glm" || provider === "glmt";
const isGlm = isGlmProvider(provider);
const isQoder = provider === "qoder";
const isCloudflare = provider === "cloudflare-ai";
const localProviderMetadata = getLocalProviderMetadata(provider);
@@ -6225,7 +6233,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const usesBaseUrl = isBaseUrlConfigurableProvider(connection?.provider);
const defaultBaseUrl = getProviderBaseUrlDefault(connection?.provider);
const isVertex = connection?.provider === "vertex" || connection?.provider === "vertex-partner";
const isGlm = connection?.provider === "glm" || connection?.provider === "glmt";
const isGlm = isGlmProvider(connection?.provider);
const isCloudflare = connection?.provider === "cloudflare-ai";
const isCodex = connection?.provider === "codex";
const isClaude = connection?.provider === "claude";

View File

@@ -11,6 +11,7 @@ import ProviderIcon from "@/shared/components/ProviderIcon";
const planVariants = {
free: "default",
lite: "primary",
pro: "primary",
ultra: "success",
enterprise: "info",

View File

@@ -64,6 +64,7 @@ const TIER_FILTERS = [
{ key: "ultra", labelKey: "tierUltra" },
{ key: "pro", labelKey: "tierPro" },
{ key: "plus", labelKey: "tierPlus" },
{ key: "lite", label: "Lite" },
{ key: "free", labelKey: "tierFree" },
{ key: "unknown", labelKey: "tierUnknown" },
];
@@ -349,6 +350,7 @@ export default function ProviderLimits() {
ultra: 0,
pro: 0,
plus: 0,
lite: 0,
free: 0,
unknown: 0,
};
@@ -520,7 +522,7 @@ export default function ProviderLimits() {
color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
}}
>
<span>{t(tier.labelKey)}</span>
<span>{tier.label || t(tier.labelKey)}</span>
<span className="opacity-85">{tierCounts[tier.key] || 0}</span>
</button>
);
@@ -632,8 +634,11 @@ export default function ProviderLimits() {
const remainingPercentage = Math.round(remainingPercentageRaw);
const colors = getBarColor(remainingPercentage);
const cd = formatCountdown(q.resetAt);
const shortName = formatQuotaLabel(q.name);
const shortName = q.displayName || formatQuotaLabel(q.name);
const staleAfterReset = q.staleAfterReset === true;
const details = Array.isArray(q.details)
? q.details.filter((detail) => detail && detail.used > 0)
: [];
return (
<div
@@ -674,6 +679,16 @@ export default function ProviderLimits() {
{shortName}
</span>
{details.length > 0 ? (
<span className="text-[10px] text-text-muted whitespace-nowrap">
{details
.map(
(detail) => `${formatQuotaLabel(detail.name)} ${detail.used}`
)
.join(" · ")}
</span>
) : null}
{/* Countdown */}
{staleAfterReset ? (
<span className="text-[10px] text-text-muted whitespace-nowrap">
@@ -796,7 +811,10 @@ export default function ProviderLimits() {
<div className="py-6 px-4 text-center text-text-muted text-[13px]">
{t("noAccountsForTierFilter")}{" "}
<strong>
{t(TIER_FILTERS.find((tier) => tier.key === tierFilter)?.labelKey || "tierUnknown")}
{(() => {
const tier = TIER_FILTERS.find((tier) => tier.key === tierFilter);
return tier?.label || t(tier?.labelKey || "tierUnknown");
})()}
</strong>
.
</div>

View File

@@ -22,11 +22,16 @@ const QUOTA_LABEL_MAP: Record<string, string> = {
agentic_request_freetrial: "Agentic (Trial)",
credits: "AI Credits",
models: "Models",
"5 Hours Quota": "5 Hours",
"Weekly Quota": "Weekly",
"Monthly Tools": "Monthly Tools",
tokens: "Tokens",
time_limit: "Time Limit",
mcp_monthly: "Monthly",
"search-prime": "Web Search",
"web-reader": "Web Reader",
zread: "Zread",
};
const GLM_QUOTA_ORDER: Record<string, number> = {
session: 0,
weekly: 1,
mcp_monthly: 2,
};
function toRecord(value: unknown): Record<string, unknown> {
@@ -202,9 +207,10 @@ export function parseQuotaData(provider, data) {
if (!data || typeof data !== "object") return [];
const normalizedQuotas = [];
const providerId = String(provider || "").toLowerCase();
try {
switch (provider.toLowerCase()) {
switch (providerId) {
case "github":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
@@ -216,6 +222,21 @@ export function parseQuotaData(provider, data) {
}
break;
case "glm":
case "glm-cn":
case "glmt":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(
normalizeQuotaEntry(name, quota, {
displayName: quota?.displayName,
details: Array.isArray(quota?.details) ? quota.details : undefined,
})
);
});
}
break;
case "antigravity":
if (data.quotas) {
Object.entries(data.quotas).forEach(([modelKey, quota]: [string, any]) => {
@@ -361,6 +382,14 @@ export function parseQuotaData(provider, data) {
});
}
if (providerId === "glm" || providerId === "glm-cn" || providerId === "glmt") {
normalizedQuotas.sort((a, b) => {
const orderA = GLM_QUOTA_ORDER[a.name] ?? 99;
const orderB = GLM_QUOTA_ORDER[b.name] ?? 99;
return orderA - orderB;
});
}
return normalizedQuotas;
}
@@ -392,7 +421,7 @@ export function resolvePlanValue(plan, providerSpecificData) {
/**
* Normalize provider-specific plan labels into a shared tier taxonomy.
* Supported tiers: enterprise, business, team, ultra, pro, plus, free, unknown.
* Supported tiers: enterprise, business, team, ultra, pro, plus, lite, free, unknown.
*/
export function normalizePlanTier(plan) {
const raw = typeof plan === "string" ? plan.trim() : "";
@@ -455,10 +484,18 @@ export function normalizePlanTier(plan) {
return { key: "ultra", label: "Ultra", variant: "success", rank: 4, raw };
}
if (upper.includes("MAX")) {
return { key: "ultra", label: "Max", variant: "success", rank: 4, raw };
}
if (upper.includes("PRO") || upper.includes("PREMIUM")) {
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
}
if (upper.includes("LITE") || upper.includes("LIGHT")) {
return { key: "lite", label: "Lite", variant: "primary", rank: 2, raw };
}
if (upper.includes("PLUS") || upper.includes("PAID")) {
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
}

View File

@@ -22,7 +22,10 @@ import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";
import { getAntigravityHeaders } from "@omniroute/open-sse/services/antigravityHeaders.ts";
import { getAntigravityModelsDiscoveryUrls } from "@omniroute/open-sse/config/antigravityUpstream.ts";
import { getGlmModelsUrl } from "@omniroute/open-sse/config/glmProvider.ts";
import {
buildGlmCodingHeaders,
buildGlmModelsUrl,
} from "@omniroute/open-sse/config/glmProvider.ts";
import { getImageProvider } from "@omniroute/open-sse/config/imageRegistry.ts";
import { getVideoProvider } from "@omniroute/open-sse/config/videoRegistry.ts";
import { resolveAntigravityVersion } from "@omniroute/open-sse/services/antigravityVersion.ts";
@@ -1551,40 +1554,73 @@ export async function GET(
}
}
if (provider === "glm" || provider === "glmt") {
if (provider === "glm" || provider === "glm-cn" || provider === "glmt") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const url = getGlmModelsUrl(connection.providerSpecificData);
const token = apiKey || accessToken;
const glmProviderSpecificData = {
...asRecord(connection.providerSpecificData),
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
};
const discoveredTargets = [
{
transport: "openai" as const,
url: buildGlmModelsUrl(glmProviderSpecificData, "openai"),
},
{
transport: "anthropic" as const,
url: buildGlmModelsUrl(glmProviderSpecificData, "anthropic"),
},
];
const discoveryTargets = discoveredTargets.filter(
(target, index, all) => all.findIndex((other) => other.url === target.url) === index
);
let response: Response;
let response: Response | null = null;
try {
response = await safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
for (const target of discoveryTargets) {
response = await safeOutboundFetch(target.url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
method: "GET",
headers:
target.transport === "openai"
? token
? buildGlmCodingHeaders(token, false)
: { "Content-Type": "application/json", Accept: "application/json" }
: {
"Content-Type": "application/json",
Accept: "application/json",
...(token ? { "x-api-key": token } : {}),
"anthropic-version": "2023-06-01",
},
});
if (response.ok) break;
if (response.status === 401 || response.status === 403) break;
}
} catch (error) {
const fallback = buildDiscoveryErrorFallbackResponse(error);
if (fallback) return fallback;
throw error;
}
if (!response.ok) {
if (!response?.ok) {
if (response?.status === 401 || response?.status === 403) {
return NextResponse.json(
{ error: `Failed to fetch models: ${response.status}` },
{ status: response.status }
);
}
const fallback = buildDiscoveryFallbackResponse();
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch models: ${response.status}` },
{ status: response.status }
{ error: `Failed to fetch models: ${response?.status || 502}` },
{ status: response?.status || 502 }
);
}