Files
2026-07-13 13:39:12 +08:00

489 lines
14 KiB
TypeScript

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_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.2",
name: "GLM 5.2",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5.2-high",
name: "GLM 5.2 High",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5.2-max",
name: "GLM 5.2 Max",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
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({
international: "https://api.z.ai/api/coding/paas/v4/models",
china: "https://open.bigmodel.cn/api/coding/paas/v4/models",
});
export const GLM_QUOTA_URLS = Object.freeze({
international: "https://api.z.ai/api/monitor/usage/quota/limit",
china: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
});
export const GLMT_TIMEOUT_MS = 900_000;
export const GLM_TIMEOUT_MS = 3_000_000; // 50 min — aligned with Z.AI Coding Plan FAQ (API_TIMEOUT_MS=3000000)
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.195 (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 idx = url.search(/[?#]/);
if (idx === -1) return { base: url, suffix: "" };
return { base: url.substring(0, idx), suffix: url.substring(idx) };
}
export function getGlmApiRegion(providerSpecificData: unknown): GlmApiRegion {
const data = asRecord(providerSpecificData);
return data.apiRegion === "china" ? "china" : "international";
}
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 getProviderSpecificString(data: JsonRecord, keys: readonly string[]): string | null {
for (const key of keys) {
const value = asString(data[key]);
if (value) return value;
}
return null;
}
export const GLM_TEAM_QUOTA_ORGANIZATION_KEYS = [
"glmOrganizationId",
"bigmodelOrganization",
"glmOrganization",
] as const;
export const GLM_TEAM_QUOTA_PROJECT_KEYS = [
"glmProjectId",
"bigmodelProject",
"glmProject",
] as const;
export const GLM_TEAM_QUOTA_ALIAS_KEYS = [
"bigmodelOrganization",
"glmOrganization",
"bigmodelProject",
"glmProject",
] as const;
export type GlmTeamQuotaConfig =
| { state: "none" }
| { state: "configured"; organizationId: string; projectId: string }
| { state: "incomplete"; missing: "glmOrganizationId" | "glmProjectId" };
export function getGlmTeamQuotaConfig(providerSpecificData: unknown): GlmTeamQuotaConfig {
const data = asRecord(providerSpecificData);
const organizationId = getProviderSpecificString(data, GLM_TEAM_QUOTA_ORGANIZATION_KEYS);
const projectId = getProviderSpecificString(data, GLM_TEAM_QUOTA_PROJECT_KEYS);
if (!organizationId && !projectId) return { state: "none" };
if (organizationId && projectId) {
return { state: "configured", organizationId, projectId };
}
return {
state: "incomplete",
missing: organizationId ? "glmProjectId" : "glmOrganizationId",
};
}
export function buildGlmQuotaFetch(
apiKey: string,
providerSpecificData?: unknown
): { url: string; headers: Record<string, string> } {
const teamConfig = getGlmTeamQuotaConfig(providerSpecificData);
const baseUrl = getGlmQuotaUrl(providerSpecificData);
const url =
teamConfig.state === "configured"
? baseUrl.includes("?")
? `${baseUrl}&type=2`
: `${baseUrl}?type=2`
: baseUrl;
const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
};
if (teamConfig.state === "configured") {
headers["bigmodel-organization"] = teamConfig.organizationId;
headers["bigmodel-project"] = teamConfig.projectId;
}
return { url, headers };
}
function stripKnownGlmEndpointSuffix(baseUrl: string): { base: string; suffix: string } {
const parts = splitUrlQueryAndHash(baseUrl);
let base = parts.base;
while (base.endsWith("/")) {
base = base.slice(0, -1);
}
const countTokensMatch = base.match(/\/(?:v\d+\/)?messages\/count_tokens$/i);
if (countTokensMatch) {
base = base.substring(0, base.length - countTokensMatch[0].length);
} else {
const messagesMatch = base.match(/\/(?:v\d+\/)?messages$/i);
if (messagesMatch) {
base = base.substring(0, base.length - messagesMatch[0].length);
} else if (base.toLowerCase().endsWith("/chat/completions")) {
base = base.substring(0, base.length - "/chat/completions".length);
} else if (base.toLowerCase().endsWith("/models")) {
base = base.substring(0, base.length - "/models".length);
}
}
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 {
let base = splitUrlQueryAndHash(baseUrl).base;
while (base.endsWith("/")) {
base = base.slice(0, -1);
}
return base;
}
function addBetaQuery(url: string): string {
const parsed = new URL(url);
parsed.searchParams.set("beta", "true");
return parsed.toString();
}
export function isAnthropicGlmBaseUrl(baseUrl: string): boolean {
const base = stripQueryAndTrailingSlash(baseUrl).toLowerCase();
return base.includes("/api/anthropic/") || base.endsWith("/api/anthropic");
}
export function isCodingGlmBaseUrl(baseUrl: string): boolean {
const base = stripQueryAndTrailingSlash(baseUrl).toLowerCase();
const idx = base.indexOf("/api/coding/paas/v");
if (idx === -1) return false;
const afterV = base.charCodeAt(idx + "/api/coding/paas/v".length);
return afterV >= 48 && afterV <= 57; // first char after 'v' must be a digit
}
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",
};
}