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

@@ -460,15 +460,15 @@ QODER_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT=claude-cli/2.1.121 (external, cli)
CODEX_USER_AGENT=codex-cli/0.125.0 (Windows 10.0.26100; x64)
GITHUB_USER_AGENT=GitHubCopilotChat/0.45.1
ANTIGRAVITY_USER_AGENT=antigravity/1.107.0 darwin/arm64
KIRO_USER_AGENT=AWS-SDK-JS/3.0.0 kiro-ide/1.0.0
QODER_USER_AGENT=Qoder-Cli
QWEN_USER_AGENT=QwenCode/0.15.3 (linux; x64)
CURSOR_USER_AGENT=connect-es/1.6.1
GEMINI_CLI_USER_AGENT=google-api-nodejs-client/10.3.0
CLAUDE_USER_AGENT="claude-cli/2.1.137 (external, cli)"
CODEX_USER_AGENT="codex-cli/0.130.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
ANTIGRAVITY_USER_AGENT="antigravity/1.23.2 darwin/arm64"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
QODER_USER_AGENT="Qoder-Cli"
QWEN_USER_AGENT="QwenCode/0.15.9 (linux; x64)"
CURSOR_USER_AGENT="Cursor/3.3"
GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -26,10 +26,7 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [
...ANTHROPIC_BETA_BASE.slice(3),
].join(",");
// Static-config fallbacks for providerRegistry.ts. Runtime cloak in base.ts
// emits the same values via CLAUDE_CODE_VERSION in claudeIdentity.ts —
// keep both in sync.
export const CLAUDE_CLI_VERSION = "2.1.131";
export const CLAUDE_CLI_VERSION = "2.1.137";
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.81.0";
export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0";

View File

@@ -1,4 +1,4 @@
const DEFAULT_CODEX_CLIENT_VERSION = "0.125.0";
const DEFAULT_CODEX_CLIENT_VERSION = "0.130.0";
const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200";
const DEFAULT_CODEX_USER_AGENT_ARCH = "x64";
const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION";

View File

