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
@@ -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;