Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const proxyClientMaxBodySize = process.env.NINEROUTER_PROXY_CLIENT_MAX_BODY_SIZE
const nextConfig = {
distDir: process.env.NEXT_DIST_DIR || ".next",
output: "standalone",
serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite"],
serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite", "cloakbrowser", "playwright-core"],
turbopack: {
root: tracingRoot
},
Expand Down
208 changes: 208 additions & 0 deletions open-sse/executors/glm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { DefaultExecutor } from "./default.js";
import zcodeConfig from "../../src/lib/zcode/config.js";
import { GLM_CODING_PLAN_MODEL_MAP } from "../../src/lib/zcode/constants.js";
import { injectZcodeSystemPrompt } from "../../src/lib/zcode/systemPrompt.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import {
getCaptchaManager,
getZcodeCaptchaPort,
isCaptchaError,
} from "../../src/lib/zcode/captcha-service.js";
import {
applyZcodeApiKeyHeaders,
applyZcodeCodingPlanHeaders,
} from "../../src/lib/zcode/headers.js";

const MAX_CAPTCHA_RETRIES = 3;

/** Per-request context — avoids singleton executor cross-request credential races. */
export const glmRequestContext = new AsyncLocalStorage();

function getGlmRequestContext() {
return glmRequestContext.getStore();
}

export class GlmExecutor extends DefaultExecutor {
constructor() {
super("glm");
}

usesCodingPlan(credentials) {
if (!credentials?.providerSpecificData?.useCodingPlan) return false;
return !!(credentials.providerSpecificData?.zcodeJwtToken || credentials.accessToken);
}

usesApiKeyOnly(credentials) {
return !!credentials?.apiKey && !this.usesCodingPlan(credentials);
}

getUrlIndex() {
return getGlmRequestContext()?.urlIndex ?? 0;
}

usesZcodeApiKeyUpstream(credentials) {
const urlIndex = this.getUrlIndex();
return (
this.usesApiKeyOnly(credentials) ||
(this.usesCodingPlan(credentials) && urlIndex === 1)
);
}

canFallbackToApiKey(credentials) {
return this.usesCodingPlan(credentials) && !!credentials?.apiKey;
}

getFallbackCount() {
const credentials = getGlmRequestContext()?.credentials;
if (!credentials) return 1;
return this.canFallbackToApiKey(credentials) ? 2 : 1;
}

buildUrl(model, stream, urlIndex = 0, credentials = null) {
const ctx = getGlmRequestContext();
if (ctx) ctx.urlIndex = urlIndex;

if (this.usesCodingPlan(credentials)) {
if (urlIndex === 0) return zcodeConfig.codingPlanUrl;
if (this.canFallbackToApiKey(credentials)) {
return zcodeConfig.apiKeyFallbackUrl;
}
}
if (this.usesApiKeyOnly(credentials)) {
return zcodeConfig.apiKeyFallbackUrl;
}
return super.buildUrl(model, stream, urlIndex, credentials);
}

transformRequest(model, body, stream, credentials) {
const transformed = super.transformRequest(model, body, stream, credentials);
if (!transformed || typeof transformed !== "object") return transformed;

const modelId = transformed.model || model;
if (typeof modelId !== "string") return transformed;

const mapped =
getModelUpstreamId("glm", modelId) ||
GLM_CODING_PLAN_MODEL_MAP[modelId.toLowerCase()] ||
modelId;

if (mapped !== modelId) {
transformed.model = mapped;
}

if (this.usesCodingPlan(credentials) && this.getUrlIndex() === 0) {
return injectZcodeSystemPrompt(transformed, {
modelRef: `builtin:zai-start-plan/${transformed.model || mapped}`,
});
}

return transformed;
}

buildHeaders(credentials, stream = true) {
const headers = super.buildHeaders(credentials, stream);

if (this.usesCodingPlan(credentials) && this.getUrlIndex() === 0) {
applyZcodeCodingPlanHeaders(headers, credentials);
} else if (this.usesZcodeApiKeyUpstream(credentials)) {
applyZcodeApiKeyHeaders(headers, credentials);
}

return headers;
}

shouldRetry(status, urlIndex) {
const credentials = getGlmRequestContext()?.credentials;
if (
status === 401 &&
urlIndex === 0 &&
credentials &&
this.canFallbackToApiKey(credentials)
) {
return true;
}
return super.shouldRetry(status, urlIndex);
}

parseError(response, bodyText) {
if (!bodyText) {
return super.parseError(response, bodyText);
}

try {
const json = JSON.parse(bodyText);
const err = json?.error;
const code = err?.code ?? json?.code;
const message = err?.message ?? json?.msg ?? json?.message;

if (code === "1113" || (typeof message === "string" && message.includes("1113"))) {
return {
status: response.status || 429,
message:
"GLM quota exhausted or no active resource package for this model. " +
"Check ZCode Start/Coding Plan balance, try glm-5-turbo, or wait for daily reset.",
};
}

if (code === "3010" || (typeof message === "string" && message.includes("concurrency limit"))) {
return {
status: response.status || 429,
message:
"Z.AI model admission concurrency limit exceeded. Close other ZCode/9router sessions and retry.",
};
}

if (typeof message === "string" && message.length > 0) {
return { status: response.status, message };
}
} catch {
/* fall through */
}

return super.parseError(response, bodyText);
}

async executeWithCaptcha(params) {
const { credentials } = params;
const captchaManager = getCaptchaManager();
const port = getZcodeCaptchaPort();

for (let attempt = 1; attempt <= MAX_CAPTCHA_RETRIES; attempt++) {
let verifyParam;
try {
verifyParam = await captchaManager.getVerifyParam(port);
} catch (err) {
throw new Error(`Captcha verification failed: ${err.message}`);
}

const credsWithCaptcha = {
...credentials,
providerSpecificData: {
...(credentials.providerSpecificData || {}),
_captchaVerifyParam: verifyParam,
},
};

const result = await super.execute({ ...params, credentials: credsWithCaptcha });

if (result.response.status === 403 && (await isCaptchaError(result.response))) {
captchaManager.invalidate();
continue;
}

return result;
}

throw new Error("Captcha expired multiple times. Restart the service or check CloakBrowser.");
}

async execute(params) {
const { credentials } = params;
return glmRequestContext.run({ credentials, urlIndex: 0 }, () =>
this.executeWithCaptcha(params)
);
}
}

