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

View File

@@ -269,8 +269,7 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements {
"SELECT id, name, machine_id, allowed_models, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, max_sessions, revoked_at, expires_at, ip_allowlist, scopes, rate_limits, is_banned, key_hash FROM api_keys WHERE key = ? OR key_hash = ?"
);
_stmtInsertKey = db.prepare(
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix, key_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix, scopes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix, key_hash, scopes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
_stmtDeleteKey = db.prepare("DELETE FROM api_keys WHERE id = ?");
}
@@ -459,7 +458,6 @@ async function hashKey(key: string): Promise<string> {
return createHash("sha256").update(key).digest("hex");
}
export async function createApiKey(name: string, machineId: string) {
export async function createApiKey(name: string, machineId: string, scopes: string[] = []) {
if (!machineId) {
throw new Error("machineId is required");
@@ -493,7 +491,7 @@ export async function createApiKey(name: string, machineId: string, scopes: stri
0,
apiKey.createdAt,
apiKey.key.slice(0, 12),
await hashKey(apiKey.key)
await hashKey(apiKey.key),
JSON.stringify(scopes)
);
setNoLog(apiKey.id, false);
@@ -584,8 +582,6 @@ export async function updateApiKeyPermissions(
rateLimits: update.rateLimits,
isBanned: update.isBanned,
expiresAt: update.expiresAt,
maxSessions: (update as { maxSessions?: number | null; expiresAt?: string | null })
.maxSessions,
maxSessions: (update as { maxSessions?: number | null }).maxSessions,
scopes: (update as { scopes?: string[] | null }).scopes,
};
@@ -603,7 +599,6 @@ export async function updateApiKeyPermissions(
normalized.rateLimits === undefined &&
normalized.isBanned === undefined &&
normalized.expiresAt === undefined &&
(normalized as Record<string, unknown>).maxSessions === undefined
(normalized as Record<string, unknown>).maxSessions === undefined &&
(normalized as Record<string, unknown>).scopes === undefined
) {
@@ -734,7 +729,9 @@ export async function updateApiKeyPermissions(
// Also invalidate Redis if key_hash is available
try {
const row = db.prepare("SELECT key_hash FROM api_keys WHERE id = ?").get(id) as { key_hash: string | null } | undefined;
const row = db.prepare("SELECT key_hash FROM api_keys WHERE id = ?").get(id) as
| { key_hash: string | null }
| undefined;
if (row?.key_hash) {
const { getRedisClient } = await import("@/shared/utils/rateLimiter");
const redis = getRedisClient();
@@ -938,7 +935,6 @@ export async function getApiKeyMetadata(
revokedAt: null,
expiresAt: null,
ipAllowlist: [],
scopes: [],
isBanned: false,
keyHash: null,
scopes: ["manage"],

View File

@@ -239,7 +239,11 @@ export async function resolveComboForModel(
const regex = globToRegex(row.pattern);
if (regex.test(modelStr)) {
try {
return JSON.parse(row.combo_data);
const combo = JSON.parse(row.combo_data) as Record<string, unknown>;
if (combo.isActive === false) {
continue;
}
return combo;
} catch {
// Corrupted combo data — skip
continue;

View File

@@ -259,7 +259,10 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
contextWindow,
maxInputTokens: synced?.limit_input ?? contextWindow,
maxOutputTokens:
synced?.limit_output ?? spec?.maxOutputTokens ?? MODEL_SPECS.__default__.maxOutputTokens,
synced?.limit_output ??
(typeof registryModel?.maxOutputTokens === "number" ? registryModel.maxOutputTokens : null) ??
spec?.maxOutputTokens ??
MODEL_SPECS.__default__.maxOutputTokens,
defaultThinkingBudget: spec?.defaultThinkingBudget ?? 0,
thinkingBudgetCap: spec?.thinkingBudgetCap ?? null,
thinkingOverhead: spec?.thinkingOverhead ?? null,

View File

@@ -46,7 +46,7 @@ interface ProviderConnectionLike {
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"glm",
"zai",
"glm-cn",
"glmt",
"minimax",
"minimax-cn",

View File

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

View File

@@ -1469,7 +1469,16 @@ export const updateKeyPermissionsSchema = z
expiresAt: z.string().datetime().nullable().optional(),
maxSessions: z.number().int().min(0).max(10000).optional(),
accessSchedule: z.union([accessScheduleSchema, z.null()]).optional(),
rateLimits: z.union([z.array(z.object({ limit: z.number().int().positive(), window: z.number().int().positive() })).max(50), z.null()]).optional(),
rateLimits: z
.union([
z
.array(
z.object({ limit: z.number().int().positive(), window: z.number().int().positive() })
)
.max(50),
z.null(),
])
.optional(),
scopes: z.array(z.string().trim().min(1).max(64)).max(16).optional(),
})
.superRefine((value, ctx) => {
@@ -1484,7 +1493,7 @@ export const updateKeyPermissionsSchema = z
value.expiresAt === undefined &&
value.maxSessions === undefined &&
value.accessSchedule === undefined &&
value.rateLimits === undefined
value.rateLimits === undefined &&
value.scopes === undefined
) {
ctx.addIssue({

View File

@@ -855,7 +855,7 @@ async function handleSingleModelChat(
return result.response;
}
if (result.errorType === "stream_readiness_timeout") {
if (result.errorType === "stream_timeout" || result.errorType === "stream_early_eof") {
// Stream readiness timeout is an upstream stall, not an account/quota failure.
// Do NOT mark the account as unavailable or trip the circuit breaker.
return result.response;

View File

@@ -1489,7 +1489,7 @@ export async function markAccountUnavailable(
model,
"forbidden",
status,
effectiveProviderProfile?.baseCooldownMs ?? COOLDOWN_MS.unavailable,
effectiveProviderProfile?.baseCooldownMs ?? COOLDOWN_MS.serviceUnavailable,
effectiveProviderProfile
);
updateProviderConnection(connectionId, {
@@ -1505,7 +1505,11 @@ export async function markAccountUnavailable(
return { shouldFallback: true, cooldownMs: lockout.cooldownMs };
}
const terminalStatus = resolveTerminalConnectionStatus(status, result, providerErrorType);
const terminalStatus = resolveTerminalConnectionStatus(
status,
result as { permanent?: boolean; creditsExhausted?: boolean },
providerErrorType
);
const cooldownMs = terminalStatus ? 0 : rawCooldownMs;
// ── 404 model-only lockout: connection stays active ──
@@ -1605,7 +1609,7 @@ export async function markAccountUnavailable(
// NOTE: For permanent bans we disable immediately — no threshold needed,
// because a permanent ban (403 "Verify your account" / ToS violation) will
// NEVER recover, so retrying is pointless regardless of attempt count.
if (result.permanent) {
if ((result as { permanent?: boolean }).permanent) {
try {
const settings = await getCachedSettings();
const autoDisableEnabled = settings.autoDisableBannedAccounts ?? false;