feat: separate image profiles from text routing
This commit is contained in:
+186
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { copyFile, readFile, rm } from "node:fs/promises";
|
||||
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -32,8 +32,17 @@ import {
|
||||
} from "./admin-lib.mjs";
|
||||
|
||||
const DEFAULT_PROFILES_DIR = path.join(os.homedir(), ".config", "codex-retry-gateway", "profiles");
|
||||
const DEFAULT_IMAGE_PROFILES_DIR = path.join(os.homedir(), ".config", "codex-retry-gateway", "image-profiles");
|
||||
const DEFAULT_REQUEST_BODY_LIMIT_BYTES = 1024 * 1024 * 1024;
|
||||
const LEGACY_DEFAULT_REQUEST_BODY_LIMIT_BYTES = 10 * 1024 * 1024;
|
||||
const IMAGE_PROFILE_ENV_KEYS = [
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_BASE_URL",
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_AUTH_MODE",
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_AUTH_ENV",
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_AUTH_FILE",
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_PATH",
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_KEY",
|
||||
];
|
||||
|
||||
function parseEnvFile(content) {
|
||||
const parsed = {};
|
||||
@@ -67,6 +76,10 @@ function getProfileName(options) {
|
||||
return `${options.profile || positional || process.env.CODEX_RETRY_GATEWAY_PROFILE || ""}`.trim();
|
||||
}
|
||||
|
||||
function getImageProfileName(options) {
|
||||
return `${options.imageProfile || process.env.CODEX_RETRY_GATEWAY_IMAGE_PROFILE || ""}`.trim();
|
||||
}
|
||||
|
||||
function resolveDefaultRequestedProfileName(existingState) {
|
||||
const stateProfileName = `${existingState?.profile_name || ""}`.trim();
|
||||
if (stateProfileName) {
|
||||
@@ -93,6 +106,31 @@ function resolvePreferredProfileName({ requestedProfileName, existingState, pref
|
||||
return stateProfileName;
|
||||
}
|
||||
|
||||
function profileFileExists(profilesDir, profileName) {
|
||||
return /^[A-Za-z0-9_.-]+$/.test(`${profileName || ""}`)
|
||||
&& fs.existsSync(path.join(profilesDir, `${profileName}.env`));
|
||||
}
|
||||
|
||||
function resolveImageProfileName({
|
||||
requestedImageProfileName,
|
||||
textProfileName,
|
||||
existingState,
|
||||
preferStateProfile,
|
||||
imageProfilesDir,
|
||||
}) {
|
||||
if (requestedImageProfileName) {
|
||||
return profileFileExists(imageProfilesDir, requestedImageProfileName)
|
||||
? requestedImageProfileName
|
||||
: "";
|
||||
}
|
||||
const candidates = [];
|
||||
if (preferStateProfile) {
|
||||
candidates.push(`${existingState?.image_profile_name || ""}`.trim());
|
||||
}
|
||||
candidates.push(textProfileName);
|
||||
return candidates.find((profileName) => profileFileExists(imageProfilesDir, profileName)) || "";
|
||||
}
|
||||
|
||||
function boolFromEnv(value, fallback = false) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return fallback;
|
||||
@@ -165,6 +203,58 @@ function buildProfileAuthConfig(profileEnv) {
|
||||
return authConfig;
|
||||
}
|
||||
|
||||
function inferImageProfileAuthMode(imageProfileEnv) {
|
||||
if (imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_MODE) {
|
||||
return normalizeAuthMode(imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_MODE);
|
||||
}
|
||||
if (
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_PATH ||
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_KEY
|
||||
) {
|
||||
return "auth_json";
|
||||
}
|
||||
if (
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_FILE ||
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_ENV
|
||||
) {
|
||||
return "fixed_bearer";
|
||||
}
|
||||
return "fixed_bearer";
|
||||
}
|
||||
|
||||
function buildImageProfileAuthConfig(imageProfileEnv) {
|
||||
const authMode = inferImageProfileAuthMode(imageProfileEnv);
|
||||
const authConfig = {
|
||||
image_auth_mode: authMode,
|
||||
image_auth_env: "",
|
||||
image_auth_file: "",
|
||||
image_auth_json_path: "",
|
||||
image_auth_json_key: "",
|
||||
};
|
||||
|
||||
if (authMode === "fixed_bearer") {
|
||||
authConfig.image_auth_env =
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_ENV ||
|
||||
"CODEX_RETRY_GATEWAY_IMAGE_API_KEY";
|
||||
authConfig.image_auth_file =
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_FILE ||
|
||||
"";
|
||||
} else if (authMode === "manual_bearer") {
|
||||
authConfig.image_auth_file =
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_FILE ||
|
||||
"";
|
||||
} else if (authMode === "auth_json") {
|
||||
authConfig.image_auth_json_path =
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_PATH ||
|
||||
"";
|
||||
authConfig.image_auth_json_key =
|
||||
imageProfileEnv.CODEX_RETRY_GATEWAY_IMAGE_AUTH_JSON_KEY ||
|
||||
"OPENAI_API_KEY";
|
||||
}
|
||||
|
||||
return authConfig;
|
||||
}
|
||||
|
||||
async function loadProfileEnv(profileName, profilesDir) {
|
||||
const profilePath = path.join(profilesDir, `${profileName}.env`);
|
||||
if (!fs.existsSync(profilePath)) {
|
||||
@@ -177,7 +267,68 @@ async function loadProfileEnv(profileName, profilesDir) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildProfileConfig({ profileName, profileEnv, existingGatewayConfig, providerContext, localGatewayBaseUrl }) {
|
||||
function serializeEnvValue(value) {
|
||||
const text = `${value ?? ""}`;
|
||||
if (/^[A-Za-z0-9_./:@?&=,+-]*$/.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return JSON.stringify(text);
|
||||
}
|
||||
|
||||
function hasLegacyImageProfileConfig(profileEnv) {
|
||||
return Boolean(`${profileEnv?.CODEX_RETRY_GATEWAY_IMAGE_BASE_URL || ""}`.trim());
|
||||
}
|
||||
|
||||
async function migrateLegacyImageProfile(profileName, profileEnv, imageProfilesDir) {
|
||||
if (!hasLegacyImageProfileConfig(profileEnv)) {
|
||||
return null;
|
||||
}
|
||||
const imageProfilePath = path.join(imageProfilesDir, `${profileName}.env`);
|
||||
if (fs.existsSync(imageProfilePath)) {
|
||||
return { profilePath: imageProfilePath, migrated: false };
|
||||
}
|
||||
|
||||
const pairs = IMAGE_PROFILE_ENV_KEYS
|
||||
.filter((key) => profileEnv[key] !== undefined)
|
||||
.map((key) => [key, profileEnv[key]]);
|
||||
await mkdir(imageProfilesDir, { recursive: true });
|
||||
await writeFile(
|
||||
imageProfilePath,
|
||||
[
|
||||
"# Migrated from a legacy text profile by codex-retry-gateway.",
|
||||
"# Image configuration is now independent from text profiles.",
|
||||
...pairs.map(([key, value]) => `${key}=${serializeEnvValue(value)}`),
|
||||
"",
|
||||
].join("\n"),
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
return { profilePath: imageProfilePath, migrated: true };
|
||||
}
|
||||
|
||||
async function loadImageProfileEnv(profileName, imageProfilesDir) {
|
||||
if (!profileName) {
|
||||
return { profilePath: null, env: {} };
|
||||
}
|
||||
const profilePath = path.join(imageProfilesDir, `${profileName}.env`);
|
||||
if (!fs.existsSync(profilePath)) {
|
||||
throw new Error(`Image profile env file was not found: ${profilePath}`);
|
||||
}
|
||||
const content = await readFile(profilePath, "utf8");
|
||||
return {
|
||||
profilePath,
|
||||
env: parseEnvFile(content),
|
||||
};
|
||||
}
|
||||
|
||||
function buildProfileConfig({
|
||||
profileName,
|
||||
profileEnv,
|
||||
imageProfileName,
|
||||
imageProfileEnv,
|
||||
existingGatewayConfig,
|
||||
providerContext,
|
||||
localGatewayBaseUrl,
|
||||
}) {
|
||||
const upstreamBaseUrl =
|
||||
profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_BASE_URL ||
|
||||
existingGatewayConfig?.upstream_base_url ||
|
||||
@@ -190,6 +341,7 @@ function buildProfileConfig({ profileName, profileEnv, existingGatewayConfig, pr
|
||||
}
|
||||
|
||||
const profileAuthConfig = buildProfileAuthConfig(profileEnv);
|
||||
const imageProfileAuthConfig = buildImageProfileAuthConfig(imageProfileEnv || {});
|
||||
const reasoningMatchMode = normalizeReasoningMatchMode(
|
||||
profileEnv.CODEX_RETRY_GATEWAY_REASONING_MATCH_MODE ||
|
||||
existingGatewayConfig?.reasoning_match_mode ||
|
||||
@@ -204,6 +356,9 @@ function buildProfileConfig({ profileName, profileEnv, existingGatewayConfig, pr
|
||||
: DEFAULT_LISTEN_PORT,
|
||||
upstream_base_url: upstreamBaseUrl,
|
||||
...profileAuthConfig,
|
||||
image_profile_name: imageProfileName || "",
|
||||
image_base_url: imageProfileEnv?.CODEX_RETRY_GATEWAY_IMAGE_BASE_URL || "",
|
||||
...imageProfileAuthConfig,
|
||||
request_body_limit_bytes: profileEnv.CODEX_RETRY_GATEWAY_REQUEST_BODY_LIMIT_BYTES
|
||||
? normalizeRequestBodyLimitBytes(profileEnv.CODEX_RETRY_GATEWAY_REQUEST_BODY_LIMIT_BYTES)
|
||||
: normalizeRequestBodyLimitBytes(existingGatewayConfig?.request_body_limit_bytes),
|
||||
@@ -288,6 +443,7 @@ async function ensureCodexPointsToGateway({ paths, codexConfigPath, providerCont
|
||||
async function main() {
|
||||
const options = parseOptions(process.argv, { booleanFlags: ["no-codex-config-update", "prefer-state-profile"] });
|
||||
const profilesDir = options.profilesDir || DEFAULT_PROFILES_DIR;
|
||||
const imageProfilesDir = options.imageProfilesDir || DEFAULT_IMAGE_PROFILES_DIR;
|
||||
const stateRoot = options.stateRoot || process.env.CODEX_RETRY_GATEWAY_STATE_ROOT || DEFAULT_STATE_ROOT;
|
||||
const codexConfigPath =
|
||||
options.codexConfigPath ||
|
||||
@@ -315,7 +471,31 @@ async function main() {
|
||||
}
|
||||
|
||||
const { profilePath, env: profileEnv } = await loadProfileEnv(profileName, profilesDir);
|
||||
const migration = await migrateLegacyImageProfile(profileName, profileEnv, imageProfilesDir);
|
||||
const requestedImageProfileName = getImageProfileName(options);
|
||||
const imageProfileName = resolveImageProfileName({
|
||||
requestedImageProfileName,
|
||||
textProfileName: profileName,
|
||||
existingState,
|
||||
preferStateProfile: Boolean(options.preferStateProfile),
|
||||
imageProfilesDir,
|
||||
});
|
||||
if (requestedImageProfileName && !imageProfileName) {
|
||||
throw new Error(`Image profile env file was not found: ${path.join(imageProfilesDir, `${requestedImageProfileName}.env`)}`);
|
||||
}
|
||||
const { profilePath: imageProfilePath, env: imageProfileEnv } = await loadImageProfileEnv(
|
||||
imageProfileName,
|
||||
imageProfilesDir,
|
||||
);
|
||||
if (migration?.migrated) {
|
||||
process.stdout.write(
|
||||
`[run-profile] migrated legacy image settings text=${profileName} image=${imageProfileName || profileName}\n`,
|
||||
);
|
||||
}
|
||||
for (const [key, value] of Object.entries(profileEnv)) {
|
||||
if (key.startsWith("CODEX_RETRY_GATEWAY_IMAGE_")) {
|
||||
continue;
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
@@ -330,6 +510,8 @@ async function main() {
|
||||
const gatewayConfig = buildProfileConfig({
|
||||
profileName,
|
||||
profileEnv,
|
||||
imageProfileName,
|
||||
imageProfileEnv,
|
||||
existingGatewayConfig,
|
||||
providerContext,
|
||||
localGatewayBaseUrl,
|
||||
@@ -355,6 +537,8 @@ async function main() {
|
||||
last_started_at: new Date().toISOString(),
|
||||
profile_name: profileName,
|
||||
profile_env_path: profilePath,
|
||||
image_profile_name: imageProfileName || "",
|
||||
image_profile_env_path: imageProfilePath,
|
||||
codex_config_path: codexConfigPath,
|
||||
provider_name: providerContext.providerName,
|
||||
original_base_url: installState.originalBaseUrl,
|
||||
|
||||
Reference in New Issue
Block a user