export default GlmExecutor;
3 changes: 3 additions & 0 deletions open-sse/executors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import { CommandCodeExecutor } from "./commandcode.js";
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { DefaultExecutor } from "./default.js";
import { GlmExecutor } from "./glm.js";

const executors = {
glm: new GlmExecutor(),
antigravity: new AntigravityExecutor(),
azure: new AzureExecutor(),
"gemini-cli": new GeminiCLIExecutor(),
Expand Down Expand Up @@ -77,3 +79,4 @@ export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { GlmExecutor } from "./glm.js";
28 changes: 19 additions & 9 deletions open-sse/providers/registry/glm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ export default {
icon: "code",
color: "#2563EB",
textIcon: "GL",
website: "https://open.bigmodel.cn",
deprecated: true,
deprecationNotice: "RISK_NOTICE",
website: "https://z.ai",
notice: {
apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys",
text: "Z.AI international (z.ai). OAuth Coding Plan or API key via api.z.ai.",
apiKeyUrl: "https://z.ai",
signupUrl: "https://z.ai",
},
},
category: "apikey",
category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
format: "claude",
urlSuffix: "?beta=true",
headers: {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14",
...CLAUDE_API_HEADERS,
},
auth: {
combined: true,
Expand All @@ -33,13 +37,19 @@ export default {
},
},
models: [
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5.2", name: "GLM 5.2", upstreamModelId: "GLM-5.2" },
{ id: "glm-5.1", name: "GLM 5.1", upstreamModelId: "GLM-5.1" },
{ id: "glm-5-turbo", name: "GLM 5 Turbo", upstreamModelId: "GLM-5-Turbo" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "glm-4.7", name: "GLM 4.7", upstreamModelId: "GLM-4.7" },
{ id: "glm-4.6", name: "GLM 4.6" },
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)" },
],
oauth: {
refreshLeadMs: 600000,
},
features: {
usage: true,
usageApikey: true,
},
};
};
2 changes: 2 additions & 0 deletions open-sse/services/tokenRefresh.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PROVIDERS } from "../config/providers.js";
import { OAUTH_ENDPOINTS, REFRESH_LEAD_MS } from "../config/appConstants.js";
import {
refreshXaiToken,
refreshGlmToken,
refreshAccessToken,
refreshClaudeOAuthToken,
refreshGoogleToken,
Expand Down Expand Up @@ -127,6 +128,7 @@ const REFRESH_HANDLERS = {
github: (c, log) => refreshGitHubToken(c.refreshToken, log),
kiro: (c, log) => refreshKiroToken(c.refreshToken, c.providerSpecificData, log),
xai: (c, log) => refreshXaiToken(c.refreshToken, log),
glm: (c, log) => refreshGlmToken(c.refreshToken, c, log),
vertex: vertexRefreshHandler,
"vertex-partner": vertexRefreshHandler
};
Expand Down
30 changes: 30 additions & 0 deletions open-sse/services/tokenRefresh/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,36 @@ export async function refreshGitHubToken(refreshToken, log) {
}, log);
}

