refactor: require yaml codex pool config

This commit is contained in:
Codex
2026-06-14 16:21:11 +00:00
parent 19a28a91bd
commit 65ffa09b50
3 changed files with 221 additions and 386 deletions
+160 -253
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "./config";
@@ -9,7 +9,6 @@ import { applyLocalCaddyManagedSite } from "./pk01-caddy";
import {
codexPoolSentinelSummary,
codexPoolSentinelRuntimeImage,
defaultCodexPoolSentinelConfig,
readCodexPoolSentinelConfig,
renderCodexPoolSentinelManifest,
type CodexPoolSentinelConfig,
@@ -23,12 +22,6 @@ const fieldManager = "unidesk-platform-infra";
const sub2apiConfigPath = rootPath("config", "platform-infra", "sub2api.yaml");
const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
const sentinelImageDockerfilePath = rootPath("src", "components", "platform-infra", "sub2api", "sentinel.Dockerfile");
const defaultPoolGroupName = "unidesk-codex-pool";
const defaultPoolApiKeyName = "unidesk-codex-pool-api-key";
const defaultPoolApiKeySecretName = "sub2api-codex-pool-api-key";
const defaultPoolApiKeySecretKey = "API_KEY";
const defaultMinOwnerBalanceUsd = 1000;
const defaultAccountPriority = 10;
const remoteJobDir = "/tmp/unidesk-platform-infra-sub2api-codex-pool";
const remoteJobTimeoutMs = 15 * 60_000;
const remoteJobPollMs = 5_000;
@@ -136,9 +129,11 @@ interface CodexPoolConfig {
relatedIssues: number[];
};
groupName: string;
groupDescription: string;
apiKeyName: string;
apiKeySecretName: string;
apiKeySecretKey: string;
adminEmailDefault: string;
minOwnerBalanceUsd: number;
minOwnerConcurrency: number;
minOwnerConcurrencySource: "auto" | "yaml";
@@ -760,6 +755,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
serviceName: runtimeTarget.serviceName,
serviceDns: runtimeTarget.serviceDns,
appSecretName: runtimeTarget.appSecretName,
adminEmailDefault: pool.adminEmailDefault,
proxy: runtimeTarget.egressProxy?.applyToSentinel ? {
httpProxy: runtimeTarget.egressProxy.httpProxy,
noProxy: runtimeTarget.egressProxy.noProxy,
@@ -769,9 +765,11 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
},
pool: {
groupName: pool.groupName,
groupDescription: pool.groupDescription,
apiKeyName: pool.apiKeyName,
apiKeySecretName: pool.apiKeySecretName,
apiKeySecretKey: pool.apiKeySecretKey,
adminEmailDefault: pool.adminEmailDefault,
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
minOwnerConcurrency: pool.minOwnerConcurrency,
defaultAccountPriority: pool.defaultAccountPriority,
@@ -1171,10 +1169,7 @@ function collectCodexProfiles(): CodexProfile[] {
const pool = readCodexPoolConfig();
if (!existsSync(codexDir)) return [];
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultSentinelProtect, pool.defaultAccountPriority);
return configs.map((entry) => {
return pool.profiles.map((entry) => {
const resolved = resolveProfileFiles(codexDir, entry);
const profile = entry.profile;
const accountName = entry.accountName ?? uniqueAccountName(profile, seenAccountNames);
@@ -1247,41 +1242,6 @@ function collectCodexProfiles(): CodexProfile[] {
});
}
function discoverCodexProfileConfigs(
codexDir: string,
defaultTempUnschedulable = defaultCodexTempUnschedulablePolicy(),
defaultSentinelProtect = defaultCodexSentinelProtectPolicy(),
defaultPriority = defaultAccountPriority,
): CodexPoolProfileConfig[] {
return readdirSync(codexDir)
.filter((file) => file === "config.toml" || file.startsWith("config.toml."))
.sort((a, b) => {
if (a === "config.toml") return -1;
if (b === "config.toml") return 1;
return a.localeCompare(b);
})
.map((configFile) => {
const suffix = configFile === "config.toml" ? "" : configFile.slice("config.toml.".length);
const profile = suffix === "" ? "default" : suffix;
return {
profile,
accountName: null,
configFile,
authFile: suffix === "" ? "auth.json" : `auth.json.${suffix}`,
fallbackConfigFile: null,
fallbackAuthFile: null,
openaiResponsesWebSocketsV2Mode: null,
upstreamUserAgent: null,
trustUpstream: false,
sentinelProtect: defaultSentinelProtect,
priority: defaultPriority,
capacity: null,
loadFactor: null,
tempUnschedulable: defaultTempUnschedulable,
};
});
}
function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig): { configFile: string; authFile: string } {
const configFile = existsSync(join(codexDir, profile.configFile)) || profile.fallbackConfigFile === null
? profile.configFile
@@ -1293,7 +1253,6 @@ function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig):
}
function readCodexPoolConfig(): CodexPoolConfig {
const defaults = defaultCodexPoolConfig();
if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required`);
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`);
@@ -1306,19 +1265,18 @@ function readCodexPoolConfig(): CodexPoolConfig {
if (!isRecord(pool)) throw new Error(`${codexPoolConfigPath}.pool must be a YAML object`);
if (!isRecord(parsed.profiles)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`);
rejectSchedulableYamlField(pool, "pool");
const defaultTempUnschedulable = readTempUnschedulablePolicy(pool.defaultTempUnschedulable, "pool.defaultTempUnschedulable", defaults.defaultTempUnschedulable);
const defaultTempUnschedulable = readTempUnschedulablePolicy(pool.defaultTempUnschedulable, "pool.defaultTempUnschedulable");
const defaultAccountPriorityValue = readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority");
const defaultAccountCapacityValue = readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity");
const defaultAccountLoadFactorValue = readAccountLoadFactor(pool.defaultAccountLoadFactor, "pool.defaultAccountLoadFactor");
const defaultSentinelProtect = readSentinelProtectPolicy(pool.defaultSentinelProtect, "pool.defaultSentinelProtect", defaults.defaultSentinelProtect);
const defaultSentinelProtect = readSentinelProtectPolicy(pool.defaultSentinelProtect, "pool.defaultSentinelProtect");
const profiles = readProfileConfig(
parsed.profiles,
defaults.profiles,
defaultTempUnschedulable,
defaultSentinelProtect,
defaultAccountPriorityValue,
);
const manualAccounts = readManualAccountsConfig(parsed.manualAccounts, defaults.manualAccounts);
const manualAccounts = readManualAccountsConfig(parsed.manualAccounts);
assertProtectedManualAccountsNotManaged(profiles, manualAccounts);
const declaredAccountCapacity = desiredProfileCapacityTotal(profiles, defaultAccountCapacityValue);
const minOwnerConcurrencySource = pool.minOwnerConcurrency === undefined || pool.minOwnerConcurrency === null ? "auto" : "yaml";
@@ -1333,11 +1291,13 @@ function readCodexPoolConfig(): CodexPoolConfig {
owner: requiredStringConfigField(metadata, "owner", "metadata"),
relatedIssues: integerArrayConfigField(metadata, "relatedIssues", "metadata"),
},
groupName: stringValue(pool.groupName) ?? defaults.groupName,
apiKeyName: stringValue(pool.apiKeyName) ?? defaults.apiKeyName,
apiKeySecretName: stringValue(pool.apiKeySecretName) ?? defaults.apiKeySecretName,
apiKeySecretKey: stringValue(pool.apiKeySecretKey) ?? defaults.apiKeySecretKey,
minOwnerBalanceUsd: numberValue(pool.minOwnerBalanceUsd) ?? defaults.minOwnerBalanceUsd,
groupName: requiredStringConfigField(pool, "groupName", "pool"),
groupDescription: readRequiredDescription(pool.groupDescription, "pool.groupDescription", 300),
apiKeyName: requiredStringConfigField(pool, "apiKeyName", "pool"),
apiKeySecretName: requiredStringConfigField(pool, "apiKeySecretName", "pool"),
apiKeySecretKey: requiredStringConfigField(pool, "apiKeySecretKey", "pool"),
adminEmailDefault: readRequiredEmail(pool.adminEmailDefault, "pool.adminEmailDefault"),
minOwnerBalanceUsd: readRequiredPositiveNumber(pool.minOwnerBalanceUsd, "pool.minOwnerBalanceUsd"),
minOwnerConcurrency,
minOwnerConcurrencySource,
defaultAccountPriority: defaultAccountPriorityValue,
@@ -1347,10 +1307,10 @@ function readCodexPoolConfig(): CodexPoolConfig {
defaultSentinelProtect,
profiles,
manualAccounts,
publicExposure: readPublicExposureConfig(parsed.publicExposure, defaults.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex, defaults.localCodex),
publicExposure: readPublicExposureConfig(parsed.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex),
sentinel: {
...readCodexPoolSentinelConfig(parsed.sentinel, defaults.sentinel, codexPoolConfigPath),
...readCodexPoolSentinelConfig(parsed.sentinel, codexPoolConfigPath),
protectedManualAccounts: manualAccounts.protected.map((account) => account.accountName),
},
};
@@ -1366,96 +1326,13 @@ function readCodexPoolConfig(): CodexPoolConfig {
return config;
}
function defaultCodexPoolConfig(): CodexPoolConfig {
const runtimeTarget = codexPoolRuntimeTarget();
return {
version: 1,
kind: "platform-infra-sub2api-codex-pool",
metadata: {
id: "sub2api-codex-pool",
owner: "unidesk",
relatedIssues: [],
},
groupName: defaultPoolGroupName,
apiKeyName: defaultPoolApiKeyName,
apiKeySecretName: defaultPoolApiKeySecretName,
apiKeySecretKey: defaultPoolApiKeySecretKey,
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
minOwnerConcurrency: 1,
minOwnerConcurrencySource: "auto",
defaultAccountPriority,
defaultAccountCapacity: 5,
defaultAccountLoadFactor: 1,
defaultTempUnschedulable: defaultCodexTempUnschedulablePolicy(),
defaultSentinelProtect: defaultCodexSentinelProtectPolicy(),
profiles: [],
manualAccounts: {
protected: [],
},
publicExposure: {
enabled: false,
proxyName: "platform-infra-sub2api",
configMapName: "sub2api-frpc-config",
deploymentName: "sub2api-frpc",
frpcImage: "fatedier/frpc:v0.68.1",
serverAddr: "74.48.78.17",
serverPort: 7000,
remotePort: 21880,
localIP: runtimeTarget.serviceDns.split(":")[0],
localPort: 8080,
publicBaseUrl: "http://74.48.78.17:21880",
masterBaseUrl: "http://127.0.0.1:21880",
masterFrps: {
configPath: "/opt/hwlab-frp/frps.dev.toml",
containerName: "hwlab-frps-dev",
},
masterCaddy: {
enabled: true,
domain: "sub2api.74-48-78-17.nip.io",
configPath: "/etc/caddy/Caddyfile",
serviceName: "caddy",
upstreamBaseUrl: "http://127.0.0.1:21880",
responseHeaderTimeoutSeconds: 180,
edgeRetry: {
enabled: false,
tryDurationSeconds: 0,
tryIntervalMilliseconds: 250,
retryMatch: {
methods: [],
paths: [],
},
},
},
},
localCodex: {
backupSuffix: "pre-sub2api",
providerName: "OpenAI",
wireApi: "responses",
modelContextWindow: 272000,
modelAutoCompactTokenLimit: 240000,
supportsWebSockets: true,
responsesWebSocketsV2: true,
responsesSmokeModel: "gpt-5.5",
},
sentinel: defaultCodexPoolSentinelConfig(),
};
}
export function defaultCodexTempUnschedulablePolicy(): CodexTempUnschedulablePolicy {
return {
enabled: false,
rules: [],
};
}
function readProfileConfig(
value: unknown,
defaults: CodexPoolProfileConfig[],
defaultTempUnschedulable: CodexTempUnschedulablePolicy,
defaultSentinelProtect: CodexSentinelProtectPolicy,
defaultPriority: number,
): CodexPoolProfileConfig[] {
if (!isRecord(value)) return defaults;
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`);
const entries = value.entries;
if (!Array.isArray(entries)) throw new Error(`${codexPoolConfigPath}.profiles.entries must be a YAML array`);
return entries.map((entry, index) => {
@@ -1506,11 +1383,9 @@ function desiredProfileCapacityTotal(profiles: CodexPoolProfileConfig[], default
return profiles.reduce((total, profile) => total + (profile.capacity ?? defaultCapacity), 0);
}
function readManualAccountsConfig(value: unknown, defaults: CodexPoolManualAccountsConfig): CodexPoolManualAccountsConfig {
if (value === undefined || value === null) return defaults;
function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`);
const protectedRaw = value.protected;
if (protectedRaw === undefined || protectedRaw === null) return { protected: [] };
if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`);
const seen = new Set<string>();
const protectedAccounts = protectedRaw.map((entry, index): CodexPoolManualAccountProtection => {
@@ -1536,7 +1411,8 @@ function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolMa
if (value === undefined || value === null) return null;
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
const enabled = value.enabled === undefined ? true : value.enabled === true;
const source = stringValue(value.source) ?? "target-egress-proxy";
const source = stringValue(value.source);
if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`);
if (source !== "target-egress-proxy") throw new Error(`${codexPoolConfigPath}.${key}.source must be target-egress-proxy`);
const proxyName = stringValue(value.proxyName);
if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`);
@@ -1552,7 +1428,8 @@ function readManualAccountGroupBinding(value: unknown, key: string): CodexPoolMa
if (value === undefined || value === null) return null;
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
const enabled = value.enabled === undefined ? true : value.enabled === true;
const source = stringValue(value.source) ?? "pool-group";
const source = stringValue(value.source);
if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`);
if (source !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key}.source must be pool-group`);
return {
enabled,
@@ -1618,36 +1495,42 @@ function readTrustUpstream(value: unknown, key: string): boolean {
return parsed;
}
export function defaultCodexSentinelProtectPolicy(): CodexSentinelProtectPolicy {
return {
enabled: false,
consecutiveFailures: 1,
initialRetryDelaySeconds: 2,
maxRetryDelaySeconds: 60,
backoffMultiplier: 2,
};
}
function readSentinelProtectPolicy(value: unknown, key: string, fallback = defaultCodexSentinelProtectPolicy()): CodexSentinelProtectPolicy {
if (value === undefined || value === null) return fallback;
function readSentinelProtectPolicy(value: unknown, key: string, fallback?: CodexSentinelProtectPolicy): CodexSentinelProtectPolicy {
if (value === undefined || value === null) {
if (fallback !== undefined) return fallback;
throw new Error(`${codexPoolConfigPath}.${key} is required`);
}
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback.enabled);
if (!enabled) return { ...fallback, enabled: false };
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled);
const required = ["consecutiveFailures", "initialRetryDelaySeconds", "maxRetryDelaySeconds", "backoffMultiplier"];
for (const field of required) {
if (value[field] === undefined || value[field] === null) {
if (fallback === undefined && (value[field] === undefined || value[field] === null)) {
throw new Error(`${codexPoolConfigPath}.${key}.${field} is required`);
}
if (enabled && value[field] === undefined && fallback === undefined) {
throw new Error(`${codexPoolConfigPath}.${key}.${field} is required when enabled=true`);
}
}
const consecutiveFailures = readBoundedInteger(value.consecutiveFailures, `${key}.consecutiveFailures`, 1, 20);
const initialRetryDelaySeconds = readBoundedInteger(value.initialRetryDelaySeconds, `${key}.initialRetryDelaySeconds`, 1, 3600);
const maxRetryDelaySeconds = readBoundedInteger(value.maxRetryDelaySeconds, `${key}.maxRetryDelaySeconds`, 1, 3600);
const backoffMultiplier = readBoundedInteger(value.backoffMultiplier, `${key}.backoffMultiplier`, 1, 10);
const consecutiveFailures = value.consecutiveFailures === undefined || value.consecutiveFailures === null
? fallback?.consecutiveFailures
: readBoundedInteger(value.consecutiveFailures, `${key}.consecutiveFailures`, 1, 20);
const initialRetryDelaySeconds = value.initialRetryDelaySeconds === undefined || value.initialRetryDelaySeconds === null
? fallback?.initialRetryDelaySeconds
: readBoundedInteger(value.initialRetryDelaySeconds, `${key}.initialRetryDelaySeconds`, 1, 3600);
const maxRetryDelaySeconds = value.maxRetryDelaySeconds === undefined || value.maxRetryDelaySeconds === null
? fallback?.maxRetryDelaySeconds
: readBoundedInteger(value.maxRetryDelaySeconds, `${key}.maxRetryDelaySeconds`, 1, 3600);
const backoffMultiplier = value.backoffMultiplier === undefined || value.backoffMultiplier === null
? fallback?.backoffMultiplier
: readBoundedInteger(value.backoffMultiplier, `${key}.backoffMultiplier`, 1, 10);
if (consecutiveFailures === undefined || initialRetryDelaySeconds === undefined || maxRetryDelaySeconds === undefined || backoffMultiplier === undefined) {
throw new Error(`${codexPoolConfigPath}.${key} is missing required sentinel protection fields`);
}
if (maxRetryDelaySeconds < initialRetryDelaySeconds) {
throw new Error(`${codexPoolConfigPath}.${key}.maxRetryDelaySeconds must be >= initialRetryDelaySeconds`);
}
return {
enabled: true,
enabled,
consecutiveFailures,
initialRetryDelaySeconds,
maxRetryDelaySeconds,
@@ -1663,8 +1546,11 @@ function readBoundedInteger(value: unknown, key: string, min: number, max: numbe
return parsed;
}
function readAccountPriority(value: unknown, key: string, fallback = defaultAccountPriority): number {
if (value === undefined || value === null) return fallback;
function readAccountPriority(value: unknown, key: string, fallback?: number): number {
if (value === undefined || value === null) {
if (fallback !== undefined) return fallback;
throw new Error(`${codexPoolConfigPath}.${key} is required`);
}
const priority = numberValue(value);
if (priority === null || !Number.isInteger(priority) || priority < 0 || priority > 1000) {
throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 0 to 1000`);
@@ -1696,8 +1582,8 @@ function readAccountLoadFactor(value: unknown, key: string): number {
return loadFactor;
}
function readCaddyTimeoutSeconds(value: unknown, key: string, fallback: number): number {
if (value === undefined || value === null) return fallback;
function readCaddyTimeoutSeconds(value: unknown, key: string): number {
if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
const seconds = numberValue(value);
if (seconds === null || !Number.isInteger(seconds) || seconds < 30 || seconds > 900) {
throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 30 to 900`);
@@ -1705,16 +1591,14 @@ function readCaddyTimeoutSeconds(value: unknown, key: string, fallback: number):
return seconds;
}
function readCaddyEdgeRetryConfig(value: unknown, fallback: CodexPoolCaddyEdgeRetryConfig, key: string): CodexPoolCaddyEdgeRetryConfig {
if (value === undefined || value === null) return cloneCaddyEdgeRetryConfig(fallback);
function readCaddyEdgeRetryConfig(value: unknown, key: string): CodexPoolCaddyEdgeRetryConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback.enabled);
if (!enabled) return { ...cloneCaddyEdgeRetryConfig(fallback), enabled: false };
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`);
const tryDurationSeconds = readPositiveInteger(value.tryDurationSeconds, `${key}.tryDurationSeconds`);
const tryIntervalMilliseconds = readPositiveInteger(value.tryIntervalMilliseconds, `${key}.tryIntervalMilliseconds`);
const retryMatchValue = isRecord(value.retryMatch) ? value.retryMatch : null;
if (retryMatchValue === null) throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must be a YAML object when enabled=true`);
if (retryMatchValue === null) throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must be a YAML object`);
const retryMatch = {
methods: readCaddyRetryMethods(retryMatchValue.methods, `${key}.retryMatch.methods`),
paths: readCaddyRetryPaths(retryMatchValue.paths, `${key}.retryMatch.paths`),
@@ -1733,8 +1617,8 @@ function readPositiveInteger(value: unknown, key: string): number {
return parsed;
}
function readPositiveIntegerConfig(value: unknown, key: string, fallback: number): number {
if (value === undefined || value === null) return fallback;
function readPositiveIntegerConfig(value: unknown, key: string): number {
if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`);
return readPositiveInteger(value, key);
}
@@ -1770,13 +1654,20 @@ function readCaddyRetryPaths(value: unknown, key: string): string[] {
return paths;
}
function readTempUnschedulablePolicy(value: unknown, key: string, fallback: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy {
if (value === undefined || value === null) return cloneTempUnschedulablePolicy(fallback);
function readTempUnschedulablePolicy(value: unknown, key: string, fallback?: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy {
if (value === undefined || value === null) {
if (fallback !== undefined) return cloneTempUnschedulablePolicy(fallback);
throw new Error(`${codexPoolConfigPath}.${key} is required`);
}
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`);
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback.enabled);
const rules = value.rules === undefined || value.rules === null
? cloneTempUnschedulablePolicy(fallback).rules
: readTempUnschedulableRules(value.rules, `${key}.rules`);
const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled);
let rules: CodexTempUnschedulableRule[];
if (value.rules === undefined || value.rules === null) {
if (fallback === undefined) throw new Error(`${codexPoolConfigPath}.${key}.rules is required`);
rules = cloneTempUnschedulablePolicy(fallback).rules;
} else {
rules = readTempUnschedulableRules(value.rules, `${key}.rules`);
}
if (enabled && rules.length === 0) throw new Error(`${codexPoolConfigPath}.${key}.rules must not be empty when enabled=true`);
return { enabled, rules };
}
@@ -1830,60 +1721,49 @@ function cloneTempUnschedulablePolicy(policy: CodexTempUnschedulablePolicy): Cod
};
}
function cloneCaddyEdgeRetryConfig(config: CodexPoolCaddyEdgeRetryConfig): CodexPoolCaddyEdgeRetryConfig {
return {
enabled: config.enabled,
tryDurationSeconds: config.tryDurationSeconds,
tryIntervalMilliseconds: config.tryIntervalMilliseconds,
retryMatch: {
methods: [...config.retryMatch.methods],
paths: [...config.retryMatch.paths],
},
};
}
function readBooleanConfig(value: unknown, key: string, fallback: boolean): boolean {
if (value === undefined || value === null) return fallback;
function readBooleanConfig(value: unknown, key: string, fallback?: boolean): boolean {
if (value === undefined || value === null) {
if (fallback !== undefined) return fallback;
throw new Error(`${codexPoolConfigPath}.${key} is required`);
}
const parsed = booleanValue(value);
if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`);
return parsed;
}
function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExposureConfig): CodexPoolPublicExposureConfig {
if (!isRecord(value)) return defaults;
const masterFrpsValue = isRecord(value.masterFrps) ? value.masterFrps : {};
const masterCaddyValue = isRecord(value.masterCaddy) ? value.masterCaddy : {};
function readPublicExposureConfig(value: unknown): CodexPoolPublicExposureConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.publicExposure must be a YAML object`);
const masterFrpsValue = requiredRecordConfigField(value, "masterFrps", "publicExposure");
const masterCaddyValue = requiredRecordConfigField(value, "masterCaddy", "publicExposure");
const config: CodexPoolPublicExposureConfig = {
enabled: booleanValue(value.enabled) ?? defaults.enabled,
proxyName: stringValue(value.proxyName) ?? defaults.proxyName,
configMapName: stringValue(value.configMapName) ?? defaults.configMapName,
deploymentName: stringValue(value.deploymentName) ?? defaults.deploymentName,
frpcImage: stringValue(value.frpcImage) ?? defaults.frpcImage,
serverAddr: stringValue(value.serverAddr) ?? defaults.serverAddr,
serverPort: numberValue(value.serverPort) ?? defaults.serverPort,
remotePort: numberValue(value.remotePort) ?? defaults.remotePort,
localIP: stringValue(value.localIP) ?? defaults.localIP,
localPort: numberValue(value.localPort) ?? defaults.localPort,
publicBaseUrl: normalizeBaseUrl(stringValue(value.publicBaseUrl)) ?? defaults.publicBaseUrl,
masterBaseUrl: normalizeBaseUrl(stringValue(value.masterBaseUrl)) ?? defaults.masterBaseUrl,
enabled: readBooleanConfig(value.enabled, "publicExposure.enabled"),
proxyName: requiredStringConfigField(value, "proxyName", "publicExposure"),
configMapName: requiredStringConfigField(value, "configMapName", "publicExposure"),
deploymentName: requiredStringConfigField(value, "deploymentName", "publicExposure"),
frpcImage: requiredStringConfigField(value, "frpcImage", "publicExposure"),
serverAddr: requiredStringConfigField(value, "serverAddr", "publicExposure"),
serverPort: readRequiredPort(value.serverPort, "publicExposure.serverPort"),
remotePort: readRequiredPort(value.remotePort, "publicExposure.remotePort"),
localIP: requiredStringConfigField(value, "localIP", "publicExposure"),
localPort: readRequiredPort(value.localPort, "publicExposure.localPort"),
publicBaseUrl: readRequiredBaseUrl(value.publicBaseUrl, "publicExposure.publicBaseUrl"),
masterBaseUrl: readRequiredBaseUrl(value.masterBaseUrl, "publicExposure.masterBaseUrl"),
masterFrps: {
configPath: stringValue(masterFrpsValue.configPath) ?? defaults.masterFrps.configPath,
containerName: stringValue(masterFrpsValue.containerName) ?? defaults.masterFrps.containerName,
configPath: requiredStringConfigField(masterFrpsValue, "configPath", "publicExposure.masterFrps"),
containerName: requiredStringConfigField(masterFrpsValue, "containerName", "publicExposure.masterFrps"),
},
masterCaddy: {
enabled: booleanValue(masterCaddyValue.enabled) ?? defaults.masterCaddy.enabled,
domain: stringValue(masterCaddyValue.domain) ?? defaults.masterCaddy.domain,
configPath: stringValue(masterCaddyValue.configPath) ?? defaults.masterCaddy.configPath,
serviceName: stringValue(masterCaddyValue.serviceName) ?? defaults.masterCaddy.serviceName,
upstreamBaseUrl: normalizeBaseUrl(stringValue(masterCaddyValue.upstreamBaseUrl)) ?? defaults.masterCaddy.upstreamBaseUrl,
enabled: readBooleanConfig(masterCaddyValue.enabled, "publicExposure.masterCaddy.enabled"),
domain: requiredStringConfigField(masterCaddyValue, "domain", "publicExposure.masterCaddy"),
configPath: requiredStringConfigField(masterCaddyValue, "configPath", "publicExposure.masterCaddy"),
serviceName: requiredStringConfigField(masterCaddyValue, "serviceName", "publicExposure.masterCaddy"),
upstreamBaseUrl: readRequiredBaseUrl(masterCaddyValue.upstreamBaseUrl, "publicExposure.masterCaddy.upstreamBaseUrl"),
responseHeaderTimeoutSeconds: readCaddyTimeoutSeconds(
masterCaddyValue.responseHeaderTimeoutSeconds,
"publicExposure.masterCaddy.responseHeaderTimeoutSeconds",
defaults.masterCaddy.responseHeaderTimeoutSeconds,
),
edgeRetry: readCaddyEdgeRetryConfig(
masterCaddyValue.edgeRetry,
defaults.masterCaddy.edgeRetry,
"publicExposure.masterCaddy.edgeRetry",
),
},
@@ -1907,17 +1787,17 @@ function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExpos
return config;
}
function readLocalCodexConfig(value: unknown, defaults: CodexPoolLocalCodexConfig): CodexPoolLocalCodexConfig {
if (!isRecord(value)) return defaults;
function readLocalCodexConfig(value: unknown): CodexPoolLocalCodexConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.localCodex must be a YAML object`);
const config = {
backupSuffix: stringValue(value.backupSuffix) ?? defaults.backupSuffix,
providerName: stringValue(value.providerName) ?? defaults.providerName,
wireApi: stringValue(value.wireApi) ?? defaults.wireApi,
modelContextWindow: readPositiveIntegerConfig(value.modelContextWindow, "localCodex.modelContextWindow", defaults.modelContextWindow),
modelAutoCompactTokenLimit: readPositiveIntegerConfig(value.modelAutoCompactTokenLimit, "localCodex.modelAutoCompactTokenLimit", defaults.modelAutoCompactTokenLimit),
supportsWebSockets: readBooleanConfig(value.supportsWebSockets, "localCodex.supportsWebSockets", defaults.supportsWebSockets),
responsesWebSocketsV2: readBooleanConfig(value.responsesWebSocketsV2, "localCodex.responsesWebSocketsV2", defaults.responsesWebSocketsV2),
responsesSmokeModel: stringValue(value.responsesSmokeModel) ?? defaults.responsesSmokeModel,
backupSuffix: requiredStringConfigField(value, "backupSuffix", "localCodex"),
providerName: requiredStringConfigField(value, "providerName", "localCodex"),
wireApi: requiredStringConfigField(value, "wireApi", "localCodex"),
modelContextWindow: readPositiveIntegerConfig(value.modelContextWindow, "localCodex.modelContextWindow"),
modelAutoCompactTokenLimit: readPositiveIntegerConfig(value.modelAutoCompactTokenLimit, "localCodex.modelAutoCompactTokenLimit"),
supportsWebSockets: readBooleanConfig(value.supportsWebSockets, "localCodex.supportsWebSockets"),
responsesWebSocketsV2: readBooleanConfig(value.responsesWebSocketsV2, "localCodex.responsesWebSocketsV2"),
responsesSmokeModel: requiredStringConfigField(value, "responsesSmokeModel", "localCodex"),
};
if (!/^[A-Za-z0-9._-]+$/u.test(config.backupSuffix)) throw new Error(`${codexPoolConfigPath}.localCodex.backupSuffix has an unsupported format`);
validateProxyName(config.providerName, "localCodex.providerName");
@@ -2054,9 +1934,11 @@ function codexPoolConfigSummary(pool: CodexPoolConfig): Record<string, unknown>
kind: pool.kind,
metadata: pool.metadata,
groupName: pool.groupName,
groupDescription: pool.groupDescription,
apiKeyName: pool.apiKeyName,
apiKeySecretName: pool.apiKeySecretName,
apiKeySecretKey: pool.apiKeySecretKey,
adminEmailDefault: pool.adminEmailDefault,
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
minOwnerConcurrency: pool.minOwnerConcurrency,
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
@@ -3676,6 +3558,41 @@ function requiredStringConfigField(obj: Record<string, unknown>, key: string, pa
return value;
}
function readRequiredDescription(value: unknown, key: string, maxChars: number): string {
const text = stringValue(value);
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`);
if (text.length > maxChars) throw new Error(`${codexPoolConfigPath}.${key} must be at most ${maxChars} characters`);
if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
return text;
}
function readRequiredEmail(value: unknown, key: string): string {
const text = stringValue(value);
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`);
if (!/^[^@\s]+@[^@\s]+[.][^@\s]+$/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must be an email address`);
return text;
}
function readRequiredBaseUrl(value: unknown, key: string): string {
const baseUrl = normalizeBaseUrl(stringValue(value));
if (baseUrl === null) throw new Error(`${codexPoolConfigPath}.${key} must be a valid http(s) URL`);
return baseUrl;
}
function readRequiredPort(value: unknown, key: string): number {
const port = numberValue(value);
if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`);
}
return port;
}
function readRequiredPositiveNumber(value: unknown, key: string): number {
const number = numberValue(value);
if (number === null || number <= 0) throw new Error(`${codexPoolConfigPath}.${key} must be > 0`);
return number;
}
function requiredRecordConfigField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
@@ -4151,11 +4068,8 @@ function desiredAccountCapacityTotal(pool: CodexPoolConfig): number {
function desiredAccountCapacityMap(pool: CodexPoolConfig): Record<string, number> {
const codexDir = join(homedir(), ".codex");
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultSentinelProtect, pool.defaultAccountPriority);
const capacities: Record<string, number> = {};
for (const entry of configs) {
for (const entry of pool.profiles) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
seenAccountNames.add(accountName);
capacities[accountName] = entry.capacity ?? pool.defaultAccountCapacity;
@@ -4166,11 +4080,8 @@ function desiredAccountCapacityMap(pool: CodexPoolConfig): Record<string, number
function desiredAccountLoadFactorMap(pool: CodexPoolConfig): Record<string, number> {
const codexDir = join(homedir(), ".codex");
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultSentinelProtect, pool.defaultAccountPriority);
const loadFactors: Record<string, number> = {};
for (const entry of configs) {
for (const entry of pool.profiles) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
seenAccountNames.add(accountName);
loadFactors[accountName] = entry.loadFactor ?? pool.defaultAccountLoadFactor;
@@ -4181,11 +4092,8 @@ function desiredAccountLoadFactorMap(pool: CodexPoolConfig): Record<string, numb
function desiredAccountWebSocketsV2ModeMap(pool: CodexPoolConfig): Record<string, OpenAIResponsesWebSocketsV2Mode | null> {
const codexDir = join(homedir(), ".codex");
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultSentinelProtect, pool.defaultAccountPriority);
const modes: Record<string, OpenAIResponsesWebSocketsV2Mode | null> = {};
for (const entry of configs) {
for (const entry of pool.profiles) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
seenAccountNames.add(accountName);
modes[accountName] = entry.openaiResponsesWebSocketsV2Mode;
@@ -4196,11 +4104,8 @@ function desiredAccountWebSocketsV2ModeMap(pool: CodexPoolConfig): Record<string
function desiredAccountTempUnschedulableMap(pool: CodexPoolConfig): Record<string, unknown> {
const codexDir = join(homedir(), ".codex");
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultSentinelProtect, pool.defaultAccountPriority);
const policies: Record<string, unknown> = {};
for (const entry of configs) {
for (const entry of pool.profiles) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
seenAccountNames.add(accountName);
policies[accountName] = renderSub2ApiTempUnschedulableCredentials(entry.tempUnschedulable);
@@ -4230,9 +4135,11 @@ SERVICE_DNS = ${JSON.stringify(target.serviceDns)}
FIELD_MANAGER = "${fieldManager}"
APP_SECRET_NAME = ${JSON.stringify(target.appSecretName)}
POOL_GROUP_NAME = "${pool.groupName}"
POOL_GROUP_DESCRIPTION = ${JSON.stringify(pool.groupDescription)}
POOL_API_KEY_NAME = "${pool.apiKeyName}"
POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}"
POOL_ADMIN_EMAIL_DEFAULT = ${JSON.stringify(pool.adminEmailDefault)}
MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)}
MIN_OWNER_CONCURRENCY = ${JSON.stringify(pool.minOwnerConcurrency)}
MIN_OWNER_CONCURRENCY_SOURCE = ${JSON.stringify(pool.minOwnerConcurrencySource)}
@@ -4507,7 +4414,7 @@ def find_access_token(data):
return None
def login():
admin_email = get_config_value("sub2api-config", "ADMIN_EMAIL") or "admin@sub2api.platform-infra.local"
admin_email = get_config_value("sub2api-config", "ADMIN_EMAIL") or POOL_ADMIN_EMAIL_DEFAULT
admin_password = decode_secret_value(APP_SECRET_NAME, "ADMIN_PASSWORD")
if not admin_password:
raise RuntimeError("ADMIN_PASSWORD missing from sub2api-secrets")
@@ -4523,7 +4430,7 @@ def login():
def group_payload():
return {
"name": POOL_GROUP_NAME,
"description": "UniDesk-managed Codex API-key pool for G14 k3s internal Sub2API clients.",
"description": POOL_GROUP_DESCRIPTION,
"platform": "openai",
"rate_multiplier": 1,
"is_exclusive": False,