feat: add Sub2API runtime configuration CRUD
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
||||
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
||||
|
||||
import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
|
||||
import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexRuntimeConfig, CodexRuntimeTemplate, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
|
||||
import { booleanValue, integerArrayConfigField, integerConfigField, isRecord, normalizeBaseUrl, numberValue, readRequiredBaseUrl, readRequiredDescription, readRequiredEmail, readRequiredPort, readRequiredPositiveNumber, requiredRecordConfigField, requiredStringConfigField, stringValue, validateKubernetesName } from "./config-utils";
|
||||
import { readAuthAPIKey, uniqueAccountName } from "./redaction";
|
||||
import { codexPoolConfigPath } from "./types";
|
||||
@@ -59,6 +59,7 @@ export function collectCodexProfiles(): CodexProfile[] {
|
||||
priority: entry.priority,
|
||||
capacity: entry.capacity ?? pool.defaultAccountCapacity,
|
||||
loadFactor: entry.loadFactor ?? pool.defaultAccountLoadFactor,
|
||||
tempUnschedulableTemplate: entry.tempUnschedulableTemplate,
|
||||
tempUnschedulable: entry.tempUnschedulable,
|
||||
authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing",
|
||||
ok: false,
|
||||
@@ -127,13 +128,21 @@ export 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");
|
||||
const runtime = readRuntimeConfig(parsed.runtime);
|
||||
const defaultTempUnschedulableTemplate = readRuntimeTemplateRef(
|
||||
pool.defaultTempUnschedulableTemplate,
|
||||
"pool.defaultTempUnschedulableTemplate",
|
||||
runtime,
|
||||
);
|
||||
const defaultTempUnschedulable = runtime.templatesById[defaultTempUnschedulableTemplate]!.tempUnschedulable;
|
||||
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");
|
||||
const profiles = readProfileConfig(
|
||||
parsed.profiles,
|
||||
runtime,
|
||||
defaultTempUnschedulableTemplate,
|
||||
defaultTempUnschedulable,
|
||||
defaultSentinelProtect,
|
||||
defaultAccountPriorityValue,
|
||||
@@ -165,9 +174,11 @@ export function readCodexPoolConfig(): CodexPoolConfig {
|
||||
defaultAccountPriority: defaultAccountPriorityValue,
|
||||
defaultAccountCapacity: defaultAccountCapacityValue,
|
||||
defaultAccountLoadFactor: defaultAccountLoadFactorValue,
|
||||
defaultTempUnschedulableTemplate,
|
||||
defaultTempUnschedulable,
|
||||
defaultSentinelProtect,
|
||||
profiles,
|
||||
runtime,
|
||||
manualAccounts,
|
||||
publicExposure: readPublicExposureConfig(parsed.publicExposure),
|
||||
localCodex: readLocalCodexConfig(parsed.localCodex),
|
||||
@@ -188,8 +199,46 @@ export function readCodexPoolConfig(): CodexPoolConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function readRuntimeConfig(value: unknown): CodexRuntimeConfig {
|
||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.runtime must be a YAML object`);
|
||||
if (!Array.isArray(value.templates)) throw new Error(`${codexPoolConfigPath}.runtime.templates must be a YAML array`);
|
||||
const templates: CodexRuntimeTemplate[] = value.templates.map((item, index) => {
|
||||
if (!isRecord(item)) throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}] must be a YAML object`);
|
||||
const id = requiredStringConfigField(item, "id", `runtime.templates[${index}]`);
|
||||
if (!/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(id)) {
|
||||
throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}].id has an unsupported format`);
|
||||
}
|
||||
const kind = requiredStringConfigField(item, "kind", `runtime.templates[${index}]`);
|
||||
if (kind !== "temp-unschedulable") {
|
||||
throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}].kind must be temp-unschedulable`);
|
||||
}
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
description: readRequiredDescription(item.description, `runtime.templates[${index}].description`, 300),
|
||||
tempUnschedulable: readTempUnschedulablePolicy(item.spec, `runtime.templates[${index}].spec`),
|
||||
};
|
||||
});
|
||||
if (templates.length === 0) throw new Error(`${codexPoolConfigPath}.runtime.templates must not be empty`);
|
||||
const templatesById: Record<string, CodexRuntimeTemplate> = {};
|
||||
for (const template of templates) {
|
||||
if (templatesById[template.id] !== undefined) throw new Error(`${codexPoolConfigPath}.runtime.templates duplicates id ${template.id}`);
|
||||
templatesById[template.id] = template;
|
||||
}
|
||||
return { templates, templatesById };
|
||||
}
|
||||
|
||||
export function readRuntimeTemplateRef(value: unknown, key: string, runtime: CodexRuntimeConfig): string {
|
||||
const id = stringValue(value);
|
||||
if (id === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty template id`);
|
||||
if (runtime.templatesById[id] === undefined) throw new Error(`${codexPoolConfigPath}.${key} references missing runtime template ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function readProfileConfig(
|
||||
value: unknown,
|
||||
runtime: CodexRuntimeConfig,
|
||||
defaultTempUnschedulableTemplate: string,
|
||||
defaultTempUnschedulable: CodexTempUnschedulablePolicy,
|
||||
defaultSentinelProtect: CodexSentinelProtectPolicy,
|
||||
defaultPriority: number,
|
||||
@@ -221,7 +270,13 @@ export function readProfileConfig(
|
||||
const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`, defaultPriority);
|
||||
const capacity = entry.capacity === undefined || entry.capacity === null ? null : readAccountCapacity(entry.capacity, `profiles.entries[${index}].capacity`);
|
||||
const loadFactor = entry.loadFactor === undefined || entry.loadFactor === null ? null : readAccountLoadFactor(entry.loadFactor, `profiles.entries[${index}].loadFactor`);
|
||||
const tempUnschedulable = readTempUnschedulablePolicy(entry.tempUnschedulable, `profiles.entries[${index}].tempUnschedulable`, defaultTempUnschedulable);
|
||||
if (entry.tempUnschedulable !== undefined) {
|
||||
throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}].tempUnschedulable was replaced by tempUnschedulableTemplate`);
|
||||
}
|
||||
const tempUnschedulableTemplate = entry.tempUnschedulableTemplate === undefined || entry.tempUnschedulableTemplate === null
|
||||
? defaultTempUnschedulableTemplate
|
||||
: readRuntimeTemplateRef(entry.tempUnschedulableTemplate, `profiles.entries[${index}].tempUnschedulableTemplate`, runtime);
|
||||
const tempUnschedulable = runtime.templatesById[tempUnschedulableTemplate]?.tempUnschedulable ?? defaultTempUnschedulable;
|
||||
return {
|
||||
profile,
|
||||
accountName,
|
||||
@@ -236,6 +291,7 @@ export function readProfileConfig(
|
||||
priority,
|
||||
capacity,
|
||||
loadFactor,
|
||||
tempUnschedulableTemplate,
|
||||
tempUnschedulable,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user