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_READINESS_TIMEOUT_MS } from "../config/constants.ts"; type JsonRecord = Record; type GlmExecuteResult = Awaited> & { 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 { 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 { 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 | null, _model?: string, transport: GlmTransport = getGlmTransport(credentials.providerSpecificData) ): Record { 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 { 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 | 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_READINESS_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 { 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;