refactor: require yaml codex pool config
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user