export async function refreshGlmToken(refreshToken, credentials, log) {
if (!refreshToken) return null;
return dedupRefresh("glm", refreshToken, async () => {
try {
const mod = await import("../../../src/lib/zcode/oauth.js");
const refreshed = await mod.refreshGlmOAuthCredentials({
...credentials,
refreshToken,
});
if (!refreshed) {
log?.warn?.("TOKEN_REFRESH", "glm refresh skipped (no client secret or refresh token)");
return null;
}
log?.info?.("TOKEN_REFRESH", "Successfully refreshed GLM OAuth credentials", {
hasNewAccessToken: !!refreshed.accessToken,
hasNewApiKey: !!refreshed.apiKey,
expiresIn: refreshed.expiresIn,
});
return refreshed;
} catch (e) {
log?.warn?.("TOKEN_REFRESH", `glm refresh failed: ${e?.message || e}`);
const msg = String(e?.message || "");
if (msg.includes("invalid_grant") || msg.includes("invalid_request")) {
return { error: "invalid_grant" };
}
return null;
}
}, log);
}

export async function refreshCopilotToken(githubAccessToken, log) {
if (!githubAccessToken) return null;
return dedupRefresh("copilot", githubAccessToken, async () => {
Expand Down
4 changes: 2 additions & 2 deletions open-sse/services/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const USAGE_HANDLERS = {
qwen: (c) => getQwenUsage(c.accessToken, c.providerSpecificData),
iflow: (c) => getIflowUsage(c.accessToken),
ollama: (c) => getOllamaUsage(c.accessToken),
glm: (c) => getGlmUsage(c.apiKey, c.provider, c.proxyOptions),
"glm-cn": (c) => getGlmUsage(c.apiKey, c.provider, c.proxyOptions),
glm: (c) => getGlmUsage(c, c.proxyOptions),
"glm-cn": (c) => getGlmUsage(c, c.proxyOptions),
minimax: (c) => getMiniMaxUsage(c.apiKey, c.provider, c.proxyOptions),
"minimax-cn": (c) => getMiniMaxUsage(c.apiKey, c.provider, c.proxyOptions),
"vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions),
Expand Down
Loading