@@ -3,22 +3,101 @@ import { ANTHROPIC_VERSION_HEADER } from "./anthropicHeaders.ts";
type JsonRecord = Record<string, unknown>;
export type GlmApiRegion = "international" | "china";
export type GlmTransport = "openai" | "anthropic";
export const GLM_SHARED_HEADERS = Object.freeze({
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
export const GLM_DEFAULT_BASE_URLS = Object.freeze({
international: "https://api.z.ai/api/coding/paas/v4/chat/completions",
china: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
});
export const GLM_ANTHROPIC_DEFAULT_BASE_URLS = Object.freeze({
international: "https://api.z.ai/api/anthropic/v1/messages",
china: "https://open.bigmodel.cn/api/anthropic/v1/messages",
});
export const GLM_SHARED_MODELS = Object.freeze([
{ id: "glm-5.1", name: "GLM 5.1", contextLength: 204800 },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-5-turbo", name: "GLM 5 Turbo" },
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)", contextLength: 128000 },
{ id: "glm-4.6", name: "GLM 4.6" },
{ id: "glm-4.5v", name: "GLM 4.5V (Vision)", contextLength: 16000 },
{ id: "glm-4.5", name: "GLM 4.5", contextLength: 128000 },
{ id: "glm-4.5-air", name: "GLM 4.5 Air", contextLength: 128000 },
{
id: "glm-5.1",
name: "GLM 5.1",
contextLength: 204800,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5",
name: "GLM 5",
contextLength: 200000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5-turbo",
name: "GLM 5 Turbo",
contextLength: 200000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-4.7-flash",
name: "GLM 4.7 Flash",
contextLength: 200000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-4.7",
name: "GLM 4.7",
contextLength: 200000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-4.6v",
name: "GLM 4.6V (Vision)",
contextLength: 128000,
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
},
{
id: "glm-4.6",
name: "GLM 4.6",
contextLength: 200000,
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-4.5v",
name: "GLM 4.5V (Vision)",
contextLength: 16000,
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportsVision: true,
},
{
id: "glm-4.5",
name: "GLM 4.5",
contextLength: 128000,
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-4.5-air",
name: "GLM 4.5 Air",
contextLength: 128000,
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
},
]);
export const GLM_MODELS_URLS = Object.freeze({
@@ -33,25 +112,258 @@ export const GLM_QUOTA_URLS = Object.freeze({
export const GLMT_TIMEOUT_MS = 900_000;
export const GLM_TIMEOUT_MS = 900_000;
export const GLM_REQUEST_DEFAULTS = Object.freeze({
maxTokens: 16_384,
});
export const GLMT_REQUEST_DEFAULTS = Object.freeze({
maxTokens: 65_536,
temperature: 0.2,
thinkingBudgetTokens: 24_576,
thinkingType: "adaptive" as const,
});
export const GLM_COUNT_TOKENS_TIMEOUT_MS = 3_000;
export const GLM_CLAUDE_CODE_USER_AGENT = "claude-cli/2.1.137 (external, sdk-cli)";
export const GLM_ANTHROPIC_BETA = [
"claude-code-20250219",
"interleaved-thinking-2025-05-14",
"context-management-2025-06-27",
"prompt-caching-scope-2026-01-05",
"advisor-tool-2026-03-01",
"effort-2025-11-24",
].join(",");
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function asString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function splitUrlQueryAndHash(url: string): { base: string; suffix: string } {
const match = url.match(/^([^?#]*)(.*)$/);
return { base: match?.[1] ?? url, suffix: match?.[2] ?? "" };
}
export function getGlmApiRegion(providerSpecificData: unknown): GlmApiRegion {
const data = asRecord(providerSpecificData);
return data.apiRegion === "china" ? "china" : "international";
}
export function getGlmModelsUrl(providerSpecificData: unknown): string {
export function buildGlmModelsUrl(
providerSpecificData: unknown,
transport: GlmTransport = "openai",
fallbackBaseUrl?: string | null
): string {
const data = asRecord(providerSpecificData);
const customModelsUrl = asString(data.modelsUrl);
if (customModelsUrl) return customModelsUrl;
if (transport === "anthropic") {
return joinGlmBaseAndPath(
getGlmAnthropicBaseUrl(providerSpecificData, fallbackBaseUrl),
"/v1/models"
);
}
const configuredBaseUrl = asString(data.baseUrl);
if (configuredBaseUrl) {
if (isAnthropicGlmBaseUrl(configuredBaseUrl)) {
return GLM_MODELS_URLS[getGlmApiRegion(providerSpecificData)];
}
return joinGlmBaseAndPath(configuredBaseUrl, "/models");
}
return GLM_MODELS_URLS[getGlmApiRegion(providerSpecificData)];
}
export function getGlmQuotaUrl(providerSpecificData: unknown): string {
return GLM_QUOTA_URLS[getGlmApiRegion(providerSpecificData)];
}
function stripKnownGlmEndpointSuffix(baseUrl: string): { base: string; suffix: string } {
const parts = splitUrlQueryAndHash(baseUrl);
const base = parts.base
.replace(/\/+$/g, "")
.replace(/\/(?:v\d+\/)?messages\/count_tokens$/i, "")
.replace(/\/(?:v\d+\/)?messages$/i, "")
.replace(/\/chat\/completions$/i, "")
.replace(/\/models$/i, "");
return { base, suffix: parts.suffix };
}
function joinGlmBaseAndPath(baseUrl: string, path: string): string {
const { base, suffix } = stripKnownGlmEndpointSuffix(baseUrl);
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const versionMatch = base.match(/\/v\d+$/i);
if (
versionMatch &&
normalizedPath.toLowerCase().startsWith(`${versionMatch[0].toLowerCase()}/`)
) {
return `${base}${normalizedPath.slice(versionMatch[0].length)}${suffix}`;
}
return `${base}${normalizedPath}${suffix}`;
}
function stripQueryAndTrailingSlash(baseUrl: string): string {
return splitUrlQueryAndHash(baseUrl).base.replace(/\/+$/g, "");
}
function addBetaQuery(url: string): string {
const parsed = new URL(url);
parsed.searchParams.set("beta", "true");
return parsed.toString();
}
export function isAnthropicGlmBaseUrl(baseUrl: string): boolean {
return /\/api\/anthropic(?:\/|$)/i.test(stripQueryAndTrailingSlash(baseUrl));
}
export function isCodingGlmBaseUrl(baseUrl: string): boolean {
return /\/api\/coding\/paas\/v\d+(?:\/|$)/i.test(stripQueryAndTrailingSlash(baseUrl));
}
export function getGlmBaseUrl(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): string {
const data = asRecord(providerSpecificData);
const configuredBaseUrl = asString(data.baseUrl);
if (configuredBaseUrl) return configuredBaseUrl;
const regionalBaseUrl =
typeof fallbackBaseUrl === "string" &&
fallbackBaseUrl.trim() &&
isCodingGlmBaseUrl(fallbackBaseUrl)
? fallbackBaseUrl.trim()
: GLM_DEFAULT_BASE_URLS[getGlmApiRegion(providerSpecificData)];
if (regionalBaseUrl) return regionalBaseUrl;
return typeof fallbackBaseUrl === "string" && fallbackBaseUrl.trim()
? fallbackBaseUrl.trim()
: GLM_DEFAULT_BASE_URLS.international;
}
export function getGlmAnthropicBaseUrl(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): string {
const data = asRecord(providerSpecificData);
const anthropicBaseUrl = asString(data.anthropicBaseUrl);
if (anthropicBaseUrl) return anthropicBaseUrl;
const configuredBaseUrl = asString(data.baseUrl);
if (configuredBaseUrl) {
if (isCodingGlmBaseUrl(configuredBaseUrl)) {
return GLM_ANTHROPIC_DEFAULT_BASE_URLS[getGlmApiRegion(providerSpecificData)];
}
return configuredBaseUrl;
}
if (
typeof fallbackBaseUrl === "string" &&
fallbackBaseUrl.trim() &&
isCodingGlmBaseUrl(fallbackBaseUrl)
) {
return GLM_ANTHROPIC_DEFAULT_BASE_URLS[
fallbackBaseUrl.includes("open.bigmodel.cn") ? "china" : getGlmApiRegion(providerSpecificData)
];
}
if (
typeof fallbackBaseUrl === "string" &&
fallbackBaseUrl.trim() &&
!isCodingGlmBaseUrl(fallbackBaseUrl)
) {
return fallbackBaseUrl.trim();
}
return GLM_ANTHROPIC_DEFAULT_BASE_URLS[getGlmApiRegion(providerSpecificData)];
}
export function getGlmPrimaryTransport(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): GlmTransport {
const data = asRecord(providerSpecificData);
const configuredTransport = asString(data.primaryTransport);
if (configuredTransport === "anthropic") return "anthropic";
if (configuredTransport === "openai") return "openai";
return isAnthropicGlmBaseUrl(getGlmBaseUrl(providerSpecificData, fallbackBaseUrl))
? "anthropic"
: "openai";
}
export function getGlmTransport(providerSpecificData: unknown, fallbackBaseUrl?: string | null) {
return getGlmPrimaryTransport(providerSpecificData, fallbackBaseUrl);
}
export function buildGlmChatUrl(
providerSpecificData: unknown,
transport: GlmTransport = "openai",
fallbackBaseUrl?: string | null
): string {
if (transport === "anthropic") {
return buildGlmAnthropicMessagesUrl(providerSpecificData, fallbackBaseUrl);
}
return buildGlmOpenAIChatUrl(providerSpecificData, fallbackBaseUrl);
}
export function buildGlmOpenAIChatUrl(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): string {
const configuredBaseUrl = getGlmBaseUrl(providerSpecificData, fallbackBaseUrl);
const baseUrl = isAnthropicGlmBaseUrl(configuredBaseUrl)
? GLM_DEFAULT_BASE_URLS[getGlmApiRegion(providerSpecificData)]
: configuredBaseUrl;
return joinGlmBaseAndPath(baseUrl, "/chat/completions");
}
export function buildGlmAnthropicMessagesUrl(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): string {
return addBetaQuery(
joinGlmBaseAndPath(
getGlmAnthropicBaseUrl(providerSpecificData, fallbackBaseUrl),
"/v1/messages"
)
);
}
export function buildGlmCountTokensUrl(
providerSpecificData: unknown,
fallbackBaseUrl?: string | null
): string {
return addBetaQuery(
joinGlmBaseAndPath(
getGlmAnthropicBaseUrl(providerSpecificData, fallbackBaseUrl),
"/v1/messages/count_tokens"
)
);
}
export function buildGlmCodingHeaders(apiKey: string, stream = true): Record<string, string> {
return {
"Content-Type": "application/json",
Accept: stream ? "text/event-stream" : "application/json",
Authorization: `Bearer ${apiKey}`,
};
}
export function buildGlmBaseHeaders(apiKey: string, stream = true): Record<string, string> {
return {
"Content-Type": "application/json",
Accept: stream ? "text/event-stream" : "application/json",
"x-api-key": apiKey,
"anthropic-version": ANTHROPIC_VERSION_HEADER,
"anthropic-beta": GLM_ANTHROPIC_BETA,
"anthropic-dangerous-direct-browser-access": "true",
"User-Agent": GLM_CLAUDE_CODE_USER_AGENT,
"X-Stainless-Lang": "js",
"X-Stainless-Runtime": "node",
"X-Stainless-Retry-Count": "0",
"accept-language": "*",
"accept-encoding": "gzip, deflate, br, zstd",
connection: "keep-alive",
};
}

View File

@@ -11,7 +11,7 @@ export const GITHUB_COPILOT_OPENAI_INTENT = "conversation-panel";
export const GITHUB_COPILOT_DEFAULT_INITIATOR = "user";
export const GITHUB_COPILOT_USER_AGENT_LIBRARY = "electron-fetch";
export const QWEN_CLI_VERSION = "0.15.4";
export const QWEN_CLI_VERSION = "0.15.9";
export const QWEN_STAINLESS_LANG = "js";
export const QWEN_STAINLESS_PACKAGE_VERSION = "5.11.0";
export const QWEN_STAINLESS_RETRY_COUNT = "1";
@@ -26,7 +26,7 @@ export const KIRO_AMZ_USER_AGENT = "aws-sdk-js/3.0.0 kiro-ide/1.0.0";
export const KIRO_STREAMING_TARGET =
"AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
export const CURSOR_REGISTRY_VERSION = "3.2.14";
export const CURSOR_REGISTRY_VERSION = "3.3";
export function getGitHubCopilotChatHeaders(
accept = "application/json",

View File

@@ -18,9 +18,10 @@ import {
} from "./anthropicHeaders.ts";
import { getCodexDefaultHeaders } from "./codexClient.ts";
import {
GLM_REQUEST_DEFAULTS,
GLMT_REQUEST_DEFAULTS,
GLM_TIMEOUT_MS,
GLMT_TIMEOUT_MS,
GLM_SHARED_HEADERS,
GLM_SHARED_MODELS,
} from "./glmProvider.ts";
import { MARITALK_DEFAULT_BASE_URL } from "./maritalk.ts";
@@ -46,6 +47,7 @@ export interface RegistryModel {
supportsReasoning?: boolean;
supportsVision?: boolean;
supportsXHighEffort?: boolean;
maxOutputTokens?: number;
targetFormat?: string;
strip?: readonly string[];
unsupportedParams?: readonly string[];
@@ -880,14 +882,14 @@ export const REGISTRY: Record<string, RegistryEntry> = {
glm: {
id: "glm",
alias: "glm",
format: "claude",
executor: "default",
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
format: "openai",
executor: "glm",
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
defaultContextLength: 200000,
urlSuffix: "?beta=true",
authType: "apikey",
authHeader: "x-api-key",
headers: GLM_SHARED_HEADERS,
authHeader: "bearer",
requestDefaults: GLM_REQUEST_DEFAULTS,
timeoutMs: GLM_TIMEOUT_MS,
models: [...GLM_SHARED_MODELS],
},
@@ -895,26 +897,26 @@ export const REGISTRY: Record<string, RegistryEntry> = {
id: "glm-cn",
alias: "glm-cn",
format: "openai",
executor: "default",
executor: "glm",
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
authType: "apikey",
authHeader: "bearer",
defaultContextLength: 200000,
models: [{ id: "glm-5.1", name: "GLM-5.1" }],
requestDefaults: GLM_REQUEST_DEFAULTS,
timeoutMs: GLM_TIMEOUT_MS,
models: [...GLM_SHARED_MODELS],
passthroughModels: true,
},
glmt: {
id: "glmt",
alias: "glmt",
format: "claude",
executor: "default",
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
format: "openai",
executor: "glm",
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
defaultContextLength: 200000,
urlSuffix: "?beta=true",
authType: "apikey",
authHeader: "x-api-key",
headers: GLM_SHARED_HEADERS,
authHeader: "bearer",
requestDefaults: GLMT_REQUEST_DEFAULTS,
timeoutMs: GLMT_TIMEOUT_MS,
models: [...GLM_SHARED_MODELS],

View File

@@ -453,7 +453,7 @@ export class AntigravityExecutor extends BaseExecutor {
return { ...c, role, parts };
}) || [];
const contents = [];
const contents: any[] = [];
for (const c of normalizedContents) {
if (!Array.isArray(c.parts) || c.parts.length === 0) continue;
if (contents.length > 0 && contents[contents.length - 1].role === c.role) {
@@ -535,9 +535,9 @@ export class AntigravityExecutor extends BaseExecutor {
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: credentials.refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
refresh_token: credentials.refreshToken || "",
client_id: this.config.clientId || "",
client_secret: this.config.clientSecret || "",
}),
});
@@ -802,7 +802,7 @@ export class AntigravityExecutor extends BaseExecutor {
let response = await fetch(url, {
method: "POST",
headers: finalHeaders,
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream),
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream) as any,
...(stream ? { duplex: "half" } : {}),
signal,
});
@@ -814,7 +814,7 @@ export class AntigravityExecutor extends BaseExecutor {
response = await fetch(url, {
method: "POST",
headers: retryHeaders,
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream),
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream) as any,
...(stream ? { duplex: "half" } : {}),
signal,
});
@@ -829,7 +829,7 @@ export class AntigravityExecutor extends BaseExecutor {
}
// Parse retry time for 429/503 responses
let retryMs = null;
let retryMs: number | null = null;
if (
response.status === HTTP_STATUS.RATE_LIMITED ||
@@ -884,7 +884,7 @@ export class AntigravityExecutor extends BaseExecutor {
const creditsResp = await fetch(url, {
method: "POST",
headers: finalCreditsHeaders,
body: getChunkedOrFixedBody(serializedCreditsRequest.bodyString, stream),
body: getChunkedOrFixedBody(serializedCreditsRequest.bodyString, stream) as any,
...(stream ? { duplex: "half" } : {}),
signal,
});

View File

@@ -205,6 +205,10 @@ export class BaseExecutor {
return Math.max(1, Math.floor(configured));
}
getCountTokensTimeoutMs() {
return this.getTimeoutMs();
}
buildUrl(
model: string,
stream: boolean,
@@ -228,7 +232,7 @@ export class BaseExecutor {
return `${normalized}${path}`;
}
const baseUrls = this.getBaseUrls();
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl || "";
}
buildHeaders(
@@ -333,7 +337,10 @@ export class BaseExecutor {
static FETCH_START_TIMEOUT_MS = FETCH_TIMEOUT_MS;
// Override in subclass for provider-specific refresh
async refreshCredentials(credentials: ProviderCredentials, log: ExecutorLog | null) {
async refreshCredentials(
credentials: ProviderCredentials,
log: ExecutorLog | null
): Promise<Partial<ProviderCredentials> | null> {
void credentials;
void log;
return null;
@@ -379,12 +386,12 @@ export class BaseExecutor {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let activeSignal = signal || null;
let controller: AbortController | null = null;
const timeoutMs = this.getTimeoutMs();
const timeoutMs = this.getCountTokensTimeoutMs();
if (!activeSignal) {
if (timeoutMs > 0) {
controller = new AbortController();
timeoutId = setTimeout(() => controller?.abort(), timeoutMs);
activeSignal = controller.signal;
activeSignal = signal ? mergeAbortSignals(signal, controller.signal) : controller.signal;
}
try {

View File

@@ -1088,7 +1088,7 @@ export class CodexExecutor extends BaseExecutor {
};
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
buildUrl(model: string, stream: boolean, urlIndex = 0, credentials: any = null) {
void model;
void stream;
void urlIndex;

400
open-sse/executors/glm.ts Normal file
View File

@@ -0,0 +1,400 @@
import { randomUUID } from "node:crypto";
import { DefaultExecutor } from "./default.ts";
import {
applyConfiguredUserAgent,
mergeAbortSignals,
mergeUpstreamExtraHeaders,
type CountTokensInput,
type ExecuteInput,
type ProviderCredentials,
} from "./base.ts";
import {
buildGlmBaseHeaders,
buildGlmChatUrl,
buildGlmCodingHeaders,
buildGlmCountTokensUrl,
GLM_COUNT_TOKENS_TIMEOUT_MS,
type GlmTransport,
getGlmTransport,
} from "../config/glmProvider.ts";
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
import { CLAUDE_CLI_STAINLESS_PACKAGE_VERSION } from "../config/anthropicHeaders.ts";
import {
getRuntimeVersion,
normalizeStainlessArch,
normalizeStainlessPlatform,
} from "../config/providerHeaderProfiles.ts";
import { translateNonStreamingResponse } from "../handlers/responseTranslator.ts";
import { translateRequest } from "../translator/index.ts";
import { FORMATS } from "../translator/formats.ts";
import { createSSETransformStreamWithLogger } from "../utils/stream.ts";
import { ensureStreamReadiness } from "../utils/streamReadiness.ts";
import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts";
type JsonRecord = Record<string, unknown>;
type GlmExecuteResult = Awaited<ReturnType<DefaultExecutor["execute"]>> & {
targetFormat?: string;
};
function asRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
function getEffectiveKey(credentials: ProviderCredentials): string {
const extraKeys = (credentials.providerSpecificData?.extraApiKeys as string[] | undefined) ?? [];
if (credentials.apiKey && credentials.connectionId && extraKeys.length > 0) {
return getRotatingApiKey(credentials.connectionId, credentials.apiKey, extraKeys);
}
return credentials.apiKey || credentials.accessToken || "";
}
function applyGlmRequestDefaults(body: unknown, defaults?: JsonRecord | null): unknown {
const record = asRecord(body);
if (!record || !defaults) return body;
const next = { ...(applyProviderRequestDefaults(record, defaults) as JsonRecord) };
const thinkingType = typeof defaults.thinkingType === "string" ? defaults.thinkingType : null;
if (thinkingType && next.thinking === undefined) {
next.thinking = { type: thinkingType };
} else if (thinkingType && asRecord(next.thinking)?.type === "enabled") {
next.thinking = { ...asRecord(next.thinking), type: thinkingType };
}
return next;
}
function hasTools(body: unknown): boolean {
const record = asRecord(body);
return Array.isArray(record?.tools) && record.tools.length > 0;
}
function isRetryableGlmFallbackStatus(status: number): boolean {
return status === 404 || status === 408 || status === 409 || status === 429 || status >= 500;
}
function isRetryableGlmFallbackError(error: unknown): boolean {
if (!error) return false;
const err = error instanceof Error ? error : new Error(String(error));
if (err.name === "AbortError") return false;
return true;
}
function cloneHeaders(headers: Headers): Headers {
const next = new Headers();
headers.forEach((value, key) => next.set(key, value));
return next;
}
function isJsonResponse(response: Response): boolean {
return (response.headers.get("content-type") || "").toLowerCase().includes("application/json");
}
async function translateJsonResponse(response: Response): Promise<Response> {
const parsed = await response.json().catch(() => null);
const translated = translateNonStreamingResponse(parsed, FORMATS.CLAUDE, FORMATS.OPENAI);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "application/json");
headers.delete("content-length");
return new Response(JSON.stringify(translated), {
status: response.status,
statusText: response.statusText,
headers,
});
}
async function translateAnthropicJsonResponse(response: Response): Promise<Response> {
const parsed = await response.json().catch(() => null);
const translated = response.ok
? translateNonStreamingResponse(parsed, FORMATS.CLAUDE, FORMATS.OPENAI)
: translateAnthropicJsonError(parsed);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "application/json");
headers.delete("content-length");
return new Response(JSON.stringify(translated), {
status: response.status,
statusText: response.statusText,
headers,
});
}
function translateAnthropicJsonError(parsed: unknown): JsonRecord {
const root = asRecord(parsed) || {};
const error = asRecord(root.error) || root;
const message =
typeof error.message === "string" && error.message.trim()
? error.message
: typeof root.message === "string" && root.message.trim()
? root.message
: "GLM Anthropic transport error";
const type =
typeof error.type === "string" && error.type.trim()
? error.type
: typeof root.type === "string" && root.type.trim()
? root.type
: "upstream_error";
return {
error: {
message,
type,
},
};
}
function translateSseResponse(response: Response, provider: string, model: string): Response {
if (!response.body) return response;
const transform = createSSETransformStreamWithLogger(
FORMATS.CLAUDE,
FORMATS.OPENAI,
provider,
null,
null,
model
);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "text/event-stream");
headers.delete("content-length");
return new Response(response.body.pipeThrough(transform), {
status: response.status,
statusText: response.statusText,
headers,
});
}
export class GlmExecutor extends DefaultExecutor {
constructor(provider = "glm") {
super(provider);
}
buildUrl(
_model: string,
_stream: boolean,
_urlIndex = 0,
credentials: ProviderCredentials | null = null
) {
const primaryTransport = getGlmTransport(credentials?.providerSpecificData);
const transport =
_urlIndex === 1 ? (primaryTransport === "openai" ? "anthropic" : "openai") : primaryTransport;
return buildGlmChatUrl(credentials?.providerSpecificData, transport, this.config.baseUrl);
}
buildCountTokensUrl(_model: string, credentials: ProviderCredentials | null = null) {
return buildGlmCountTokensUrl(credentials?.providerSpecificData, this.config.baseUrl);
}
getCountTokensTimeoutMs() {
return GLM_COUNT_TOKENS_TIMEOUT_MS;
}
buildHeaders(
credentials: ProviderCredentials,
stream = true,
_clientHeaders?: Record<string, string> | null,
_model?: string,
transport: GlmTransport = getGlmTransport(credentials.providerSpecificData)
): Record<string, string> {
if (transport === "openai") {
return buildGlmCodingHeaders(getEffectiveKey(credentials), stream);
}
return {
...buildGlmBaseHeaders(getEffectiveKey(credentials), stream),
"X-Stainless-Arch": normalizeStainlessArch(),
"X-Stainless-OS": normalizeStainlessPlatform(),
"X-Stainless-Runtime-Version": getRuntimeVersion(),
"X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
"X-Claude-Code-Session-Id": randomUUID(),
"x-client-request-id": randomUUID(),
};
}
transformRequest(
model: string,
body: unknown,
stream: boolean,
credentials: ProviderCredentials
) {
const cleanedBody = super.transformRequest(model, body, stream, credentials);
return applyGlmRequestDefaults(cleanedBody, this.config.requestDefaults as JsonRecord | null);
}
transformForTransport(
model: string,
body: unknown,
stream: boolean,
credentials: ProviderCredentials,
transport: GlmTransport
) {
const transformed = this.transformRequest(model, body, stream, credentials);
if (transport === "openai") {
const record = asRecord(transformed);
if (record && stream && hasTools(record) && record.tool_stream === undefined) {
return { ...record, tool_stream: true };
}
return transformed;
}
return translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
model,
{ ...(transformed as JsonRecord), _disableToolPrefix: true },
stream,
credentials,
this.provider,
null,
{ preserveCacheControl: false }
);
}
private async executeTransport(
input: ExecuteInput,
transport: GlmTransport
): Promise<GlmExecuteResult> {
const credentials = input.credentials;
const url = buildGlmChatUrl(credentials?.providerSpecificData, transport, this.config.baseUrl);
const headers = this.buildHeaders(
credentials,
input.stream,
input.clientHeaders,
input.model,
transport
);
applyConfiguredUserAgent(headers, credentials.providerSpecificData);
mergeUpstreamExtraHeaders(headers, input.upstreamExtraHeaders);
const transformedBody = this.transformForTransport(
input.model,
input.body,
input.stream,
credentials,
transport
);
const fetchStartTimeoutMs = this.getTimeoutMs();
const timeoutController = fetchStartTimeoutMs > 0 ? new AbortController() : null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (timeoutController) {
timeoutId = setTimeout(() => {
const timeoutError = new Error(`Fetch timeout after ${fetchStartTimeoutMs}ms on ${url}`);
timeoutError.name = "TimeoutError";
timeoutController.abort(timeoutError);
}, fetchStartTimeoutMs);
}
const timeoutSignal = timeoutController?.signal ?? null;
const combinedSignal =
input.signal && timeoutSignal
? mergeAbortSignals(input.signal, timeoutSignal)
: input.signal || timeoutSignal;
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
signal: combinedSignal || undefined,
});
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
if (input.stream && response.ok) {
const readiness = await ensureStreamReadiness(response, {
timeoutMs: STREAM_IDLE_TIMEOUT_MS,
provider: this.provider,
model: input.model,
log: input.log,
});
response = readiness.response;
}
const result = { response, url, headers, transformedBody };
if (transport === "anthropic") {
const translatedResponse =
input.stream && result.response.ok
? translateSseResponse(result.response, this.provider, input.model)
: isJsonResponse(result.response)
? await translateAnthropicJsonResponse(result.response)
: result.response;
return {
...result,
response: translatedResponse,
url,
headers,
transformedBody,
targetFormat: FORMATS.OPENAI,
};
}
return {
...result,
url,
headers,
transformedBody,
targetFormat: FORMATS.OPENAI,
};
}
async execute(input: ExecuteInput): Promise<GlmExecuteResult> {
const primaryTransport = getGlmTransport(
input.credentials.providerSpecificData,
this.config.baseUrl
);
const fallbackTransport: GlmTransport = primaryTransport === "openai" ? "anthropic" : "openai";
let primaryResult: GlmExecuteResult | null = null;
try {
primaryResult = await this.executeTransport(input, primaryTransport);
if (!isRetryableGlmFallbackStatus(primaryResult.response.status)) {
return primaryResult;
}
input.log?.debug?.(
"GLM_FALLBACK",
`${primaryTransport} returned ${primaryResult.response.status}; trying ${fallbackTransport}`
);
} catch (error) {
if (!isRetryableGlmFallbackError(error)) throw error;
input.log?.debug?.(
"GLM_FALLBACK",
`${primaryTransport} error (${error instanceof Error ? error.message : String(error)}); trying ${fallbackTransport}`
);
}
try {
const fallbackResult = await this.executeTransport(input, fallbackTransport);
if (fallbackResult.response.ok || !primaryResult) {
return fallbackResult;
}
} catch (error) {
if (!primaryResult) throw error;
input.log?.debug?.(
"GLM_FALLBACK",
`${fallbackTransport} fallback failed (${error instanceof Error ? error.message : String(error)}); returning primary response`
);
}
return primaryResult;
}
async countTokens(input: CountTokensInput) {
return super.countTokens({
...input,
credentials: {
...input.credentials,
providerSpecificData: {
...(input.credentials.providerSpecificData || {}),
primaryTransport: "anthropic",
},
},
});
}
}
export default GlmExecutor;

View File

@@ -6,6 +6,7 @@ import { KiroExecutor } from "./kiro.ts";
import { CodexExecutor } from "./codex.ts";
import { CursorExecutor } from "./cursor.ts";
import { DefaultExecutor } from "./default.ts";
import { GlmExecutor } from "./glm.ts";
import { PollinationsExecutor } from "./pollinations.ts";
import { CloudflareAIExecutor } from "./cloudflare-ai.ts";
import { OpencodeExecutor } from "./opencode.ts";
@@ -31,6 +32,9 @@ const executors = {
"amazon-q": new KiroExecutor("amazon-q"),
codex: new CodexExecutor(),
cursor: new CursorExecutor(),
glm: new GlmExecutor("glm"),
"glm-cn": new GlmExecutor("glm-cn"),
glmt: new GlmExecutor("glmt"),
cu: new CursorExecutor(), // Alias for cursor
"azure-openai": new AzureOpenAIExecutor(),
gitlab: new GitlabExecutor(),
@@ -81,6 +85,7 @@ export { KiroExecutor } from "./kiro.ts";
export { CodexExecutor } from "./codex.ts";
export { CursorExecutor } from "./cursor.ts";
export { DefaultExecutor } from "./default.ts";
export { GlmExecutor } from "./glm.ts";
export { PollinationsExecutor } from "./pollinations.ts";
export { CloudflareAIExecutor } from "./cloudflare-ai.ts";
export { OpencodeExecutor } from "./opencode.ts";

View File

@@ -3996,6 +3996,12 @@ export async function handleChatCore({
});
if (streamReadiness.ok === false) {
const { response: failureResponse, reason } = streamReadiness;
const failure = {
status: failureResponse.status,
message: reason,
code: streamReadiness.code,
type: streamReadiness.type,
};
trackPendingRequest(model, provider, connectionId, false);
appendRequestLog({
model,
@@ -4011,7 +4017,7 @@ export async function handleChatCore({
claudeCacheMeta: claudePromptCacheLogMeta,
cacheSource: "upstream",
});
persistFailureUsage(failureResponse.status, "stream_readiness_timeout");
persistFailureUsage(failureResponse.status, streamReadiness.code);
// Do NOT call onStreamFailure — a stream stall is an upstream issue,
// not an account/quota failure. Marking the account unavailable here
// would lock out legitimate accounts when the upstream hangs.
@@ -4019,7 +4025,7 @@ export async function handleChatCore({
success: false,
status: failureResponse.status,
error: reason,
errorType: "stream_readiness_timeout",
errorType: streamReadiness.type,
response: failureResponse,
};
}

View File

@@ -6,6 +6,7 @@ import {
getModelsByProviderId,
getProviderModels,
} from "../../config/providerModels.ts";
import { buildGlmAnthropicMessagesUrl, buildGlmOpenAIChatUrl } from "../../config/glmProvider.ts";
import { supportsToolCalling } from "../../services/modelCapabilities.ts";
import { getPricingForModel } from "../../../src/shared/constants/pricing.ts";
@@ -16,31 +17,70 @@ describe("GLM Coding provider registry surfaces", () => {
expect(entry).toBeDefined();
expect(entry?.id).toBe("glm");
expect(entry?.alias).toBe("glm");
expect(entry?.format).toBe("claude");
expect(entry?.baseUrl).toBe("https://api.z.ai/api/anthropic/v1/messages");
expect(entry?.format).toBe("openai");
expect(entry?.executor).toBe("glm");
expect(entry?.baseUrl).toBe("https://api.z.ai/api/coding/paas/v4/chat/completions");
expect(entry?.authType).toBe("apikey");
expect(entry?.authHeader).toBe("x-api-key");
expect(entry?.headers?.["Anthropic-Version"]).toBe("2023-06-01");
expect(entry?.authHeader).toBe("bearer");
expect(entry?.headers?.["Anthropic-Version"]).toBeUndefined();
expect(entry?.requestDefaults).toEqual({ maxTokens: 16384 });
expect(entry?.timeoutMs).toBe(900000);
});
it("registers GLMT as an explicit high-budget preset over the GLM transport", () => {
it("preserves custom GLM base URL query parameters while deriving transport endpoints", () => {
const providerSpecificData = {
baseUrl:
"https://proxy.example/glm/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm",
};
expect(buildGlmOpenAIChatUrl(providerSpecificData)).toBe(
"https://proxy.example/glm/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm"
);
expect(
buildGlmAnthropicMessagesUrl({
anthropicBaseUrl:
"https://proxy.example/glm/api/anthropic/v1/messages?tenant=alpha&route=glm",
})
).toBe("https://proxy.example/glm/api/anthropic/v1/messages?tenant=alpha&route=glm&beta=true");
});
it("registers GLMT as an explicit high-budget preset over the dual GLM transport", () => {
const entry = getRegistryEntry("glmt");
expect(entry).toBeDefined();
expect(entry?.id).toBe("glmt");
expect(entry?.alias).toBe("glmt");
expect(entry?.format).toBe("claude");
expect(entry?.baseUrl).toBe("https://api.z.ai/api/anthropic/v1/messages");
expect(entry?.format).toBe("openai");
expect(entry?.executor).toBe("glm");
expect(entry?.baseUrl).toBe("https://api.z.ai/api/coding/paas/v4/chat/completions");
expect(entry?.authType).toBe("apikey");
expect(entry?.authHeader).toBe("x-api-key");
expect(entry?.authHeader).toBe("bearer");
expect(entry?.headers?.["Anthropic-Version"]).toBeUndefined();
expect(entry?.requestDefaults).toEqual({
maxTokens: 65536,
temperature: 0.2,
thinkingBudgetTokens: 24576,
thinkingType: "adaptive",
});
expect(entry?.timeoutMs).toBe(900000);
});
it("registers GLM China on the same executor and capability surface", () => {
const entry = getRegistryEntry("glm-cn");
expect(entry).toBeDefined();
expect(entry?.id).toBe("glm-cn");
expect(entry?.alias).toBe("glmcn");
expect(entry?.format).toBe("openai");
expect(entry?.executor).toBe("glm");
expect(entry?.baseUrl).toBe("https://open.bigmodel.cn/api/coding/paas/v4/chat/completions");
expect(entry?.requestDefaults).toEqual({ maxTokens: 16384 });
expect(entry?.timeoutMs).toBe(900000);
expect(getProviderModels("glmcn").map((model) => model.id)).toEqual(
getProviderModels("glm").map((model) => model.id)
);
});
it("exposes the same GLM model inventory through registry-derived model helpers", () => {
const byProviderId = getModelsByProviderId("glm");
const byAlias = getProviderModels("glm");
@@ -71,13 +111,15 @@ describe("GLM Coding provider registry surfaces", () => {
expect(get("glm-4.5v")?.contextLength).toBe(16000);
expect(get("glm-4.5")?.contextLength).toBe(128000);
expect(get("glm-4.5-air")?.contextLength).toBe(128000);
expect(get("glm-5.1")?.maxOutputTokens).toBe(131072);
expect(get("glm-4.6")?.maxOutputTokens).toBe(32768);
// Models inheriting the 200K provider default
expect(get("glm-5")?.contextLength).toBeUndefined();
expect(get("glm-5-turbo")?.contextLength).toBeUndefined();
expect(get("glm-4.7-flash")?.contextLength).toBeUndefined();
expect(get("glm-4.7")?.contextLength).toBeUndefined();
expect(get("glm-4.6")?.contextLength).toBeUndefined();
// Models with explicit 200K defaults to avoid null capabilities in direct routes.
expect(get("glm-5")?.contextLength).toBe(200000);
expect(get("glm-5-turbo")?.contextLength).toBe(200000);
expect(get("glm-4.7-flash")?.contextLength).toBe(200000);
expect(get("glm-4.7")?.contextLength).toBe(200000);
expect(get("glm-4.6")?.contextLength).toBe(200000);
});
it("keeps representative GLM Coding models tool-call capable and priced", () => {

View File

@@ -217,7 +217,7 @@ export async function getRuntimeProviderProfile(provider: string | null | undefi
try {
const { getCachedSettings } = await import("@/lib/db/readCache");
const settings = await getCachedSettings();
const category = getProviderCategory(provider);
const category = getProviderCategory(provider || "");
return buildProviderProfile(category, settings);
} catch {
return getProviderProfile(provider);
@@ -493,7 +493,7 @@ export function getModelLockoutInfo(provider, connectionId, model) {
*/
export function getAllModelLockouts() {
const now = Date.now();
const active = [];
const active: any[] = [];
for (const key of modelLockouts.keys()) {
cleanupModelLockKey(key, now);
}
@@ -616,7 +616,7 @@ export function getProvidersInCooldown(): Array<{
provider: string;
failureCount: number;
cooldownRemainingMs: number | null;
lastFailureAt: number;
lastFailureAt: number | null;
}> {
return getAllCircuitBreakerStatuses()
.filter((status) => {
@@ -880,14 +880,24 @@ export function getQuotaCooldown(backoffLevel = 0) {
* @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number, reason?: string }}
*/
export function checkFallbackError(
status,
errorText,
backoffLevel = 0,
_model = null,
provider = null,
headers = null,
status: number,
errorText: string | null,
backoffLevel: number = 0,
_model: string | null = null,
provider: string | null = null,
headers: any = null,
profileOverride: ProviderProfile | null = null
) {
): {
shouldFallback: boolean;
cooldownMs: number;
baseCooldownMs?: number;
newBackoffLevel?: number;
usedUpstreamRetryHint?: boolean;
reason?: string;
permanent?: boolean;
creditsExhausted?: boolean;
dailyQuotaExhausted?: boolean;
} {
const errorStr = (errorText || "").toString();
const profile = profileOverride ?? (provider ? getProviderProfile(provider) : null);
const maxBackoffSteps = profile?.maxBackoffSteps ?? BACKOFF_CONFIG.maxLevel;
@@ -1103,7 +1113,7 @@ export function getUnavailableUntil(cooldownMs) {
* Get the earliest rateLimitedUntil from a list of accounts
*/
export function getEarliestRateLimitedUntil(accounts) {
let earliest = null;
let earliest: number | null = null;
const now = Date.now();
for (const acc of accounts) {
if (!acc.rateLimitedUntil) continue;
@@ -1126,7 +1136,7 @@ export function formatRetryAfter(rateLimitedUntil) {
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
const parts = [];
const parts: string[] = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s > 0 || parts.length === 0) parts.push(`${s}s`);

View File

@@ -33,11 +33,8 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = [
"interleaved-thinking-2025-05-14",
"effort-2025-11-24",
].join(",");
// Keep aligned with CLAUDE_CODE_VERSION in claudeIdentity.ts. The
// "(external, sdk-cli)" suffix here distinguishes SDK-driven CC-compat
// relays from the native (external, cli) path.
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.131";
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.131 (external, sdk-cli)";
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.137";
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.137 (external, sdk-cli)";
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.81.0";
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0";
export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07";

View File

@@ -253,11 +253,12 @@ function getTargetProvider(modelStr: string, providerId?: string | null): string
return providerId || parsed.provider || parsed.providerAlias || "unknown";
}
function isStreamReadinessTimeoutErrorBody(errorBody: unknown): boolean {
function isStreamReadinessFailureErrorBody(errorBody: unknown): boolean {
if (!errorBody || typeof errorBody !== "object") return false;
const error = (errorBody as Record<string, unknown>).error;
if (!error || typeof error !== "object") return false;
return (error as Record<string, unknown>).code === "STREAM_READINESS_TIMEOUT";
const code = (error as Record<string, unknown>).code;
return code === "STREAM_READINESS_TIMEOUT" || code === "STREAM_EARLY_EOF";
}
function toRecordedTarget(target: ResolvedComboTarget) {
@@ -2124,8 +2125,9 @@ export async function handleComboChat({
}
}
const isStreamReadinessTimeout =
result.status === 504 && isStreamReadinessTimeoutErrorBody(errorBody);
const isStreamReadinessFailure =
(result.status === 502 || result.status === 504) &&
isStreamReadinessFailureErrorBody(errorBody);
// Fix #1681: Status 499 means client disconnected — stop combo loop immediately.
// There is no point trying fallback models when nobody is listening.
@@ -2157,13 +2159,13 @@ export async function handleComboChat({
);
// Trigger shared provider circuit breaker for 5xx errors and connection failures
if (isProviderFailureCode(result.status)) {
if (!isStreamReadinessFailure && isProviderFailureCode(result.status)) {
recordProviderFailure(provider, log, target.connectionId, profile);
}
// Check if this is a transient error worth retrying on same model
const isTransient =
!isStreamReadinessTimeout && [408, 429, 500, 502, 503, 504].includes(result.status);
!isStreamReadinessFailure && [408, 429, 500, 502, 503, 504].includes(result.status);
if (retry < maxRetries && isTransient) {
continue; // Retry same model
}
@@ -2432,8 +2434,12 @@ async function handleRoundRobinCombo({
if (text) {
errorText = text.substring(0, 500);
errorBody = JSON.parse(text);
const parsedError = errorBody?.error;
errorText =
errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
(typeof parsedError === "object" && parsedError?.message) ||
(typeof parsedError === "string" ? parsedError : null) ||
errorBody?.message ||
errorText;
retryAfter = errorBody?.retryAfter || null;
}
} catch {
@@ -2474,8 +2480,9 @@ async function handleRoundRobinCombo({
}
}
const isStreamReadinessTimeout =
result.status === 504 && isStreamReadinessTimeoutErrorBody(errorBody);
const isStreamReadinessFailure =
(result.status === 502 || result.status === 504) &&
isStreamReadinessFailureErrorBody(errorBody);
// Round-robin uses the same target-level fallback rule as other combo
// strategies: non-ok target responses fall through to the next target.
@@ -2498,7 +2505,11 @@ async function handleRoundRobinCombo({
);
// Transient errors → mark in semaphore so round-robin stops stampeding this target.
if (TRANSIENT_FOR_SEMAPHORE.includes(result.status) && cooldownMs > 0) {
if (
!isStreamReadinessFailure &&
TRANSIENT_FOR_SEMAPHORE.includes(result.status) &&
cooldownMs > 0
) {
semaphore.markRateLimited(semaphoreKey, cooldownMs);
log.warn("COMBO-RR", `${modelStr} error ${result.status}, cooldown ${cooldownMs}ms`);
}
@@ -2512,7 +2523,7 @@ async function handleRoundRobinCombo({
// Transient error → retry same model
const isTransient =
!isStreamReadinessTimeout && [408, 429, 500, 502, 503, 504].includes(result.status);
!isStreamReadinessFailure && [408, 429, 500, 502, 503, 504].includes(result.status);
if (retry < maxRetries && isTransient) {
continue;
}

View File

@@ -4,7 +4,7 @@ import {
normalizeCloudCodePlatform,
} from "./cloudCodeHeaders.ts";
export const GEMINI_CLI_VERSION = "0.40.1";
export const GEMINI_CLI_VERSION = "0.41.2";
export const GEMINI_CLI_GOOGLE_API_NODE_CLIENT_VERSION = "9.15.1";
const GEMINI_CLI_LOAD_CODE_ASSIST_METADATA = Object.freeze({

View File

@@ -4,6 +4,7 @@ export interface ProviderRequestDefaults {
maxTokens?: number;
temperature?: number;
thinkingBudgetTokens?: number;
thinkingType?: "enabled" | "adaptive";
}
function asRecord(value: unknown): JsonRecord | null {

View File

@@ -107,6 +107,10 @@ type UsageQuota = {
resetAt: string | null;
unlimited: boolean;
displayName?: string;
details?: Array<{
name: string;
used: number;
}>;
currency?: string;
grantedBalance?: number;
toppedUpBalance?: number;
@@ -126,6 +130,32 @@ function toNumber(value: unknown, fallback = 0): number {
return Number.isFinite(parsed) ? parsed : fallback;
}
function toPercentage(value: unknown): number {
return Math.max(0, Math.min(100, toNumber(value, 0)));
}
function toTitleCase(value: string): string {
return value
.trim()
.split(/[\s_-]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(" ");
}
function getGlmTokenQuotaName(
limit: JsonRecord,
existingQuotas: Record<string, UsageQuota>
): string {
const unit = toNumber(limit.unit, 0);
const number = toNumber(limit.number, 0);
if (unit === 3 && number === 5) return "session";
if ((unit === 4 && number === 7) || (unit === 3 && number >= 24 * 7)) return "weekly";
return existingQuotas.session ? "weekly" : "session";
}
function getFieldValue(source: unknown, snakeKey: string, camelKey: string): unknown {
const obj = toRecord(source);
return obj[snakeKey] ?? obj[camelKey] ?? null;
@@ -580,33 +610,71 @@ async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string,
}
const json = await res.json();
if (toNumber(json.code, 200) === 401 || json.success === false) {
throw new Error("Invalid API key");
}
const data = toRecord(json.data);
const limits: unknown[] = Array.isArray(data.limits) ? data.limits : [];
const quotas: Record<string, UsageQuota> = {};
for (const limit of limits) {
const src = toRecord(limit);
const label = getGlmQuotaLabel(src.type, src.unit);
if (!label) continue;
const usedPercent = toNumber(src.percentage, 0);
const type = String(src.type || "").toUpperCase();
const resetMs = toNumber(src.nextResetTime, 0);
const remaining = Math.max(0, 100 - usedPercent);
const resetAt = resetMs > 0 ? new Date(resetMs).toISOString() : null;
quotas[label] = {
used: usedPercent,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null,
unlimited: false,
};
if (type === "TOKENS_LIMIT") {
const quotaName = getGlmTokenQuotaName(src, quotas);
const usedPercent = toPercentage(src.percentage);
const remaining = Math.max(0, 100 - usedPercent);
quotas[quotaName] = {
used: usedPercent,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt,
unlimited: false,
};
continue;
}
if (type === "TIME_LIMIT") {
const total = toNumber(src.usage, toNumber(src.total, 0));
const remaining = toNumber(src.remaining, Math.max(0, 100 - toPercentage(src.percentage)));
const used = toNumber(src.currentValue, Math.max(0, total - remaining));
const remainingPercentage =
total > 0 ? Math.max(0, Math.min(100, Math.round((remaining / total) * 100))) : 0;
quotas["mcp_monthly"] = {
used,
total,
remaining,
remainingPercentage,
resetAt,
unlimited: false,
displayName: "Monthly",
details: Array.isArray(src.usageDetails)
? src.usageDetails.map((item) => {
const detail = toRecord(item);
return {
name: String(detail.modelCode || detail.name || "usage"),
used: toNumber(detail.usage, 0),
};
})
: undefined,
};
}
}
const levelRaw = typeof data.level === "string" ? data.level : "";
const plan = levelRaw
? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase()
: "Unknown";
const levelRaw =
typeof data.planName === "string"
? data.planName
: typeof data.level === "string"
? data.level
: "";
const plan = levelRaw ? toTitleCase(levelRaw.replace(/\s*plan$/i, "")) : null;
return { plan, quotas: orderGlmQuotas(quotas) };
}
@@ -807,9 +875,12 @@ export async function getUsageForProvider(connection, options: { forceRefresh?:
case "qoder":
return await getQoderUsage(accessToken);
case "glm":
case "zai":
case "glm-cn":
case "glmt":
return await getGlmUsage(apiKey, providerSpecificData);
return await getGlmUsage(apiKey, {
...(providerSpecificData || {}),
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
});
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey, provider);

View File

@@ -89,8 +89,8 @@ export const DEFAULT_SAFETY_SETTINGS = [
];
// Convert OpenAI content to Gemini parts
export function convertOpenAIContentToParts(content) {
const parts = [];
export function convertOpenAIContentToParts(content: any) {
const parts: any[] = [];
if (typeof content === "string") {
parts.push({ text: content });

View File

@@ -12,7 +12,7 @@ import { createRequire } from "module";
const CACHE_TTL_MS = 60 * 60 * 1000;
const DB_KEY = "cursorupdate.lastUpdatedAndShown.version";
const FALLBACK_VERSION = "3.2.14";
const FALLBACK_VERSION = "3.3";
let cachedVersion: string | null = null;
let cachedAt = 0;

View File

@@ -7,7 +7,7 @@ type StreamReadinessLogger = {
export type StreamReadinessResult =
| { ok: true; response: Response }
| { ok: false; response: Response; reason: string };
| { ok: false; response: Response; reason: string; code: string; type: string };
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
@@ -88,13 +88,18 @@ export function hasUsefulStreamContent(text: string): boolean {
return false;
}
function createErrorResponse(status: number, message: string): Response {
function createErrorResponse(
status: number,
message: string,
code: string,
type: string
): Response {
return new Response(
JSON.stringify({
error: {
message,
type: "stream_timeout",
code: "STREAM_READINESS_TIMEOUT",
type,
code,
},
}),
{ status, headers: { "Content-Type": "application/json" } }
@@ -183,7 +188,14 @@ export async function ensureStreamReadiness(
return {
ok: false,
reason,
response: createErrorResponse(HTTP_STATUS.GATEWAY_TIMEOUT, reason),
code: "STREAM_READINESS_TIMEOUT",
type: "stream_timeout",
response: createErrorResponse(
HTTP_STATUS.GATEWAY_TIMEOUT,
reason,
"STREAM_READINESS_TIMEOUT",
"stream_timeout"
),
};
}
@@ -200,7 +212,14 @@ export async function ensureStreamReadiness(
return {
ok: false,
reason,
response: createErrorResponse(HTTP_STATUS.GATEWAY_TIMEOUT, reason),
code: "STREAM_READINESS_TIMEOUT",
type: "stream_timeout",
response: createErrorResponse(
HTTP_STATUS.GATEWAY_TIMEOUT,
reason,
"STREAM_READINESS_TIMEOUT",
"stream_timeout"
),
};
}
@@ -213,7 +232,14 @@ export async function ensureStreamReadiness(
return {
ok: false,
reason,
response: createErrorResponse(HTTP_STATUS.BAD_GATEWAY, reason),
code: "STREAM_EARLY_EOF",
type: "stream_early_eof",
response: createErrorResponse(
HTTP_STATUS.BAD_GATEWAY,
reason,
"STREAM_EARLY_EOF",
"stream_early_eof"
),
};
}

View File

@@ -107,12 +107,19 @@ function parseEnvFile(filePath) {
const eqIdx = trimmed.indexOf("=");
if (eqIdx < 1) continue;
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
const val = unquoteEnvValue(trimmed.slice(eqIdx + 1).trim());
env[key] = val;
}
return env;
}
function unquoteEnvValue(value) {
if (value.length < 2) return value;
const quote = value[0];
if ((quote !== '"' && quote !== "'") || value[value.length - 1] !== quote) return value;
return value.slice(1, -1);
}
// ── Write a simple KEY=VALUE env file ───────────────────────────────────────
function writeEnvFile(filePath, env) {
const lines = [

View File

@@ -111,10 +111,17 @@ function parseEnvEntry(line) {
if (eqIndex < 1) return null;
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1).trim();
const value = unquoteEnvValue(trimmed.slice(eqIndex + 1).trim());
return [key, value];
}
function unquoteEnvValue(value) {
if (value.length < 2) return value;
const quote = value[0];
if ((quote !== '"' && quote !== "'") || value[value.length - 1] !== quote) return value;
return value.slice(1, -1);
}
function parseExampleEntries(content, scope = "full") {
const entries = new Map();
const lines = content.split(/\r?\n/);

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;

View File

@@ -24,6 +24,7 @@ const { initTranslators } = await import("../../open-sse/translator/index.ts");
const { clearInflight } = await import("../../open-sse/services/requestDedup.ts");
const { setCliCompatProviders } = await import("../../open-sse/config/cliFingerprints.ts");
const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
const { GEMINI_CLI_VERSION } = await import("../../open-sse/services/geminiCliHeaders.ts");
const { getCircuitBreaker, resetAllCircuitBreakers } =
await import("../../src/shared/utils/circuitBreaker.ts");
const { clearProviderFailure } = await import("../../open-sse/services/accountFallback.ts");
@@ -653,10 +654,10 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
assert.match(call.url, /chatgpt\.com\/backend-api\/codex\/responses$/);
assert.equal(call.headers.Authorization, "Bearer codex-oauth-token");
assert.equal(call.headers.Accept, "text/event-stream");
assert.equal(call.headers.Version, "0.125.0");
assert.equal(call.headers.Version, "0.130.0");
assert.equal(call.headers["Openai-Beta"], "responses=experimental");
assert.equal(call.headers["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(call.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.equal(call.headers["User-Agent"], "codex-cli/0.130.0 (Windows 10.0.26200; x64)");
assert.equal(call.headers["x-codex-window-id"], "conv_codex_fingerprint:0");
assert.ok(call.headers["x-client-request-id"], "expected Codex request id header");
assert.ok(call.headers["x-codex-turn-metadata"], "expected Codex turn metadata header");
@@ -936,7 +937,9 @@ test("chat pipeline sends Gemini CLI OAuth requests with native Cloud Code trans
assert.equal(generateCall.headers.Accept, "application/json");
assert.match(
generateCall.headers["User-Agent"],
/^GeminiCLI\/0\.40\.1\/gemini-3-flash-preview .* google-api-nodejs-client\/9\.15\.1$/
new RegExp(
`^GeminiCLI/${GEMINI_CLI_VERSION.replaceAll(".", "\\.")}/gemini-3-flash-preview .* google-api-nodejs-client/9\\.15\\.1$`
)
);
assert.match(generateCall.headers["X-Goog-Api-Client"], /^gl-node\/\d+\.\d+\.\d+$/);
assert.equal(generateCall.body.project, "fresh-project");

View File

@@ -24,7 +24,7 @@ function computeOldFingerprint(firstUserMessageText: string, version: string): s
}
describe("Anthropic billing header fingerprint (#1638)", () => {
const ccVersion = "2.1.121";
const ccVersion = "2.1.137";
it("should produce the same fingerprint for different messages (stable)", () => {
const fp1 = computeStableFingerprint(ccVersion);

View File

@@ -64,6 +64,23 @@ test("bootstrapEnv prefers ~/.omniroute/.env over server.env", () => {
});
});
test("bootstrapEnv strips matching quotes from env values", () => {
withTempEnv(({ dataDir }) => {
process.env.DATA_DIR = dataDir;
fs.mkdirSync(dataDir, { recursive: true });
fs.writeFileSync(
path.join(dataDir, "server.env"),
'JWT_SECRET="jwt-from-server-env"\nCLAUDE_USER_AGENT="claude-cli/2.1.137 (external, cli)"\n',
"utf8"
);
const env = bootstrapEnv({ quiet: true });
assert.equal(env.JWT_SECRET, "jwt-from-server-env");
assert.equal(env.CLAUDE_USER_AGENT, "claude-cli/2.1.137 (external, cli)");
});
});
test("bootstrapEnv refuses to generate a new key over encrypted data", () => {
withTempEnv(({ dataDir }) => {
process.env.DATA_DIR = dataDir;

View File

@@ -84,12 +84,12 @@ test("CLI fingerprint preserves Codex executor User-Agent and maps legacy Copilo
"codex",
{
Authorization: "Bearer token",
"User-Agent": "codex-cli/0.125.0 (Windows 10.0.26100; x64)",
"User-Agent": "codex-cli/0.130.0 (Windows 10.0.26200; x64)",
},
{ model: "gpt-5.5", messages: [], stream: true }
);
assert.equal(codex.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26100; x64)");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.130.0 (Windows 10.0.26200; x64)");
assert.deepEqual(Object.keys(JSON.parse(codex.bodyString)), ["model", "stream", "messages"]);
const copilot = applyFingerprint(
@@ -106,7 +106,7 @@ test("CLI fingerprint preserves Codex executor User-Agent and maps legacy Copilo
Authorization: "Bearer token",
"Content-Type": "application/json",
"User-Agent":
"GeminiCLI/0.40.1/gemini-2.5-flash (linux; arm64; terminal) google-api-nodejs-client/9.15.1",
"GeminiCLI/0.41.2/gemini-2.5-flash (linux; arm64; terminal) google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "gl-node/22.22.2",
Accept: "*/*",
},

View File

@@ -156,10 +156,10 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for
assert.equal(standardHeaders.Authorization, "Bearer codex-token");
assert.equal(standardHeaders.Accept, "text/event-stream");
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
assert.equal(standardHeaders.Version, "0.125.0");
assert.equal(standardHeaders.Version, "0.130.0");
assert.equal(standardHeaders["Openai-Beta"], "responses=experimental");
assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.130.0 (Windows 10.0.26200; x64)");
assert.equal(compactHeaders.Accept, "application/json");
});
@@ -168,13 +168,13 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
await withEnv(
{
CODEX_CLIENT_VERSION: "0.125.0",
CODEX_CLIENT_VERSION: "0.130.0",
CODEX_USER_AGENT: undefined,
},
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.125.0");
assert.equal(headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.equal(headers.Version, "0.130.0");
assert.equal(headers["User-Agent"], "codex-cli/0.130.0 (Windows 10.0.26200; x64)");
}
);
@@ -185,7 +185,7 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
},
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.125.0");
assert.equal(headers.Version, "0.130.0");
assert.equal(headers["User-Agent"], "custom-codex/9.9.9");
}
);

View File

@@ -371,7 +371,7 @@ test("GeminiCLIExecutor.execute applies CLI fingerprint to the final Cloud Code
assert.equal(finalBody.project, "old-project");
assert.match(finalBody.user_prompt_id, /^agent-/);
assert.match(finalBody.request.session_id, /^-\d+$/);
assert.match(finalCall.headers["User-Agent"], /^GeminiCLI\/0\.40\.1\/gemini-3\.1-pro-preview /);
assert.match(finalCall.headers["User-Agent"], /^GeminiCLI\/0\.41\.2\/gemini-3\.1-pro-preview /);
assert.equal(finalCall.headers.Accept, "*/*");
} finally {
setCliCompatProviders([]);

View File

@@ -0,0 +1,617 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getExecutor } from "../../open-sse/executors/index.ts";
import { GlmExecutor } from "../../open-sse/executors/glm.ts";
function makeSseResponse(lines: string[]): Response {
return new Response(lines.join("\n\n") + "\n\n", {
headers: { "Content-Type": "text/event-stream" },
});
}
test("GlmExecutor normalizes GLM coding and Anthropic URLs without duplicating endpoints", () => {
const executor = new GlmExecutor("glm");
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
}),
"https://api.z.ai/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4/" },
}),
"https://api.z.ai/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: {
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
},
}),
"https://api.z.ai/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: {
baseUrl: "https://proxy.example.com/api/coding/paas/v4/v1/messages",
},
}),
"https://proxy.example.com/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic" },
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1" },
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(
new GlmExecutor("glm-cn").buildUrl("glm-5.1", true, 0, {
providerSpecificData: {
anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic/v1",
primaryTransport: "anthropic",
},
}),
"https://open.bigmodel.cn/api/anthropic/v1/messages?beta=true"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 1, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic" },
}),
"https://api.z.ai/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 1, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
}),
"https://api.z.ai/api/coding/paas/v4/chat/completions"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages?beta=true",
},
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(
executor.buildCountTokensUrl("glm-5.1", {
providerSpecificData: {
baseUrl: "https://proxy.example.com/api/anthropic/v1/messages/count_tokens",
},
}),
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?beta=true"
);
assert.equal(
executor.buildUrl("glm-5.1", true, 0, {
providerSpecificData: {
baseUrl:
"https://proxy.example.com/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm",
},
}),
"https://proxy.example.com/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm"
);
assert.equal(
executor.buildCountTokensUrl("glm-5.1", {
providerSpecificData: {
anthropicBaseUrl:
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?tenant=alpha&route=glm",
},
}),
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?tenant=alpha&route=glm&beta=true"
);
});
test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", () => {
assert.equal(getExecutor("glm") instanceof GlmExecutor, true);
assert.equal(getExecutor("glm-cn") instanceof GlmExecutor, true);
assert.equal(getExecutor("glmt") instanceof GlmExecutor, true);
const executor = new GlmExecutor("glm");
const codingHeaders = executor.buildHeaders(
{
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
true
);
assert.equal(codingHeaders.Authorization, "Bearer glm-key");
assert.equal(codingHeaders["x-api-key"], undefined);
assert.equal(codingHeaders["anthropic-version"], undefined);
assert.equal(codingHeaders["anthropic-beta"], undefined);
assert.equal(codingHeaders["anthropic-dangerous-direct-browser-access"], undefined);
assert.equal(codingHeaders.Accept, "text/event-stream");
const countTokensHeaders = executor.buildHeaders(
{
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
false,
null,
undefined,
"anthropic"
);
assert.equal(countTokensHeaders["x-api-key"], "glm-key");
assert.equal(countTokensHeaders.Authorization, undefined);
assert.equal(countTokensHeaders["anthropic-version"], "2023-06-01");
const anthropicHeaders = executor.buildHeaders(
{
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
},
true,
null,
undefined,
"anthropic"
);
assert.equal(anthropicHeaders["x-api-key"], "glm-key");
assert.equal(anthropicHeaders.Authorization, undefined);
assert.equal(anthropicHeaders.Accept, "text/event-stream");
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/);
assert.equal(anthropicHeaders["anthropic-dangerous-direct-browser-access"], "true");
assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.137 \(external, sdk-cli\)$/);
assert.equal(anthropicHeaders["X-Stainless-Lang"], "js");
assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node");
});
test("GlmExecutor preserves extra API key rotation", () => {
const executor = new GlmExecutor("glm");
const headers = executor.buildHeaders(
{
apiKey: "primary-key",
connectionId: "glm-rotation-test",
providerSpecificData: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
extraApiKeys: ["extra-key"],
},
},
true,
null,
undefined,
"anthropic"
);
assert.ok(["primary-key", "extra-key"].includes(headers["x-api-key"]));
assert.equal(headers.Authorization, undefined);
});
test("GlmExecutor applies GLMT adaptive thinking defaults without mutating caller body", () => {
const executor = new GlmExecutor("glmt");
const body = { messages: [{ role: "user", content: "hi" }] };
const transformed = executor.transformRequest("glm-5.1", body, true, {
apiKey: "glm-key",
}) as any;
assert.notEqual(transformed, body);
assert.equal((body as any).max_tokens, undefined);
assert.equal(transformed.max_tokens, 65_536);
assert.equal(transformed.temperature, 0.2);
assert.deepEqual(transformed.thinking, { type: "adaptive", budget_tokens: 24_576 });
});
test("GlmExecutor applies conservative GLM defaults without mutating caller body", () => {
const executor = new GlmExecutor("glm");
const body = { messages: [{ role: "user", content: "hi" }] };
const transformed = executor.transformRequest("glm-5.1", body, false, {
apiKey: "glm-key",
}) as any;
assert.notEqual(transformed, body);
assert.equal((body as any).max_tokens, undefined);
assert.equal(transformed.max_tokens, 16_384);
assert.equal(transformed.temperature, undefined);
assert.equal(transformed.thinking, undefined);
});
test("GlmExecutor preserves caller max token settings over GLM defaults", () => {
const executor = new GlmExecutor("glm");
const body = {
messages: [{ role: "user", content: "hi" }],
max_output_tokens: 512,
};
const transformed = executor.transformRequest("glm-5.1", body, false, {
apiKey: "glm-key",
}) as any;
assert.deepEqual(transformed, body);
assert.equal((transformed as any).max_tokens, undefined);
assert.equal((transformed as any).max_output_tokens, 512);
});
test("GlmExecutor count_tokens is best-effort and timeout bounded", async () => {
const executor = new GlmExecutor("glm");
assert.equal(
executor.buildCountTokensUrl("glm-5.1", {
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
}),
"https://api.z.ai/api/anthropic/v1/messages/count_tokens?beta=true"
);
assert.equal(executor.getCountTokensTimeoutMs(), 3_000);
const originalFetch = globalThis.fetch;
let captured: { url: string; body: any; headers: any } | null = null;
globalThis.fetch = async (url, init: RequestInit = {}) => {
captured = {
url: String(url),
body: JSON.parse(String(init.body || "{}")),
headers: init.headers,
};
return Response.json({ input_tokens: 42 });
};
try {
const result = await executor.countTokens({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
credentials: {
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.equal(result?.input_tokens, 42);
assert.ok(captured);
assert.equal(captured.url, "https://api.z.ai/api/anthropic/v1/messages/count_tokens?beta=true");
assert.equal(captured.body.model, "glm-5.1");
assert.equal(captured.headers["x-api-key"], "glm-key");
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor translates Anthropic streaming fallback to OpenAI SSE", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
return new Response(
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"glm-5.1","content":[],"stop_reason":null,"usage":{"input_tokens":1,"output_tokens":0}}}\n\nevent: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}\n\nevent: message_stop\ndata: {"type":"message_stop"}\n\n',
{
headers: { "Content-Type": "text/event-stream" },
}
);
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
stream: true,
credentials: {
apiKey: "glm-key",
providerSpecificData: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
primaryTransport: "anthropic",
},
},
});
assert.equal(result.targetFormat, "openai");
assert.equal(result.response.headers.get("content-type"), "text/event-stream");
const text = await result.response.text();
assert.match(text, /chat\.completion\.chunk/);
assert.doesNotMatch(text, /message_start/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor sends OpenAI coding payload first and enables streaming tool chunks", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
let captured: { url: string; body: any; headers: any } | null = null;
globalThis.fetch = async (url, init: RequestInit = {}) => {
captured = {
url: String(url),
body: JSON.parse(String(init.body || "{}")),
headers: init.headers,
};
return makeSseResponse([
'data: {"id":"chatcmpl-glm","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}',
"data: [DONE]",
]);
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: {
messages: [{ role: "user", content: "weather" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
parameters: { type: "object", properties: {} },
},
},
],
},
stream: true,
credentials: {
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.equal(result.response.status, 200);
assert.equal(captured?.url, "https://api.z.ai/api/coding/paas/v4/chat/completions");
assert.equal(captured?.headers.Authorization, "Bearer glm-key");
assert.equal(captured?.headers["x-api-key"], undefined);
assert.equal(captured?.headers["anthropic-version"], undefined);
assert.equal(captured?.body.tool_stream, true);
assert.equal(captured?.body.tools[0].function.name, "get_weather");
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor falls back internally to Anthropic transport and returns OpenAI JSON", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; body: any; headers: any }> = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
calls.push({
url: String(url),
body: JSON.parse(String(init.body || "{}")),
headers: init.headers,
});
if (calls.length === 1) {
return new Response(JSON.stringify({ error: "not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return Response.json({
id: "msg_glm",
type: "message",
role: "assistant",
model: "glm-5.1",
content: [{ type: "text", text: "fallback ok" }],
stop_reason: "end_turn",
usage: { input_tokens: 3, output_tokens: 2 },
});
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials: {
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.equal(calls.length, 2);
assert.equal(calls[0].url, "https://api.z.ai/api/coding/paas/v4/chat/completions");
assert.equal(calls[0].headers.Authorization, "Bearer glm-key");
assert.equal(calls[1].url, "https://api.z.ai/api/anthropic/v1/messages?beta=true");
assert.equal(calls[1].headers["x-api-key"], "glm-key");
assert.equal(calls[1].headers.Authorization, undefined);
assert.equal(calls[1].body.messages[0].role, "user");
assert.equal(calls[1].body._disableToolPrefix, undefined);
assert.equal(result.targetFormat, "openai");
const json = await result.response.json();
assert.equal(json.object, "chat.completion");
assert.equal(json.choices[0].message.content, "fallback ok");
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor falls back when primary stream ends before useful content", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = async (url) => {
calls.push(String(url));
if (calls.length === 1) {
return makeSseResponse(["event: ping", "data: {}"]);
}
return makeSseResponse([
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"glm-5.1","content":[],"stop_reason":null,"usage":{"input_tokens":1,"output_tokens":0}}}',
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"fallback stream ok"}}',
'event: message_stop\ndata: {"type":"message_stop"}',
]);
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
stream: true,
credentials: {
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.deepEqual(calls, [
"https://api.z.ai/api/coding/paas/v4/chat/completions",
"https://api.z.ai/api/anthropic/v1/messages?beta=true",
]);
assert.equal(result.response.status, 200);
assert.equal(result.targetFormat, "openai");
const text = await result.response.text();
assert.match(text, /chat\.completion\.chunk/);
assert.match(text, /fallback stream ok/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor preserves non-OK streaming upstream status before readiness", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = async (url) => {
calls.push(String(url));
return new Response(JSON.stringify({ error: "invalid api key" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
stream: true,
credentials: {
apiKey: "bad-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.deepEqual(calls, ["https://api.z.ai/api/coding/paas/v4/chat/completions"]);
assert.equal(result.response.status, 401);
assert.match(await result.response.text(), /invalid api key/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor translates Anthropic JSON errors to OpenAI-shaped fallback responses", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
return new Response(
JSON.stringify({
type: "error",
error: { type: "invalid_request_error", message: "bad anthropic fallback" },
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials: {
apiKey: "glm-key",
providerSpecificData: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
primaryTransport: "anthropic",
},
},
});
assert.equal(result.targetFormat, "openai");
assert.equal(result.response.status, 400);
const json = await result.response.json();
assert.equal(json.error.message, "bad anthropic fallback");
} finally {
globalThis.fetch = originalFetch;
}
});
test("GlmExecutor Anthropic fallback keeps tool names unprefixed", async () => {
const executor = new GlmExecutor("glm");
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; body: any; headers: any }> = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
calls.push({
url: String(url),
body: JSON.parse(String(init.body || "{}")),
headers: init.headers,
});
if (calls.length === 1) return new Response("upstream down", { status: 502 });
return Response.json({
id: "msg_tool",
type: "message",
role: "assistant",
model: "glm-5.1",
content: [
{ type: "tool_use", id: "toolu_1", name: "get_weather", input: { location: "Madrid" } },
],
stop_reason: "tool_use",
usage: { input_tokens: 4, output_tokens: 1 },
});
};
try {
const result = await executor.execute({
model: "glm-5.1",
body: {
messages: [{ role: "user", content: "weather" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
parameters: { type: "object", properties: {} },
},
},
],
},
stream: false,
credentials: {
apiKey: "glm-key",
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
},
});
assert.equal(calls.length, 2);
assert.equal(calls[1].body.tools[0].name, "get_weather");
assert.equal(calls[1].body.tools[0].name.startsWith("proxy_"), false);
assert.equal(calls[1].body._disableToolPrefix, undefined);
const json = await result.response.json();
assert.equal(json.choices[0].finish_reason, "tool_calls");
assert.equal(json.choices[0].message.tool_calls[0].function.name, "get_weather");
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -58,6 +58,142 @@ test("GLM import uses international coding endpoint when apiRegion is internatio
}
});
test("GLM import normalizes custom coding models URLs without duplicating endpoints", async () => {
await resetStorage();
const cases = [
{
baseUrl: "https://api.z.ai/api/coding/paas/v4",
expectedUrl: "https://api.z.ai/api/coding/paas/v4/models",
},
{
baseUrl: "https://api.z.ai/api/coding/paas/v4/models",
expectedUrl: "https://api.z.ai/api/coding/paas/v4/models",
},
];
const originalFetch = globalThis.fetch;
const seenUrls: string[] = [];
const connections = [];
for (const [index, testCase] of cases.entries()) {
connections.push(
await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: `glm-custom-${index}`,
apiKey: "glm-key",
providerSpecificData: { baseUrl: testCase.baseUrl },
})
);
}
globalThis.fetch = async (url, init = {}) => {
const expected = cases[seenUrls.length];
assert.ok(expected, `unexpected GLM discovery call to ${String(url)}`);
assert.equal(String(url), expected.expectedUrl);
assert.equal(init.headers.Authorization, "Bearer glm-key");
assert.equal(init.headers["x-api-key"], undefined);
assert.equal(init.headers["anthropic-version"], undefined);
seenUrls.push(String(url));
return Response.json({ data: [{ id: "glm-5", name: "GLM 5" }] });
};
try {
for (const connection of connections) {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 200);
}
assert.deepEqual(
seenUrls,
cases.map((testCase) => testCase.expectedUrl)
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GLM import falls back to Anthropic model discovery when coding discovery fails", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: "glm-discovery-fallback",
apiKey: "glm-key",
providerSpecificData: { apiRegion: "international" },
});
const originalFetch = globalThis.fetch;
const seenUrls: string[] = [];
globalThis.fetch = async (url, init = {}) => {
seenUrls.push(String(url));
if (seenUrls.length === 1) {
assert.equal(String(url), "https://api.z.ai/api/coding/paas/v4/models");
assert.equal(init.headers.Authorization, "Bearer glm-key");
assert.equal(init.headers["x-api-key"], undefined);
return new Response(JSON.stringify({ error: "bad gateway" }), { status: 502 });
}
assert.equal(String(url), "https://api.z.ai/api/anthropic/v1/models");
assert.equal(init.headers.Authorization, undefined);
assert.equal(init.headers["x-api-key"], "glm-key");
assert.equal(init.headers["anthropic-version"], "2023-06-01");
return Response.json({ data: [{ id: "glm-5.1", name: "GLM 5.1" }] });
};
try {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
provider: "glm",
connectionId: connection.id,
models: [{ id: "glm-5.1", name: "GLM 5.1" }],
source: "api",
});
assert.deepEqual(seenUrls, [
"https://api.z.ai/api/coding/paas/v4/models",
"https://api.z.ai/api/anthropic/v1/models",
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GLM import preserves auth failures instead of falling back across transports", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: "glm-auth-fail",
apiKey: "bad-key",
providerSpecificData: { apiRegion: "international" },
});
const originalFetch = globalThis.fetch;
const seenUrls: string[] = [];
globalThis.fetch = async (url) => {
seenUrls.push(String(url));
return new Response(JSON.stringify({ error: "invalid api key" }), { status: 401 });
};
try {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 401);
assert.deepEqual(seenUrls, ["https://api.z.ai/api/coding/paas/v4/models"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GLMT import shares the GLM coding models endpoint and surfaces provider metadata correctly", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
@@ -123,6 +259,38 @@ test("GLM import uses China coding endpoint when apiRegion is china", async () =
}
});
test("GLM China provider import uses the specialized GLM discovery path", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "glm-cn",
authType: "apikey",
name: "glm-cn-provider",
apiKey: "glm-cn-key",
providerSpecificData: {},
});
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, init = {}) => {
assert.equal(String(url), "https://open.bigmodel.cn/api/coding/paas/v4/models");
assert.equal(init.headers.Authorization, "Bearer glm-cn-key");
assert.equal(init.headers["x-api-key"], undefined);
return Response.json({ data: [{ id: "glm-5", name: "GLM 5" }] });
};
try {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 200);
const body = (await response.json()) as any;
assert.equal(body.provider, "glm-cn");
assert.deepEqual(body.models, [{ id: "glm-5", name: "GLM 5" }]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GLM import defaults to international endpoint when apiRegion is missing", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({

View File

@@ -26,12 +26,13 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function createCombo(name, model) {
async function createCombo(name, model, overrides = {}) {
return combosDb.createCombo({
name,
models: [{ provider: "openai", model }],
strategy: "priority",
config: { temperature: 0 },
...overrides,
});
}
@@ -181,6 +182,40 @@ test("resolveComboForModel skips corrupted combo payloads and keeps scanning", a
assert.equal(resolved.name, "fallback");
});
test("resolveComboForModel skips inactive mapped combos and keeps scanning", async () => {
const inactiveCombo = await createCombo("inactive", "gpt-4o", { isActive: false });
const fallbackCombo = await createCombo("fallback", "gpt-4o-mini");
await mappingsDb.createModelComboMapping({
pattern: "gpt-4*",
comboId: inactiveCombo.id,
priority: 10,
});
await mappingsDb.createModelComboMapping({
pattern: "gpt-*",
comboId: fallbackCombo.id,
priority: 1,
});
const resolved = await mappingsDb.resolveComboForModel("gpt-4o");
assert.ok(resolved);
assert.equal(resolved.name, "fallback");
});
test("resolveComboForModel returns null when only matching combo is inactive", async () => {
const inactiveCombo = await createCombo("inactive", "gpt-4o", { isActive: false });
await mappingsDb.createModelComboMapping({
pattern: "gpt-4*",
comboId: inactiveCombo.id,
});
const resolved = await mappingsDb.resolveComboForModel("gpt-4o");
assert.equal(resolved, null);
});
test("resolveComboForModel returns null when nothing matches", async () => {
const combo = await createCombo("alpha", "gpt-4o");

View File

@@ -16,6 +16,9 @@ test("paid individual tiers use non-gray badge variants", () => {
assert.equal(providerLimitUtils.normalizePlanTier("Plus").variant, "success");
assert.equal(providerLimitUtils.normalizePlanTier("Pro").variant, "success");
assert.equal(providerLimitUtils.normalizePlanTier("Student").variant, "success");
assert.equal(providerLimitUtils.normalizePlanTier("Lite").key, "lite");
assert.equal(providerLimitUtils.normalizePlanTier("Lite").label, "Lite");
assert.notEqual(providerLimitUtils.normalizePlanTier("Lite").variant, "default");
assert.equal(providerLimitUtils.normalizePlanTier("Free").variant, "default");
});
@@ -64,6 +67,7 @@ test("quota labels normalize session and weekly windows while preserving readabl
assert.equal(providerLimitUtils.formatQuotaLabel("weekly (7d)"), "Weekly");
assert.equal(providerLimitUtils.formatQuotaLabel("weekly sonnet (7d)"), "Weekly Sonnet");
assert.equal(providerLimitUtils.formatQuotaLabel("code_review"), "Code Review");
assert.equal(providerLimitUtils.formatQuotaLabel("mcp_monthly"), "Monthly");
});
test("MiniMax providers are exposed to the limits dashboard support list", () => {
@@ -106,43 +110,17 @@ test("MiniMax quota payloads use generic provider parsing and stale resets still
assert.equal(providerLimitUtils.formatQuotaLabel(parsed[1].name), "Weekly");
});
test("Z.AI quota labels render 5h, weekly and monthly tool usage", () => {
assert.equal(providerLimitUtils.formatQuotaLabel("5 Hours Quota"), "5 Hours");
assert.equal(providerLimitUtils.formatQuotaLabel("Weekly Quota"), "Weekly");
assert.equal(providerLimitUtils.formatQuotaLabel("Monthly Tools"), "Monthly Tools");
const future = new Date(Date.now() + 5 * 60_000).toISOString();
const parsed = providerLimitUtils.parseQuotaData("zai", {
test("GLM quota rows are ordered by session, weekly, then monthly", () => {
const parsed = providerLimitUtils.parseQuotaData("glm", {
quotas: {
"5 Hours Quota": {
used: 15,
total: 100,
remaining: 85,
remainingPercentage: 85,
resetAt: future,
},
"Weekly Quota": {
used: 4,
total: 100,
remaining: 96,
remainingPercentage: 96,
resetAt: future,
},
"Monthly Tools": {
used: 0,
total: 100,
remaining: 100,
remainingPercentage: 100,
resetAt: future,
},
mcp_monthly: { used: 10, total: 100, remainingPercentage: 90 },
weekly: { used: 20, total: 100, remainingPercentage: 80 },
session: { used: 30, total: 100, remainingPercentage: 70 },
},
});
assert.equal(parsed.length, 3);
assert.equal(parsed[0].name, "5 Hours Quota");
assert.equal(parsed[0].remainingPercentage, 85);
assert.equal(parsed[1].name, "Weekly Quota");
assert.equal(parsed[1].remainingPercentage, 96);
assert.equal(parsed[2].name, "Monthly Tools");
assert.equal(parsed[2].remainingPercentage, 100);
assert.deepEqual(
parsed.map((quota) => quota.name),
["session", "weekly", "mcp_monthly"]
);
});

View File

@@ -25,6 +25,7 @@ function writeEnvExample(rootDir: string) {
"MACHINE_ID_SALT=",
"CLAUDE_OAUTH_CLIENT_ID=claude-default",
"CODEX_OAUTH_CLIENT_ID=codex-default",
'CLAUDE_USER_AGENT="claude-cli/2.1.137 (external, cli)"',
"# COMMENTED_KEY=skip-me",
"",
].join("\n"),
@@ -64,13 +65,14 @@ test("syncEnv creates .env from .env.example and generates blank secrets", () =>
const result = syncEnv({ rootDir, quiet: true });
const envContent = fs.readFileSync(path.join(rootDir, ".env"), "utf8");
assert.deepEqual(result, { created: true, added: 6 });
assert.deepEqual(result, { created: true, added: 7 });
assert.match(envContent, /^JWT_SECRET=.{32,}$/m);
assert.match(envContent, /^API_KEY_SECRET=.{32,}$/m);
assert.match(envContent, /^STORAGE_ENCRYPTION_KEY=.{32,}$/m);
assert.match(envContent, /^MACHINE_ID_SALT=omniroute-/m);
assert.match(envContent, /^CLAUDE_OAUTH_CLIENT_ID=claude-default$/m);
assert.match(envContent, /^CODEX_OAUTH_CLIENT_ID=codex-default$/m);
assert.match(envContent, /^CLAUDE_USER_AGENT="claude-cli\/2\.1\.137 \(external, cli\)"$/m);
assert.doesNotMatch(envContent, /^COMMENTED_KEY=/m);
} finally {
process.env.DATA_DIR = origDataDir;
@@ -98,13 +100,14 @@ test("syncEnv appends only missing keys and preserves existing values", () => {
const result = syncEnv({ rootDir, quiet: true });
const envContent = fs.readFileSync(path.join(rootDir, ".env"), "utf8");
assert.deepEqual(result, { created: false, added: 4 });
assert.deepEqual(result, { created: false, added: 5 });
assert.match(envContent, /^JWT_SECRET=my-custom-secret-that-should-stay$/m);
assert.match(envContent, /^CLAUDE_OAUTH_CLIENT_ID=custom-claude$/m);
assert.match(envContent, /^API_KEY_SECRET=.{32,}$/m);
assert.match(envContent, /^STORAGE_ENCRYPTION_KEY=.{32,}$/m);
assert.match(envContent, /^MACHINE_ID_SALT=omniroute-/m);
assert.match(envContent, /^CODEX_OAUTH_CLIENT_ID=codex-default$/m);
assert.match(envContent, /^CLAUDE_USER_AGENT=claude-cli\/2\.1\.137 \(external, cli\)$/m);
assert.match(envContent, /Auto-added by sync-env/);
} finally {
process.env.DATA_DIR = origDataDir;
@@ -112,6 +115,37 @@ test("syncEnv appends only missing keys and preserves existing values", () => {
}
});
test("syncEnv treats quoted and unquoted values as equivalent", () => {
const rootDir = createTempRoot();
const origDataDir = process.env.DATA_DIR;
try {
writeEnvExample(rootDir);
fs.writeFileSync(
path.join(rootDir, ".env"),
[
"JWT_SECRET=jwt-secret",
"API_KEY_SECRET=api-secret",
"STORAGE_ENCRYPTION_KEY=storage-secret",
"MACHINE_ID_SALT=machine-salt",
"CLAUDE_OAUTH_CLIENT_ID=claude-default",
"CODEX_OAUTH_CLIENT_ID=codex-default",
'CLAUDE_USER_AGENT="claude-cli/2.1.137 (external, cli)"',
"",
].join("\n"),
"utf8"
);
process.env.DATA_DIR = rootDir;
const result = syncEnv({ rootDir, quiet: true });
assert.deepEqual(result, { created: false, added: 0 });
} finally {
process.env.DATA_DIR = origDataDir;
fs.rmSync(rootDir, { recursive: true, force: true });
}
});
test("syncEnv is idempotent when .env is already complete", () => {
const rootDir = createTempRoot();

View File

@@ -13,15 +13,15 @@ test("T20: antigravity config has updated User-Agent and sandbox fallback URL",
assert.equal(antigravity.headers["User-Agent"], antigravityUserAgent());
});
test("T20: gemini CLI fingerprint uses 0.40.1 and normalizes darwin to macos", () => {
assert.equal(GEMINI_CLI_VERSION, "0.40.1");
test("T20: gemini CLI fingerprint uses the current CLI version and normalizes darwin to macos", () => {
assert.equal(GEMINI_CLI_VERSION, "0.41.2");
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: "darwin" });
try {
assert.match(
geminiCliUserAgent("gemini-3-flash"),
/^GeminiCLI\/0\.40\.1\/gemini-3-flash \(macos; .+; terminal\) google-api-nodejs-client\/9\.15\.1$/
/^GeminiCLI\/0\.41\.2\/gemini-3-flash \(macos; .+; terminal\) google-api-nodejs-client\/9\.15\.1$/
);
} finally {
if (descriptor) {
@@ -56,10 +56,10 @@ test("T22: github config exposes dedicated responses endpoint", () => {
test("T20: codex config advertises current client headers and supported models", () => {
const codex = REGISTRY.codex;
assert.equal(codex.headers.Version, "0.125.0");
assert.equal(codex.headers.Version, "0.130.0");
assert.equal(codex.headers["Openai-Beta"], "responses=experimental");
assert.equal(codex.headers["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.125.0 (Windows 10.0.26200; x64)");
assert.equal(codex.headers["User-Agent"], "codex-cli/0.130.0 (Windows 10.0.26200; x64)");
assert.ok(codex.models.some((model) => model.id === "gpt-5.5-medium"));
assert.ok(!codex.models.some((model) => model.id === "codex-auto-review"));
});

View File

@@ -894,25 +894,32 @@ test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () =
data: {
level: "pro",
limits: [
{
type: "TIME_LIMIT",
usage: 1000,
currentValue: 12,
remaining: 988,
percentage: "1.2",
nextResetTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
usageDetails: [
{ modelCode: "search-prime", usage: 5 },
{ modelCode: "web-reader", usage: 7 },
{ modelCode: "zread", usage: 0 },
],
},
{
type: "TOKENS_LIMIT",
unit: 3,
usage: 15,
currentValue: 85,
percentage: "15",
number: 5,
percentage: "64",
nextResetTime: Date.now() + 120_000,
},
{
type: "TOKENS_LIMIT",
unit: 6,
percentage: "64",
nextResetTime: Date.now() + 604_800_000,
},
{
type: "TIME_LIMIT",
unit: 5,
percentage: "7",
nextResetTime: Date.now() + 300_000,
unit: 4,
number: 7,
percentage: "25",
nextResetTime: Date.now() + 7 * 24 * 60 * 60 * 1000,
},
{
type: "OTHER_LIMIT",
@@ -934,21 +941,19 @@ test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () =
providerSpecificData: { apiRegion: "invalid-region" },
});
assert.equal(glm.plan, "Pro");
assert.equal(glm.quotas["5 Hours Quota"].used, 15);
assert.equal(glm.quotas["5 Hours Quota"].remaining, 85);
assert.equal(glm.quotas["Weekly Quota"].used, 64);
assert.equal(glm.quotas["Weekly Quota"].remaining, 36);
assert.equal(glm.quotas["Monthly Tools"].used, 7);
assert.equal(glm.quotas["Monthly Tools"].remaining, 93);
const zai: any = await usageService.getUsageForProvider({
provider: "zai",
apiKey: "glm-key",
});
assert.equal(zai.plan, "Pro");
assert.equal(zai.quotas["5 Hours Quota"].used, 15);
assert.equal(zai.quotas["Weekly Quota"].remaining, 36);
assert.equal(zai.quotas["Monthly Tools"].remaining, 93);
assert.equal(glm.quotas.session.used, 64);
assert.equal(glm.quotas.session.remaining, 36);
assert.equal(glm.quotas.weekly.used, 25);
assert.equal(glm.quotas.weekly.remaining, 75);
assert.equal(glm.quotas.mcp_monthly.used, 12);
assert.equal(glm.quotas.mcp_monthly.remaining, 988);
assert.equal(glm.quotas.mcp_monthly.remainingPercentage, 99);
assert.equal(glm.quotas.mcp_monthly.displayName, "Monthly");
assert.deepEqual(glm.quotas.mcp_monthly.details, [
{ name: "search-prime", used: 5 },
{ name: "web-reader", used: 7 },
{ name: "zread", used: 0 },
]);
const glmt: any = await usageService.getUsageForProvider({
provider: "glmt",
@@ -959,6 +964,28 @@ test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () =
assert.equal(glmt.quotas["5 Hours Quota"].used, 15);
assert.equal(glmt.quotas["Weekly Quota"].remaining, 36);
let glmCnUrl = "";
globalThis.fetch = async (url) => {
glmCnUrl = String(url);
return new Response(
JSON.stringify({
data: {
planName: "Lite Plan",
limits: [{ type: "TOKENS_LIMIT", percentage: "64" }],
},
}),
{ status: 200 }
);
};
const glmCn: any = await usageService.getUsageForProvider({
provider: "glm-cn",
apiKey: "glm-cn-key",
providerSpecificData: { apiRegion: "international" },
});
assert.match(glmCnUrl, /open\.bigmodel\.cn/);
assert.equal(glmCn.plan, "Lite");
assert.equal(glmCn.quotas.session.remaining, 36);
globalThis.fetch = async () => new Response("nope", { status: 401 });
await assert.rejects(
() =>
@@ -1141,8 +1168,8 @@ test("usage service parses Cursor team quotas and clamps on-demand ratio", async
assert.equal(calls.length, 3);
for (const call of calls) {
assert.equal(call.init.headers.Authorization, "Bearer cursor-token");
assert.equal(call.init.headers["User-Agent"], "Cursor/3.2.14");
assert.equal(call.init.headers["x-cursor-client-version"], "3.2.14");
assert.equal(call.init.headers["User-Agent"], "Cursor/3.3");
assert.equal(call.init.headers["x-cursor-client-version"], "3.3");
}
assert.equal(usage.plan, "Cursor Team");
@@ -1256,9 +1283,9 @@ test("usage helper branches cover reset parsing, GitHub quota math, and plan inf
assert.deepEqual(__testing.buildCursorUsageHeaders("cursor-token"), {
Authorization: "Bearer cursor-token",
Accept: "application/json",
"User-Agent": "Cursor/3.2.14",
"x-cursor-client-version": "3.2.14",
"x-cursor-user-agent": "Cursor/3.2.14",
"User-Agent": "Cursor/3.3",
"x-cursor-client-version": "3.3",
"x-cursor-user-agent": "Cursor/3.3",
});
assert.equal(
__testing.getCursorMonthlyRequestLimit(