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
@@ -10,9 +10,11 @@ metadata:
pool:
groupName: unidesk-codex-pool
groupDescription: UniDesk-managed Codex API-key pool for YAML-selected Sub2API clients.
apiKeyName: unidesk-codex-pool-api-key
apiKeySecretName: sub2api-codex-pool-api-key
apiKeySecretKey: API_KEY
adminEmailDefault: admin@sub2api.platform-infra.local
minOwnerBalanceUsd: 1000
defaultAccountPriority: 10
defaultAccountCapacity: 10
@@ -92,76 +92,13 @@ export interface CodexPoolSentinelManifestOptions {
serviceName: string;
serviceDns: string;
appSecretName: string;
adminEmailDefault: string;
proxy?: {
httpProxy: string;
noProxy: string;
} | null;
}
export function defaultCodexPoolSentinelConfig(): CodexPoolSentinelConfig {
return {
monitor: {
enabled: true,
},
actions: {
enabled: false,
},
schedule: "*/1 * * * *",
image: "python:3.12-alpine",
serviceAccountName: "sub2api-account-sentinel",
configMapName: "sub2api-account-sentinel-config",
credentialsSecretName: "sub2api-account-sentinel-profiles",
stateConfigMapName: "sub2api-account-sentinel-state",
cronJobName: "sub2api-account-sentinel",
roleName: "sub2api-account-sentinel",
roleBindingName: "sub2api-account-sentinel",
model: "gpt-5.5",
endpoint: "responses",
marker: {
prefix: "UDSG_OK",
exact: true,
},
probe: {
timeoutSeconds: 30,
maxOutputTokens: 16,
transportRetryMinutes: 5,
userAgent: "Go-http-client/1.1",
},
gatewayFailureMonitor: {
enabled: false,
lookbackSeconds: 900,
tailLines: 4000,
initialTtlMinutes: 5,
maxTtlMinutes: 30,
backoffMultiplier: 2,
paths: ["/responses", "/v1/responses", "/responses/compact", "/v1/responses/compact"],
},
sdk: {
openaiPythonVersion: "2.41.1",
},
cadence: {
successInitialIntervalMinutes: 1,
successMaxIntervalMinutes: 20,
trustedSuccessMaxIntervalMinutes: 20,
untrustedSuccessMaxIntervalMinutes: 2,
successBackoffMultiplier: 2,
jitterPercent: 10,
},
freeze: {
initialTtlMinutes: 1,
maxTtlMinutes: 10,
backoffMultiplier: 2,
jitterPercent: 10,
},
pricing: {
usdPer1MInputTokens: 1.25,
usdPer1MOutputTokens: 10,
},
historyLimit: 200,
protectedManualAccounts: [],
};
}
export function codexPoolSentinelRuntimeImage(config: CodexPoolSentinelConfig): CodexPoolSentinelImageTarget {
const baseTag = config.image
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
@@ -177,28 +114,25 @@ export function codexPoolSentinelRuntimeImage(config: CodexPoolSentinelConfig):
};
}
export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolSentinelConfig, sourcePath: string): CodexPoolSentinelConfig {
if (!isRecord(value)) return defaults;
const monitor = isRecord(value.monitor) ? value.monitor : {};
const actions = isRecord(value.actions) ? value.actions : {};
const marker = isRecord(value.marker) ? value.marker : {};
const probe = isRecord(value.probe) ? value.probe : {};
const gatewayFailureMonitor = isRecord(value.gatewayFailureMonitor) ? value.gatewayFailureMonitor : {};
const sdk = isRecord(value.sdk) ? value.sdk : {};
const cadence = isRecord(value.cadence) ? value.cadence : {};
const freeze = isRecord(value.freeze) ? value.freeze : {};
const pricing = isRecord(value.pricing) ? value.pricing : {};
const legacySuccessMax = readInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, defaults.cadence.successMaxIntervalMinutes, 1, 1440);
const trustedSuccessMax = readInt(valueAt(cadence, "trustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
const untrustedSuccessMax = readInt(valueAt(cadence, "untrustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
export function readCodexPoolSentinelConfig(value: unknown, sourcePath: string): CodexPoolSentinelConfig {
if (!isRecord(value)) throw new Error(`${sourcePath}.sentinel must be a YAML object`);
const monitor = readRequiredRecord(valueAt(value, "monitor"), `${sourcePath}.sentinel.monitor`);
const actions = readRequiredRecord(valueAt(value, "actions"), `${sourcePath}.sentinel.actions`);
const marker = readRequiredRecord(valueAt(value, "marker"), `${sourcePath}.sentinel.marker`);
const probe = readRequiredRecord(valueAt(value, "probe"), `${sourcePath}.sentinel.probe`);
const gatewayFailureMonitor = readRequiredRecord(valueAt(value, "gatewayFailureMonitor"), `${sourcePath}.sentinel.gatewayFailureMonitor`);
const sdk = readRequiredRecord(valueAt(value, "sdk"), `${sourcePath}.sentinel.sdk`);
const cadence = readRequiredRecord(valueAt(value, "cadence"), `${sourcePath}.sentinel.cadence`);
const freeze = readRequiredRecord(valueAt(value, "freeze"), `${sourcePath}.sentinel.freeze`);
const pricing = readRequiredRecord(valueAt(value, "pricing"), `${sourcePath}.sentinel.pricing`);
const config: CodexPoolSentinelConfig = {
monitor: {
enabled: readBoolean(valueAt(monitor, "enabled"), `${sourcePath}.sentinel.monitor.enabled`, defaults.monitor.enabled),
enabled: readRequiredBoolean(valueAt(monitor, "enabled"), `${sourcePath}.sentinel.monitor.enabled`),
},
actions: {
enabled: readBoolean(valueAt(actions, "enabled"), `${sourcePath}.sentinel.actions.enabled`, defaults.actions.enabled),
enabled: readRequiredBoolean(valueAt(actions, "enabled"), `${sourcePath}.sentinel.actions.enabled`),
},
schedule: readString(valueAt(value, "schedule"), `${sourcePath}.sentinel.schedule`, defaults.schedule),
schedule: readRequiredString(valueAt(value, "schedule"), `${sourcePath}.sentinel.schedule`),
image: readRequiredImage(valueAt(value, "image"), `${sourcePath}.sentinel.image`),
serviceAccountName: readRequiredDnsName(valueAt(value, "serviceAccountName"), `${sourcePath}.sentinel.serviceAccountName`),
configMapName: readRequiredDnsName(valueAt(value, "configMapName"), `${sourcePath}.sentinel.configMapName`),
@@ -210,46 +144,46 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
model: readRequiredModelName(valueAt(value, "model"), `${sourcePath}.sentinel.model`),
endpoint: readRequiredEndpoint(valueAt(value, "endpoint"), `${sourcePath}.sentinel.endpoint`),
marker: {
prefix: readMarkerPrefix(valueAt(marker, "prefix"), `${sourcePath}.sentinel.marker.prefix`, defaults.marker.prefix),
exact: readBoolean(valueAt(marker, "exact"), `${sourcePath}.sentinel.marker.exact`, defaults.marker.exact),
prefix: readRequiredMarkerPrefix(valueAt(marker, "prefix"), `${sourcePath}.sentinel.marker.prefix`),
exact: readRequiredBoolean(valueAt(marker, "exact"), `${sourcePath}.sentinel.marker.exact`),
},
probe: {
timeoutSeconds: readInt(valueAt(probe, "timeoutSeconds"), `${sourcePath}.sentinel.probe.timeoutSeconds`, defaults.probe.timeoutSeconds, 3, 300),
maxOutputTokens: readInt(valueAt(probe, "maxOutputTokens"), `${sourcePath}.sentinel.probe.maxOutputTokens`, defaults.probe.maxOutputTokens, 1, 128),
transportRetryMinutes: readInt(valueAt(probe, "transportRetryMinutes"), `${sourcePath}.sentinel.probe.transportRetryMinutes`, defaults.probe.transportRetryMinutes, 1, 120),
userAgent: readUserAgent(valueAt(probe, "userAgent"), `${sourcePath}.sentinel.probe.userAgent`, defaults.probe.userAgent),
timeoutSeconds: readRequiredInt(valueAt(probe, "timeoutSeconds"), `${sourcePath}.sentinel.probe.timeoutSeconds`, 3, 300),
maxOutputTokens: readRequiredInt(valueAt(probe, "maxOutputTokens"), `${sourcePath}.sentinel.probe.maxOutputTokens`, 1, 128),
transportRetryMinutes: readRequiredInt(valueAt(probe, "transportRetryMinutes"), `${sourcePath}.sentinel.probe.transportRetryMinutes`, 1, 120),
userAgent: readRequiredUserAgent(valueAt(probe, "userAgent"), `${sourcePath}.sentinel.probe.userAgent`),
},
gatewayFailureMonitor: {
enabled: readBoolean(valueAt(gatewayFailureMonitor, "enabled"), `${sourcePath}.sentinel.gatewayFailureMonitor.enabled`, defaults.gatewayFailureMonitor.enabled),
lookbackSeconds: readInt(valueAt(gatewayFailureMonitor, "lookbackSeconds"), `${sourcePath}.sentinel.gatewayFailureMonitor.lookbackSeconds`, defaults.gatewayFailureMonitor.lookbackSeconds, 60, 7200),
tailLines: readInt(valueAt(gatewayFailureMonitor, "tailLines"), `${sourcePath}.sentinel.gatewayFailureMonitor.tailLines`, defaults.gatewayFailureMonitor.tailLines, 100, 50000),
initialTtlMinutes: readInt(valueAt(gatewayFailureMonitor, "initialTtlMinutes"), `${sourcePath}.sentinel.gatewayFailureMonitor.initialTtlMinutes`, defaults.gatewayFailureMonitor.initialTtlMinutes, 1, 1440),
maxTtlMinutes: readInt(valueAt(gatewayFailureMonitor, "maxTtlMinutes"), `${sourcePath}.sentinel.gatewayFailureMonitor.maxTtlMinutes`, defaults.gatewayFailureMonitor.maxTtlMinutes, 1, 1440),
backoffMultiplier: readInt(valueAt(gatewayFailureMonitor, "backoffMultiplier"), `${sourcePath}.sentinel.gatewayFailureMonitor.backoffMultiplier`, defaults.gatewayFailureMonitor.backoffMultiplier, 1, 10),
paths: readPathList(valueAt(gatewayFailureMonitor, "paths"), `${sourcePath}.sentinel.gatewayFailureMonitor.paths`, defaults.gatewayFailureMonitor.paths),
enabled: readRequiredBoolean(valueAt(gatewayFailureMonitor, "enabled"), `${sourcePath}.sentinel.gatewayFailureMonitor.enabled`),
lookbackSeconds: readRequiredInt(valueAt(gatewayFailureMonitor, "lookbackSeconds"), `${sourcePath}.sentinel.gatewayFailureMonitor.lookbackSeconds`, 60, 7200),
tailLines: readRequiredInt(valueAt(gatewayFailureMonitor, "tailLines"), `${sourcePath}.sentinel.gatewayFailureMonitor.tailLines`, 100, 50000),
initialTtlMinutes: readRequiredInt(valueAt(gatewayFailureMonitor, "initialTtlMinutes"), `${sourcePath}.sentinel.gatewayFailureMonitor.initialTtlMinutes`, 1, 1440),
maxTtlMinutes: readRequiredInt(valueAt(gatewayFailureMonitor, "maxTtlMinutes"), `${sourcePath}.sentinel.gatewayFailureMonitor.maxTtlMinutes`, 1, 1440),
backoffMultiplier: readRequiredInt(valueAt(gatewayFailureMonitor, "backoffMultiplier"), `${sourcePath}.sentinel.gatewayFailureMonitor.backoffMultiplier`, 1, 10),
paths: readRequiredPathList(valueAt(gatewayFailureMonitor, "paths"), `${sourcePath}.sentinel.gatewayFailureMonitor.paths`),
},
sdk: {
openaiPythonVersion: readRequiredOpenAiPythonVersion(valueAt(sdk, "openaiPythonVersion"), `${sourcePath}.sentinel.sdk.openaiPythonVersion`),
},
cadence: {
successInitialIntervalMinutes: readInt(valueAt(cadence, "successInitialIntervalMinutes"), `${sourcePath}.sentinel.cadence.successInitialIntervalMinutes`, defaults.cadence.successInitialIntervalMinutes, 1, 1440),
successMaxIntervalMinutes: legacySuccessMax,
trustedSuccessMaxIntervalMinutes: trustedSuccessMax,
untrustedSuccessMaxIntervalMinutes: untrustedSuccessMax,
successBackoffMultiplier: readInt(valueAt(cadence, "successBackoffMultiplier"), `${sourcePath}.sentinel.cadence.successBackoffMultiplier`, defaults.cadence.successBackoffMultiplier, 1, 10),
jitterPercent: readInt(valueAt(cadence, "jitterPercent"), `${sourcePath}.sentinel.cadence.jitterPercent`, defaults.cadence.jitterPercent, 0, 50),
successInitialIntervalMinutes: readRequiredInt(valueAt(cadence, "successInitialIntervalMinutes"), `${sourcePath}.sentinel.cadence.successInitialIntervalMinutes`, 1, 1440),
successMaxIntervalMinutes: readRequiredInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, 1, 1440),
trustedSuccessMaxIntervalMinutes: readRequiredInt(valueAt(cadence, "trustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes`, 1, 1440),
untrustedSuccessMaxIntervalMinutes: readRequiredInt(valueAt(cadence, "untrustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes`, 1, 1440),
successBackoffMultiplier: readRequiredInt(valueAt(cadence, "successBackoffMultiplier"), `${sourcePath}.sentinel.cadence.successBackoffMultiplier`, 1, 10),
jitterPercent: readRequiredInt(valueAt(cadence, "jitterPercent"), `${sourcePath}.sentinel.cadence.jitterPercent`, 0, 50),
},
freeze: {
initialTtlMinutes: readInt(valueAt(freeze, "initialTtlMinutes"), `${sourcePath}.sentinel.freeze.initialTtlMinutes`, defaults.freeze.initialTtlMinutes, 1, 1440),
maxTtlMinutes: readInt(valueAt(freeze, "maxTtlMinutes"), `${sourcePath}.sentinel.freeze.maxTtlMinutes`, defaults.freeze.maxTtlMinutes, 1, 1440),
backoffMultiplier: readInt(valueAt(freeze, "backoffMultiplier"), `${sourcePath}.sentinel.freeze.backoffMultiplier`, defaults.freeze.backoffMultiplier, 1, 10),
jitterPercent: readInt(valueAt(freeze, "jitterPercent"), `${sourcePath}.sentinel.freeze.jitterPercent`, defaults.freeze.jitterPercent, 0, 50),
initialTtlMinutes: readRequiredInt(valueAt(freeze, "initialTtlMinutes"), `${sourcePath}.sentinel.freeze.initialTtlMinutes`, 1, 1440),
maxTtlMinutes: readRequiredInt(valueAt(freeze, "maxTtlMinutes"), `${sourcePath}.sentinel.freeze.maxTtlMinutes`, 1, 1440),
backoffMultiplier: readRequiredInt(valueAt(freeze, "backoffMultiplier"), `${sourcePath}.sentinel.freeze.backoffMultiplier`, 1, 10),
jitterPercent: readRequiredInt(valueAt(freeze, "jitterPercent"), `${sourcePath}.sentinel.freeze.jitterPercent`, 0, 50),
},
pricing: {
usdPer1MInputTokens: readNumber(valueAt(pricing, "usdPer1MInputTokens"), `${sourcePath}.sentinel.pricing.usdPer1MInputTokens`, defaults.pricing.usdPer1MInputTokens, 0, 100000),
usdPer1MOutputTokens: readNumber(valueAt(pricing, "usdPer1MOutputTokens"), `${sourcePath}.sentinel.pricing.usdPer1MOutputTokens`, defaults.pricing.usdPer1MOutputTokens, 0, 100000),
usdPer1MInputTokens: readRequiredNumber(valueAt(pricing, "usdPer1MInputTokens"), `${sourcePath}.sentinel.pricing.usdPer1MInputTokens`, 0, 100000),
usdPer1MOutputTokens: readRequiredNumber(valueAt(pricing, "usdPer1MOutputTokens"), `${sourcePath}.sentinel.pricing.usdPer1MOutputTokens`, 0, 100000),
},
historyLimit: readInt(valueAt(value, "historyLimit"), `${sourcePath}.sentinel.historyLimit`, defaults.historyLimit, 1, 2000),
historyLimit: readRequiredInt(valueAt(value, "historyLimit"), `${sourcePath}.sentinel.historyLimit`, 1, 2000),
};
if (config.actions.enabled && !config.monitor.enabled) {
throw new Error(`${sourcePath}.sentinel.actions.enabled requires sentinel.monitor.enabled=true`);
@@ -321,7 +255,7 @@ export function renderCodexPoolSentinelManifest(
actions: config.actions,
service: {
baseUrl: `http://${options.serviceDns}`,
adminEmailDefault: "admin@sub2api.platform-infra.local",
adminEmailDefault: options.adminEmailDefault,
},
model: config.model,
endpoint: config.endpoint,
@@ -2098,27 +2032,19 @@ function valueAt(value: Record<string, unknown>, key: string): unknown {
return Object.prototype.hasOwnProperty.call(value, key) ? value[key] : undefined;
}
function readBoolean(value: unknown, key: string, fallback: boolean): boolean {
if (value === undefined || value === null) return fallback;
function readRequiredRecord(value: unknown, key: string): Record<string, unknown> {
if (!isRecord(value)) throw new Error(`${key} must be a YAML object`);
return value;
}
function readRequiredBoolean(value: unknown, key: string): boolean {
if (value === undefined || value === null) throw new Error(`${key} is required`);
if (typeof value === "boolean") return value;
if (typeof value === "string" && value.trim() === "true") return true;
if (typeof value === "string" && value.trim() === "false") return false;
throw new Error(`${key} must be a boolean`);
}
function readString(value: unknown, key: string, fallback: string): string {
if (value === undefined || value === null) return fallback;
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string`);
if (/[\r\n]/u.test(value)) throw new Error(`${key} must not contain newlines`);
return value.trim();
}
function readDnsName(value: unknown, key: string, fallback: string): string {
const text = readString(value, key, fallback);
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(text)) throw new Error(`${key} must be a Kubernetes DNS label`);
return text;
}
function readRequiredDnsName(value: unknown, key: string): string {
const text = readRequiredString(value, key);
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(text)) throw new Error(`${key} must be a Kubernetes DNS label`);
@@ -2156,21 +2082,21 @@ function readRequiredString(value: unknown, key: string): string {
return value.trim();
}
function readMarkerPrefix(value: unknown, key: string, fallback: string): string {
const text = readString(value, key, fallback);
function readRequiredMarkerPrefix(value: unknown, key: string): string {
const text = readRequiredString(value, key);
if (!/^[A-Za-z0-9_-]{2,32}$/u.test(text)) throw new Error(`${key} must be 2-32 chars of letters, digits, _ or -`);
return text;
}
function readUserAgent(value: unknown, key: string, fallback: string): string {
const text = readString(value, key, fallback);
function readRequiredUserAgent(value: unknown, key: string): string {
const text = readRequiredString(value, key);
if (/[\r\n]/u.test(text)) throw new Error(`${key} must not contain newlines`);
if (Buffer.byteLength(text, "utf8") > 200) throw new Error(`${key} must be at most 200 bytes`);
return text;
}
function readPathList(value: unknown, key: string, fallback: string[]): string[] {
if (value === undefined || value === null) return fallback;
function readRequiredPathList(value: unknown, key: string): string[] {
if (value === undefined || value === null) throw new Error(`${key} is required`);
if (!Array.isArray(value) || value.length === 0) throw new Error(`${key} must be a non-empty string array`);
const paths = value.map((item, index) => {
if (typeof item !== "string" || item.trim().length === 0) throw new Error(`${key}[${index}] must be a non-empty string`);
@@ -2181,15 +2107,15 @@ function readPathList(value: unknown, key: string, fallback: string[]): string[]
return [...new Set(paths)];
}
function readInt(value: unknown, key: string, fallback: number, min: number, max: number): number {
if (value === undefined || value === null) return fallback;
function readRequiredInt(value: unknown, key: string, min: number, max: number): number {
if (value === undefined || value === null) throw new Error(`${key} is required`);
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
if (!Number.isInteger(parsed) || parsed < min || parsed > max) throw new Error(`${key} must be an integer from ${min} to ${max}`);
return parsed;
}
function readNumber(value: unknown, key: string, fallback: number, min: number, max: number): number {
if (value === undefined || value === null) return fallback;
function readRequiredNumber(value: unknown, key: string, min: number, max: number): number {
if (value === undefined || value === null) throw new Error(`${key} is required`);
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
if (!Number.isFinite(parsed) || parsed < min || parsed > max) throw new Error(`${key} must be a number from ${min} to ${max}`);
return parsed;
+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,