feat: add sub2api codex upstream profiles

This commit is contained in:
Codex
2026-06-10 04:01:04 +00:00
parent 924a7c50f1
commit c0dde9d0e3
6 changed files with 80 additions and 26 deletions
+54 -19
View File
@@ -18,6 +18,7 @@ const defaultPoolApiKeyName = "unidesk-codex-pool-api-key";
const defaultPoolApiKeySecretName = "sub2api-codex-pool-api-key";
const defaultPoolApiKeySecretKey = "API_KEY";
const defaultMinOwnerBalanceUsd = 1000;
const defaultAccountPriority = 10;
interface DisclosureOptions {
full: boolean;
@@ -76,6 +77,7 @@ interface CodexPoolConfig {
apiKeySecretKey: string;
minOwnerBalanceUsd: number;
minOwnerConcurrency: number;
defaultAccountPriority: number;
defaultAccountCapacity: number;
defaultAccountLoadFactor: number;
defaultTempUnschedulable: CodexTempUnschedulablePolicy;
@@ -234,7 +236,7 @@ function codexPoolPlan(): Record<string, unknown> {
grouping: `All discovered Codex profiles are bound to one Sub2API group named ${pool.groupName}.`,
unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
publicExposure: pool.publicExposure.enabled
? `Master server consumers use ${pool.publicExposure.masterBaseUrl}; FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
? `Default Codex consumers use ${codexConsumerBaseUrl(pool)}; bounded master-local probes may use ${pool.publicExposure.masterBaseUrl}. FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
: "Public FRP exposure is disabled by YAML.",
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files; UniDesk-managed accounts removed from YAML are deleted from Sub2API.",
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
@@ -268,6 +270,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
apiKeySecretKey: pool.apiKeySecretKey,
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
minOwnerConcurrency: pool.minOwnerConcurrency,
defaultAccountPriority: pool.defaultAccountPriority,
defaultAccountCapacity: pool.defaultAccountCapacity,
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
},
@@ -409,7 +412,7 @@ async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOp
authPath,
backupConfigPath,
backupAuthPath,
baseUrl: pool.publicExposure.masterBaseUrl,
baseUrl: codexConsumerBaseUrl(pool),
providerName: pool.localCodex.providerName,
wireApi: pool.localCodex.wireApi,
supportsWebSockets: pool.localCodex.supportsWebSockets,
@@ -454,7 +457,9 @@ 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);
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority);
return configs.map((entry) => {
const resolved = resolveProfileFiles(codexDir, entry);
const profile = entry.profile;
@@ -526,7 +531,11 @@ function collectCodexProfiles(): CodexProfile[] {
});
}
function discoverCodexProfileConfigs(codexDir: string, defaultTempUnschedulable = defaultCodexTempUnschedulablePolicy()): CodexPoolProfileConfig[] {
function discoverCodexProfileConfigs(
codexDir: string,
defaultTempUnschedulable = defaultCodexTempUnschedulablePolicy(),
defaultPriority = defaultAccountPriority,
): CodexPoolProfileConfig[] {
return readdirSync(codexDir)
.filter((file) => file === "config.toml" || file.startsWith("config.toml."))
.sort((a, b) => {
@@ -546,7 +555,7 @@ function discoverCodexProfileConfigs(codexDir: string, defaultTempUnschedulable
fallbackAuthFile: null,
openaiResponsesWebSocketsV2Mode: null,
upstreamUserAgent: null,
priority: 1,
priority: defaultPriority,
capacity: null,
loadFactor: null,
tempUnschedulable: defaultTempUnschedulable,
@@ -579,10 +588,16 @@ function readCodexPoolConfig(): CodexPoolConfig {
apiKeySecretKey: stringValue(pool.apiKeySecretKey) ?? defaults.apiKeySecretKey,
minOwnerBalanceUsd: numberValue(pool.minOwnerBalanceUsd) ?? defaults.minOwnerBalanceUsd,
minOwnerConcurrency: readAccountCapacity(pool.minOwnerConcurrency, "pool.minOwnerConcurrency"),
defaultAccountPriority: readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority"),
defaultAccountCapacity: readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity"),
defaultAccountLoadFactor: readAccountLoadFactor(pool.defaultAccountLoadFactor, "pool.defaultAccountLoadFactor"),
defaultTempUnschedulable,
profiles: readProfileConfig(parsed.profiles, defaults.profiles, defaultTempUnschedulable),
profiles: readProfileConfig(
parsed.profiles,
defaults.profiles,
defaultTempUnschedulable,
readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority"),
),
publicExposure: readPublicExposureConfig(parsed.publicExposure, defaults.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex, defaults.localCodex),
};
@@ -608,6 +623,7 @@ function defaultCodexPoolConfig(): CodexPoolConfig {
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
// Only used for the no-YAML fallback path; real pool configs must declare pool.minOwnerConcurrency.
minOwnerConcurrency: 1,
defaultAccountPriority,
defaultAccountCapacity: 5,
defaultAccountLoadFactor: 1,
defaultTempUnschedulable: defaultCodexTempUnschedulablePolicy(),
@@ -716,7 +732,12 @@ export function defaultCodexTempUnschedulablePolicy(): CodexTempUnschedulablePol
};
}
function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[], defaultTempUnschedulable: CodexTempUnschedulablePolicy): CodexPoolProfileConfig[] {
function readProfileConfig(
value: unknown,
defaults: CodexPoolProfileConfig[],
defaultTempUnschedulable: CodexTempUnschedulablePolicy,
defaultPriority: number,
): CodexPoolProfileConfig[] {
if (!isRecord(value)) return defaults;
const entries = value.entries;
if (!Array.isArray(entries)) throw new Error(`${codexPoolConfigPath}.profiles.entries must be a YAML array`);
@@ -738,7 +759,7 @@ function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[], d
if (fallbackAuthFile !== null) validateCodexFileName(fallbackAuthFile, `profiles.entries[${index}].fallbackAuthFile`);
const openaiResponsesWebSocketsV2Mode = readOpenAIResponsesWebSocketsV2Mode(entry.openaiResponsesWebSocketsV2Mode, `profiles.entries[${index}].openaiResponsesWebSocketsV2Mode`);
const upstreamUserAgent = readUpstreamUserAgent(entry.upstreamUserAgent, `profiles.entries[${index}].upstreamUserAgent`);
const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`);
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);
@@ -776,8 +797,8 @@ function readUpstreamUserAgent(value: unknown, key: string): string | null {
return text;
}
function readAccountPriority(value: unknown, key: string): number {
if (value === undefined || value === null) return 1;
function readAccountPriority(value: unknown, key: string, fallback = defaultAccountPriority): number {
if (value === undefined || value === null) return fallback;
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`);
@@ -1043,6 +1064,7 @@ function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
apiKeyName: pool.apiKeyName,
apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
minOwnerConcurrency: pool.minOwnerConcurrency,
defaultAccountPriority: pool.defaultAccountPriority,
defaultAccountCapacity: pool.defaultAccountCapacity,
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
valuesPrinted: false,
@@ -1463,7 +1485,7 @@ function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string): Record<st
},
provider: {
name: pool.localCodex.providerName,
baseUrl: pool.publicExposure.masterBaseUrl,
baseUrl: codexConsumerBaseUrl(pool),
wireApi: pool.localCodex.wireApi,
supportsWebSockets: pool.localCodex.supportsWebSockets,
responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2,
@@ -1482,13 +1504,17 @@ function copyIfMissing(source: string, target: string): "created" | "kept-existi
function updateCodexConfigToml(current: string, pool: CodexPoolConfig): string {
return renderCodexLocalConsumerToml(current, {
providerName: pool.localCodex.providerName,
baseUrl: pool.publicExposure.masterBaseUrl,
baseUrl: codexConsumerBaseUrl(pool),
wireApi: pool.localCodex.wireApi,
supportsWebSockets: pool.localCodex.supportsWebSockets,
responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2,
});
}
function codexConsumerBaseUrl(pool: CodexPoolConfig): string {
return `${pool.publicExposure.publicBaseUrl.replace(/\/+$/u, "")}/`;
}
export function renderCodexLocalConsumerToml(current: string, options: CodexLocalConsumerTomlOptions): string {
let next = upsertTopLevelTomlString(current, "model_provider", options.providerName);
next = upsertProviderSection(next, options);
@@ -1566,7 +1592,7 @@ function upsertTomlSectionBoolean(text: string, sectionName: string, key: string
}
async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string): Promise<Record<string, unknown>> {
const probe = await probePublicModels(pool, "with-api-key", apiKey);
const probe = await probePublicModels(pool, "with-api-key", apiKey, "public");
return {
...probe,
ok: probe.ok === true,
@@ -1697,7 +1723,9 @@ function validateScript(pool: CodexPoolConfig): string {
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);
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority);
const capacities: Record<string, number> = {};
for (const entry of configs) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
@@ -1710,7 +1738,9 @@ 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);
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority);
const loadFactors: Record<string, number> = {};
for (const entry of configs) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
@@ -1723,7 +1753,9 @@ 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);
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority);
const modes: Record<string, OpenAIResponsesWebSocketsV2Mode | null> = {};
for (const entry of configs) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
@@ -1736,7 +1768,9 @@ 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);
const configs = pool.profiles.length > 0
? pool.profiles
: discoverCodexProfileConfigs(codexDir, pool.defaultTempUnschedulable, pool.defaultAccountPriority);
const policies: Record<string, unknown> = {};
for (const entry of configs) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
@@ -1770,6 +1804,7 @@ POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}"
MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)}
MIN_OWNER_CONCURRENCY = ${JSON.stringify(pool.minOwnerConcurrency)}
POOL_DEFAULT_ACCOUNT_PRIORITY = ${JSON.stringify(pool.defaultAccountPriority)}
POOL_DEFAULT_ACCOUNT_CAPACITY = ${JSON.stringify(pool.defaultAccountCapacity)}
POOL_DEFAULT_ACCOUNT_LOAD_FACTOR = ${JSON.stringify(pool.defaultAccountLoadFactor)}
RESPONSES_SMOKE_MODEL = ${JSON.stringify(pool.localCodex.responsesSmokeModel)}
@@ -2006,7 +2041,7 @@ def account_payload(profile, group_id):
"credentials": credentials,
"extra": extra,
"concurrency": int(profile.get("capacity", 5) or 5),
"priority": int(profile.get("priority", 1) or 1),
"priority": int(profile.get("priority", POOL_DEFAULT_ACCOUNT_PRIORITY) or POOL_DEFAULT_ACCOUNT_PRIORITY),
"rate_multiplier": 1,
"load_factor": int(profile.get("loadFactor", POOL_DEFAULT_ACCOUNT_LOAD_FACTOR) or POOL_DEFAULT_ACCOUNT_LOAD_FACTOR),
"group_ids": [group_id],
@@ -2040,7 +2075,7 @@ def ensure_accounts(token, profiles, group_id):
"apiKeySource": profile["apiKeySource"],
"apiKeyFingerprint": profile["apiKeyFingerprint"],
"openaiResponsesWebSocketsV2Mode": profile.get("openaiResponsesWebSocketsV2Mode"),
"priority": int(profile.get("priority", 1) or 1),
"priority": int(profile.get("priority", POOL_DEFAULT_ACCOUNT_PRIORITY) or POOL_DEFAULT_ACCOUNT_PRIORITY),
"capacity": int(profile.get("capacity", 5) or 5),
"loadFactor": int(profile.get("loadFactor", POOL_DEFAULT_ACCOUNT_LOAD_FACTOR) or POOL_DEFAULT_ACCOUNT_LOAD_FACTOR),
"runtimeConcurrency": data.get("concurrency") if isinstance(data, dict) else None,