feat: add sub2api codex upstream profiles
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user