From 5d48ef58804768753a3d790115e51ad833b0a6a9 Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Wed, 8 Jul 2026 10:08:25 +0800 Subject: [PATCH] fix: parse escaped retryable error messages --- gateway.mjs | 9 ++++++- scripts/admin-lib.mjs | 9 ++++++- scripts/run-profile.mjs | 14 +++++++---- scripts/test-gateway-e2e.mjs | 47 ++++++++++++++++++++++++++++++------ 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/gateway.mjs b/gateway.mjs index fe767ab..a7095e0 100644 --- a/gateway.mjs +++ b/gateway.mjs @@ -169,6 +169,13 @@ function normalizePath(inputPath) { return trimmed || "/"; } +function expandEscapedLineBreaks(value) { + return `${value ?? ""}` + .replace(/\\r\\n/g, "\n") + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\n"); +} + function flattenValues(value) { if (Array.isArray(value)) { return value.flatMap((item) => flattenValues(item)); @@ -503,7 +510,7 @@ function normalizePhraseList(values, fallback = []) { const normalized = flattenValues(source) .flatMap((value) => { if (typeof value === "string") { - return value.split(/\r?\n/); + return expandEscapedLineBreaks(value).split(/\r?\n/); } return [value]; }) diff --git a/scripts/admin-lib.mjs b/scripts/admin-lib.mjs index 64b95f3..bb8c92c 100644 --- a/scripts/admin-lib.mjs +++ b/scripts/admin-lib.mjs @@ -15,6 +15,13 @@ export const DEFAULT_LISTEN_PORT = 4610; export const DEFAULT_HEALTH_PATH = "/__codex_retry_gateway/health"; export const DEFAULT_REASONING_MATCH_MODE = "formula_518n_minus_2"; export const DEFAULT_REASONING_EQUALS = [516, 1034, 1552]; + +export function expandEscapedLineBreaks(value) { + return `${value ?? ""}` + .replace(/\\r\\n/g, "\n") + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\n"); +} function escapeRegExp(value) { return `${value}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -204,7 +211,7 @@ export function normalizePhraseArray(values, fallback = []) { const source = values === undefined || values === null ? fallback : values; const queue = Array.isArray(source) ? source.flat(Infinity) : [source]; const normalized = queue - .flatMap((value) => (typeof value === "string" ? value.split(/\r?\n/) : [value])) + .flatMap((value) => (typeof value === "string" ? expandEscapedLineBreaks(value).split(/\r?\n/) : [value])) .map((value) => `${value ?? ""}`.trim()) .filter(Boolean); diff --git a/scripts/run-profile.mjs b/scripts/run-profile.mjs index cdf5230..99f6da3 100644 --- a/scripts/run-profile.mjs +++ b/scripts/run-profile.mjs @@ -15,6 +15,7 @@ import { DEFAULT_REASONING_MATCH_MODE, DEFAULT_STATE_ROOT, ensureDirectory, + expandEscapedLineBreaks, getCodexProviderContext, getGatewayBaseUrl, getGatewayStatePaths, @@ -47,13 +48,16 @@ function parseEnvFile(content) { } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { + if (value.startsWith('"') && value.endsWith('"')) { + try { + value = JSON.parse(value); + } catch { + value = value.slice(1, -1); + } + } else if (value.startsWith("'") && value.endsWith("'")) { value = value.slice(1, -1); } - parsed[key] = value; + parsed[key] = typeof value === "string" ? expandEscapedLineBreaks(value) : value; } return parsed; } diff --git a/scripts/test-gateway-e2e.mjs b/scripts/test-gateway-e2e.mjs index 74f782a..3029108 100644 --- a/scripts/test-gateway-e2e.mjs +++ b/scripts/test-gateway-e2e.mjs @@ -3,13 +3,15 @@ import http from "node:http"; import net from "node:net"; import { once } from "node:events"; -import { spawn } from "node:child_process"; +import { spawn } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; - -const gatewayRoot = path.resolve(import.meta.dirname, ".."); -const gatewayEntry = path.join(gatewayRoot, "gateway.mjs"); +import os from "node:os"; +import path from "node:path"; + +import { normalizePhraseArray } from "./admin-lib.mjs"; + +const gatewayRoot = path.resolve(import.meta.dirname, ".."); +const gatewayEntry = path.join(gatewayRoot, "gateway.mjs"); function assert(condition, message) { if (!condition) { @@ -463,8 +465,13 @@ async function readSseUntilClose(url, requestBody) { }; } -async function run() { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "codex-retry-gateway-")); +async function run() { + assert( + JSON.stringify(normalizePhraseArray("a\\nb", [])) === JSON.stringify(["a", "b"]), + "normalizePhraseArray 未拆分字面量换行", + ); + + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "codex-retry-gateway-")); const upstreamPort = await getFreePort(); const gatewayPort = await getFreePort(); const configPath = path.join(tempRoot, "config.json"); @@ -1103,6 +1110,30 @@ async function run() { assert(metricsAfterRestart?.metrics?.total_proxy_request_count >= metricsBeforeRestart?.metrics?.total_proxy_request_count, "重启后 total_proxy_request_count 未保留"); assert(metricsAfterRestart?.metrics?.persistent_since, "重启后未返回 persistent_since"); + config.retryable_error_messages = [ + "Selected model is at capacity. Please try a different model.\\nstream disconnected before completion: Concurrency limit exceeded for account, please retry later", + ]; + await writeFile(configPath, JSON.stringify(config, null, 2), "utf8"); + gateway.child.kill(); + await once(gateway.child, "exit"); + gateway = startGateway(configPath, logPath); + await waitForHealth(`http://127.0.0.1:${gatewayPort}${config.health_path}`); + + const escapedNewlineCapacityResponse = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ test_capacity_before_success_times: 2, test_capacity_status: 200, test_reasoning_tokens: 128 }), + }); + const escapedNewlineCapacityBody = await escapedNewlineCapacityResponse.json(); + assert( + escapedNewlineCapacityResponse.status === 200, + `字面量换行 retryable_error_messages 下 200+capacity 未自动恢复: ${escapedNewlineCapacityResponse.status}`, + ); + assert( + escapedNewlineCapacityBody?.usage?.output_tokens_details?.reasoning_tokens === 128, + "字面量换行 retryable_error_messages 恢复后的返回体异常", + ); + await new Promise((resolve) => setTimeout(resolve, 120)); const logText = await readFile(logPath, "utf8"); assert(