fix: normalize wildcard gateway URLs for Codex

This commit is contained in:
2026-07-20 10:01:19 +08:00
parent 31e673f777
commit 14fae29618
6 changed files with 237 additions and 38 deletions
+31 -14
View File
@@ -73,9 +73,23 @@ export function getGatewayStatePaths(stateRoot = DEFAULT_STATE_ROOT) {
};
}
export function getGatewayBaseUrl(listenHost, listenPort) {
return `http://${listenHost}:${listenPort}`;
}
export function getGatewayListenerBaseUrl(listenHost, listenPort) {
return `http://${listenHost}:${listenPort}`;
}
export function getGatewayBaseUrl(listenHost, listenPort) {
// 0.0.0.0 is a valid bind address, but not a client destination. Keep the
// listener wildcard intact and use loopback for Codex, health probes, and UI
// links that originate on the same machine.
const clientHost = `${listenHost ?? ""}`.trim() === "0.0.0.0" ? "127.0.0.1" : listenHost;
return getGatewayListenerBaseUrl(clientHost, listenPort);
}
export function isGatewayBaseUrlForListener(baseUrl, listenHost, listenPort) {
const candidate = `${baseUrl ?? ""}`.trim();
return candidate === getGatewayBaseUrl(listenHost, listenPort)
|| candidate === getGatewayListenerBaseUrl(listenHost, listenPort);
}
export function getGatewayBaseUrlFromConfig(gatewayConfig) {
if (!gatewayConfig) {
@@ -408,16 +422,16 @@ export async function installForCurrentProvider({
const existingState = await readJsonFile(paths.statePath);
let originalBaseUrl = providerContext.currentBaseUrl;
if (providerContext.currentBaseUrl === localGatewayBaseUrl) {
if (!existingState?.original_base_url) {
throw new Error("Provider already points to the local gateway, but original_base_url is missing from state.");
}
originalBaseUrl = `${existingState.original_base_url}`;
}
if (originalBaseUrl === localGatewayBaseUrl) {
throw new Error("A real upstream_base_url could not be determined.");
}
if (isGatewayBaseUrlForListener(providerContext.currentBaseUrl, listenHost, listenPort)) {
if (!existingState?.original_base_url) {
throw new Error("Provider already points to the local gateway, but original_base_url is missing from state.");
}
originalBaseUrl = `${existingState.original_base_url}`;
}
if (isGatewayBaseUrlForListener(originalBaseUrl, listenHost, listenPort)) {
throw new Error("A real upstream_base_url could not be determined.");
}
const backupPath = path.join(paths.backupDir, `config-${new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15)}.toml`);
await copyFile(codexConfigPath, backupPath);
@@ -557,7 +571,10 @@ export async function launchUi({
const existingGatewayConfig = await readJsonFile(paths.configPath);
const stateGatewayBaseUrl = existingState?.gateway_base_url ? `${existingState.gateway_base_url}` : null;
const configGatewayBaseUrl = getGatewayBaseUrlFromConfig(existingGatewayConfig);
const managedGatewayBaseUrls = [requestedGatewayBaseUrl];
const managedGatewayBaseUrls = [
requestedGatewayBaseUrl,
getGatewayListenerBaseUrl(listenHost, listenPort),
];
for (const candidate of [stateGatewayBaseUrl, configGatewayBaseUrl]) {
if (candidate && !managedGatewayBaseUrls.includes(candidate)) {
managedGatewayBaseUrls.push(candidate);
+26 -5
View File
@@ -19,6 +19,7 @@ import {
getCodexProviderContext,
getGatewayBaseUrl,
getGatewayStatePaths,
isGatewayBaseUrlForListener,
normalizeIntArray,
normalizePhraseArray,
normalizeReasoningMatchMode,
@@ -345,12 +346,14 @@ function buildProfileConfig({
imageProfileEnv,
existingGatewayConfig,
providerContext,
listenHost,
listenPort,
localGatewayBaseUrl,
}) {
const upstreamBaseUrl =
profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_BASE_URL ||
existingGatewayConfig?.upstream_base_url ||
(providerContext.currentBaseUrl === localGatewayBaseUrl
(isGatewayBaseUrlForListener(providerContext.currentBaseUrl, listenHost, listenPort)
? null
: providerContext.currentBaseUrl);
@@ -425,15 +428,22 @@ function buildProfileConfig({
};
}
async function ensureCodexPointsToGateway({ paths, codexConfigPath, providerContext, localGatewayBaseUrl }) {
async function ensureCodexPointsToGateway({
paths,
codexConfigPath,
providerContext,
listenHost,
listenPort,
localGatewayBaseUrl,
}) {
await ensureDirectory(paths.backupDir);
const existingState = await readJsonFile(paths.statePath);
let originalBaseUrl = providerContext.currentBaseUrl;
if (providerContext.currentBaseUrl === localGatewayBaseUrl) {
if (isGatewayBaseUrlForListener(providerContext.currentBaseUrl, listenHost, listenPort)) {
originalBaseUrl = existingState?.original_base_url || null;
}
if (!originalBaseUrl || originalBaseUrl === localGatewayBaseUrl) {
if (!originalBaseUrl || isGatewayBaseUrlForListener(originalBaseUrl, listenHost, listenPort)) {
throw new Error("A restorable original Codex base_url could not be determined.");
}
@@ -536,19 +546,30 @@ async function main() {
imageProfileEnv,
existingGatewayConfig,
providerContext,
listenHost,
listenPort,
localGatewayBaseUrl,
});
const installState = options.noCodexConfigUpdate
? {
existingState,
originalBaseUrl: providerContext.currentBaseUrl,
originalBaseUrl: isGatewayBaseUrlForListener(providerContext.currentBaseUrl, listenHost, listenPort)
? (
existingState?.original_base_url
&& !isGatewayBaseUrlForListener(existingState.original_base_url, listenHost, listenPort)
? `${existingState.original_base_url}`
: null
)
: providerContext.currentBaseUrl,
backupPath: null,
}
: await ensureCodexPointsToGateway({
paths,
codexConfigPath,
providerContext,
listenHost,
listenPort,
localGatewayBaseUrl,
});
+59 -5
View File
@@ -638,14 +638,18 @@ async function run() {
const gatewayPort = await getFreePort();
const configPath = path.join(tempRoot, "config.json");
const logPath = path.join(tempRoot, "gateway.log");
const codexConfigPath = path.join(tempRoot, ".codex", "config.toml");
const profilesDir = path.join(tempRoot, ".config", "codex-retry-gateway", "profiles");
const imageProfilesDir = path.join(tempRoot, ".config", "codex-retry-gateway", "image-profiles");
const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
const legacyWildcardGatewayBaseUrl = `http://0.0.0.0:${gatewayPort}`;
const upstreamBaseUrl = `http://127.0.0.1:${upstreamPort}`;
const config = {
profile_name: "legacy-text",
listen_host: "127.0.0.1",
listen_host: "0.0.0.0",
listen_port: gatewayPort,
upstream_base_url: `http://127.0.0.1:${upstreamPort}`,
upstream_base_url: upstreamBaseUrl,
image_base_url: `http://127.0.0.1:${imageUpstreamPort}`,
image_auth_mode: "fixed_bearer",
image_auth_env: "TEST_CODEX_RETRY_GATEWAY_IMAGE_API_KEY",
@@ -672,12 +676,39 @@ async function run() {
await writeFile(
path.join(profilesDir, "legacy-text.env"),
[
`CODEX_RETRY_GATEWAY_UPSTREAM_BASE_URL=http://127.0.0.1:${upstreamPort}`,
"CODEX_RETRY_GATEWAY_LISTEN_HOST=0.0.0.0",
`CODEX_RETRY_GATEWAY_LISTEN_PORT=${gatewayPort}`,
`CODEX_RETRY_GATEWAY_UPSTREAM_BASE_URL=${upstreamBaseUrl}`,
"CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_MODE=passthrough",
"",
].join("\n"),
"utf8",
);
await mkdir(path.dirname(codexConfigPath), { recursive: true });
await writeFile(
codexConfigPath,
[
'model_provider = "custom"',
"",
"[model_providers.custom]",
'name = "Gateway E2E Test"',
`base_url = "${legacyWildcardGatewayBaseUrl}"`,
'wire_api = "responses"',
"",
].join("\n"),
"utf8",
);
await writeFile(
path.join(tempRoot, "state.json"),
`${JSON.stringify({
codex_config_path: codexConfigPath,
provider_name: "custom",
original_base_url: upstreamBaseUrl,
gateway_base_url: legacyWildcardGatewayBaseUrl,
profile_name: "legacy-text",
}, null, 2)}\n`,
"utf8",
);
await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
const upstream = await startFakeUpstream(upstreamPort, { label: "default" });
@@ -1036,7 +1067,7 @@ async function run() {
headers: { ...adminHeaders, "content-type": "application/json" },
body: JSON.stringify({
name: "legacy-text",
listen_host: "127.0.0.1",
listen_host: "0.0.0.0",
listen_port: gatewayPort,
upstream_base_url: `http://127.0.0.1:${upstreamPort}`,
auth_mode: "passthrough",
@@ -1044,6 +1075,29 @@ async function run() {
},
);
assert(saveActiveTextProfileResponse.status === 200, `当前文本 profile 保存失败: ${saveActiveTextProfileResponse.status}`);
const saveActiveTextProfilePayload = await saveActiveTextProfileResponse.json();
assert(
saveActiveTextProfilePayload?.applied_profile?.codex_provider_sync?.updated === true,
"保存活跃 wildcard listener profile 时未同步 Codex provider",
);
assert(
saveActiveTextProfilePayload?.applied_profile?.codex_provider_sync?.gateway_base_url === gatewayBaseUrl,
"保存活跃 wildcard listener profile 时未使用回环 gateway URL",
);
const savedCodexConfig = await readFile(codexConfigPath, "utf8");
assert(
savedCodexConfig.includes(`base_url = "${gatewayBaseUrl}"`),
"保存 wildcard listener profile 后 Codex config 未指向 127.0.0.1",
);
assert(
!savedCodexConfig.includes(`base_url = "${legacyWildcardGatewayBaseUrl}"`),
"保存 wildcard listener profile 后 Codex config 仍指向 0.0.0.0",
);
const savedState = JSON.parse(await readFile(path.join(tempRoot, "state.json"), "utf8"));
assert(savedState.gateway_base_url === gatewayBaseUrl, "runtime state 未规范化 wildcard gateway URL");
assert(savedState.original_base_url === upstreamBaseUrl, "同步 Codex config 时覆盖了原始上游 URL");
assert(savedState.latest_backup_path, "同步 Codex config 时未创建可恢复备份");
assert((await stat(savedState.latest_backup_path)).isFile(), "Codex config 备份文件不存在");
const afterTextSaveStatusResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/status`, { headers: adminHeaders });
const afterTextSaveStatusPayload = await afterTextSaveStatusResponse.json();
assert(afterTextSaveStatusPayload?.config?.image_profile_name === "image-primary", "保存文本 profile 不应重置图片 profile");
+6
View File
@@ -8,6 +8,8 @@ import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { getGatewayBaseUrl } from "./admin-lib.mjs";
const scriptsRoot = import.meta.dirname;
const launchScript = path.join(scriptsRoot, "launch-ui.sh");
const restoreScript = path.join(scriptsRoot, "restore-codex-config.sh");
@@ -122,6 +124,10 @@ async function run() {
const gatewayPort = await getFreePort();
const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
const upstreamBaseUrl = `http://127.0.0.1:${upstreamPort}`;
assert(
getGatewayBaseUrl("0.0.0.0", gatewayPort) === gatewayBaseUrl,
"Wildcard listener must normalize to loopback for local gateway URLs",
);
await mkdir(codexDir, { recursive: true });
await writeFile(