mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(glm): add dedicated coding transport (#2087)
Integrated into release/v3.8.0
This commit is contained in:
@@ -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 ${
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -11,6 +11,7 @@ import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
const planVariants = {
|
||||
free: "default",
|
||||
lite: "primary",
|
||||
pro: "primary",
|
||||
ultra: "success",
|
||||
enterprise: "info",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user