1909 lines
75 KiB
TypeScript
1909 lines
75 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import type { UniDeskConfig } from "./config";
|
|
import { rootPath } from "./config";
|
|
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
|
|
|
const g14K3sRoute = "G14:k3s";
|
|
const namespace = "platform-infra";
|
|
const serviceName = "sub2api";
|
|
const serviceDns = `${serviceName}.${namespace}.svc.cluster.local:8080`;
|
|
const fieldManager = "unidesk-platform-infra";
|
|
const appSecretName = "sub2api-secrets";
|
|
const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
|
|
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;
|
|
|
|
interface DisclosureOptions {
|
|
full: boolean;
|
|
raw: boolean;
|
|
}
|
|
|
|
interface SyncOptions extends DisclosureOptions {
|
|
confirm: boolean;
|
|
}
|
|
|
|
interface ConfirmOptions extends DisclosureOptions {
|
|
confirm: boolean;
|
|
}
|
|
|
|
interface CodexProfile {
|
|
profile: string;
|
|
accountName: string;
|
|
configFile: string;
|
|
authFile: string;
|
|
provider: string;
|
|
baseUrl: string;
|
|
wireApi: string | null;
|
|
model: string | null;
|
|
envKey: string | null;
|
|
apiKey: string | null;
|
|
apiKeySource: "auth-json" | "env" | null;
|
|
openaiResponsesWebSocketsV2Mode: OpenAIResponsesWebSocketsV2Mode | null;
|
|
upstreamUserAgent: string | null;
|
|
priority: number;
|
|
capacity: number;
|
|
authOpenAIKeyShape: string;
|
|
ok: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
type OpenAIResponsesWebSocketsV2Mode = "off" | "ctx_pool" | "passthrough";
|
|
|
|
interface CodexPoolConfig {
|
|
groupName: string;
|
|
apiKeyName: string;
|
|
apiKeySecretName: string;
|
|
apiKeySecretKey: string;
|
|
minOwnerBalanceUsd: number;
|
|
defaultAccountCapacity: number;
|
|
profiles: CodexPoolProfileConfig[];
|
|
publicExposure: CodexPoolPublicExposureConfig;
|
|
localCodex: CodexPoolLocalCodexConfig;
|
|
}
|
|
|
|
interface CodexPoolProfileConfig {
|
|
profile: string;
|
|
accountName: string | null;
|
|
configFile: string;
|
|
authFile: string;
|
|
fallbackConfigFile: string | null;
|
|
fallbackAuthFile: string | null;
|
|
openaiResponsesWebSocketsV2Mode: OpenAIResponsesWebSocketsV2Mode | null;
|
|
upstreamUserAgent: string | null;
|
|
priority: number;
|
|
capacity: number | null;
|
|
}
|
|
|
|
interface CodexPoolPublicExposureConfig {
|
|
enabled: boolean;
|
|
proxyName: string;
|
|
configMapName: string;
|
|
deploymentName: string;
|
|
frpcImage: string;
|
|
serverAddr: string;
|
|
serverPort: number;
|
|
remotePort: number;
|
|
localIP: string;
|
|
localPort: number;
|
|
publicBaseUrl: string;
|
|
masterBaseUrl: string;
|
|
masterFrps: {
|
|
configPath: string;
|
|
containerName: string;
|
|
};
|
|
}
|
|
|
|
interface CodexPoolLocalCodexConfig {
|
|
backupSuffix: string;
|
|
providerName: string;
|
|
wireApi: string;
|
|
}
|
|
|
|
export function codexPoolHelp(): unknown {
|
|
const pool = readCodexPoolConfig();
|
|
return {
|
|
command: "platform-infra sub2api codex-pool plan|sync|validate|expose|configure-local",
|
|
output: "json",
|
|
usage: [
|
|
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
|
|
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
|
|
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--full|--raw]",
|
|
"bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
|
|
"bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
|
|
],
|
|
description: "Import YAML-selected ~/.codex API-key profiles into one Sub2API OpenAI pool, expose one unified API_KEY, and optionally configure this master server's ~/.codex consumer endpoint.",
|
|
target: {
|
|
route: g14K3sRoute,
|
|
namespace,
|
|
serviceDns,
|
|
configPath: codexPoolConfigPath,
|
|
poolGroupName: pool.groupName,
|
|
poolApiKeySecretName: pool.apiKeySecretName,
|
|
poolApiKeySecretKey: pool.apiKeySecretKey,
|
|
publicBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.publicBaseUrl : null,
|
|
masterBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.masterBaseUrl : null,
|
|
secretValuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function runCodexPoolCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
|
|
const [action = "plan"] = args;
|
|
if (action === "plan") return codexPoolPlan();
|
|
if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1)));
|
|
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
|
|
if (action === "expose") return await codexPoolExpose(config, parseConfirmOptions(args.slice(1)));
|
|
if (action === "configure-local") return await codexPoolConfigureLocal(config, parseConfirmOptions(args.slice(1)));
|
|
return {
|
|
ok: false,
|
|
error: "unsupported-platform-infra-sub2api-codex-pool-command",
|
|
args,
|
|
help: codexPoolHelp(),
|
|
};
|
|
}
|
|
|
|
function parseSyncOptions(args: string[]): SyncOptions {
|
|
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
|
|
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
|
|
return { ...disclosure, confirm: args.includes("--confirm") };
|
|
}
|
|
|
|
function parseConfirmOptions(args: string[]): ConfirmOptions {
|
|
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
|
|
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
|
|
return { ...disclosure, confirm: args.includes("--confirm") };
|
|
}
|
|
|
|
function parseDisclosureOptions(args: string[]): DisclosureOptions {
|
|
validateOptions(args, new Set(["--full", "--raw"]));
|
|
const raw = args.includes("--raw");
|
|
return { full: raw || args.includes("--full"), raw };
|
|
}
|
|
|
|
function validateOptions(args: string[], booleanOptions: Set<string>): void {
|
|
for (const arg of args) {
|
|
if (booleanOptions.has(arg)) continue;
|
|
throw new Error(`unsupported option: ${arg}`);
|
|
}
|
|
}
|
|
|
|
function codexPoolPlan(): Record<string, unknown> {
|
|
const pool = readCodexPoolConfig();
|
|
const profiles = collectCodexProfiles();
|
|
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-plan",
|
|
source: {
|
|
directory: join(homedir(), ".codex"),
|
|
configPattern: "YAML-selected config files under ~/.codex",
|
|
authPattern: "YAML-selected auth files under ~/.codex",
|
|
valuesPrinted: false,
|
|
},
|
|
target: poolTarget(),
|
|
config: {
|
|
path: codexPoolConfigPath,
|
|
pool,
|
|
},
|
|
profiles: profiles.map(redactProfile),
|
|
decision: {
|
|
accountType: "openai/apikey",
|
|
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}.`
|
|
: "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.",
|
|
},
|
|
next: ok
|
|
? { sync: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" }
|
|
: { fix: "Ensure every discovered config.toml profile has a base_url and either auth.json OPENAI_API_KEY or the configured env_key present in this shell." },
|
|
};
|
|
}
|
|
|
|
async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const profiles = collectCodexProfiles();
|
|
const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
|
if (!options.confirm || !planOk) {
|
|
return {
|
|
...codexPoolPlan(),
|
|
ok: !options.confirm ? planOk : false,
|
|
mode: options.confirm ? "blocked-invalid-local-profile" : "dry-run",
|
|
next: options.confirm
|
|
? { fix: "Repair invalid local Codex profiles, then rerun sync --confirm." }
|
|
: { confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" },
|
|
};
|
|
}
|
|
|
|
const payload = {
|
|
pool: {
|
|
groupName: pool.groupName,
|
|
apiKeyName: pool.apiKeyName,
|
|
apiKeySecretName: pool.apiKeySecretName,
|
|
apiKeySecretKey: pool.apiKeySecretKey,
|
|
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
|
|
defaultAccountCapacity: pool.defaultAccountCapacity,
|
|
},
|
|
profiles: profiles.map((profile) => ({
|
|
profile: profile.profile,
|
|
accountName: profile.accountName,
|
|
configFile: profile.configFile,
|
|
authFile: profile.authFile,
|
|
provider: profile.provider,
|
|
baseUrl: profile.baseUrl,
|
|
wireApi: profile.wireApi,
|
|
model: profile.model,
|
|
apiKey: profile.apiKey,
|
|
apiKeySource: profile.apiKeySource,
|
|
apiKeyFingerprint: fingerprint(profile.apiKey ?? ""),
|
|
openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent: profile.upstreamUserAgent,
|
|
priority: profile.priority,
|
|
capacity: profile.capacity,
|
|
})),
|
|
};
|
|
const result = await capture(config, g14K3sRoute, ["script"], syncScript(payload, pool));
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sync",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sync",
|
|
local: {
|
|
profiles: profiles.map(redactProfile),
|
|
valuesPrinted: false,
|
|
},
|
|
remote: parsed ?? compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
next: {
|
|
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
|
|
},
|
|
};
|
|
}
|
|
|
|
async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const result = await capture(config, g14K3sRoute, ["script"], validateScript(pool));
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-validate",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-validate",
|
|
summary: parsed,
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
if (!pool.publicExposure.enabled) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "disabled-by-yaml",
|
|
target: poolTarget(pool),
|
|
};
|
|
}
|
|
if (!options.confirm) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "dry-run",
|
|
target: poolTarget(pool),
|
|
publicExposure: publicExposureSummary(pool),
|
|
next: {
|
|
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
|
|
},
|
|
};
|
|
}
|
|
const masterResult = await applyMasterFrpsAllowPort(pool);
|
|
const remoteResult = await capture(config, g14K3sRoute, ["script"], exposeScript(pool));
|
|
const parsed = parseJsonOutput(remoteResult.stdout);
|
|
const publicProbe = await probePublicModels(pool, "without-api-key");
|
|
const ok = masterResult.ok && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
|
|
if (options.raw) {
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
masterFrps: masterResult,
|
|
remote: compactCapture(remoteResult, { full: true }),
|
|
parsed,
|
|
publicProbe,
|
|
};
|
|
}
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "confirmed",
|
|
publicExposure: publicExposureSummary(pool),
|
|
masterFrps: masterResult,
|
|
remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }),
|
|
publicProbe,
|
|
next: {
|
|
configureLocal: "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
|
|
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
|
|
},
|
|
};
|
|
}
|
|
|
|
async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const codexDir = join(homedir(), ".codex");
|
|
const configPath = join(codexDir, "config.toml");
|
|
const authPath = join(codexDir, "auth.json");
|
|
const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`);
|
|
const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`);
|
|
if (!options.confirm) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-configure-local",
|
|
mode: "dry-run",
|
|
target: {
|
|
codexDir,
|
|
configPath,
|
|
authPath,
|
|
backupConfigPath,
|
|
backupAuthPath,
|
|
baseUrl: pool.publicExposure.masterBaseUrl,
|
|
providerName: pool.localCodex.providerName,
|
|
wireApi: pool.localCodex.wireApi,
|
|
valuesPrinted: false,
|
|
},
|
|
next: {
|
|
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
|
|
},
|
|
};
|
|
}
|
|
if (!pool.publicExposure.enabled) throw new Error(`${codexPoolConfigPath}.publicExposure.enabled must be true before configure-local`);
|
|
const keyResult = await fetchPoolApiKey(config, pool);
|
|
if (keyResult.apiKey === null) {
|
|
return {
|
|
ok: false,
|
|
action: "platform-infra-sub2api-codex-pool-configure-local",
|
|
error: keyResult.error ?? "pool API key missing",
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const writeResult = writeLocalCodexConfig(pool, keyResult.apiKey);
|
|
const validateResult = await validatePublicGatewayWithKey(pool, keyResult.apiKey);
|
|
return {
|
|
ok: writeResult.ok && validateResult.ok,
|
|
action: "platform-infra-sub2api-codex-pool-configure-local",
|
|
mode: "confirmed",
|
|
local: writeResult,
|
|
validation: validateResult,
|
|
apiKey: {
|
|
secret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
|
keyPreview: apiKeyPreview(keyResult.apiKey),
|
|
apiKeyFingerprint: fingerprint(keyResult.apiKey),
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
function collectCodexProfiles(): CodexProfile[] {
|
|
const codexDir = join(homedir(), ".codex");
|
|
const pool = readCodexPoolConfig();
|
|
if (!existsSync(codexDir)) return [];
|
|
const seenAccountNames = new Set<string>();
|
|
const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir);
|
|
return configs.map((entry) => {
|
|
const resolved = resolveProfileFiles(codexDir, entry);
|
|
const profile = entry.profile;
|
|
const accountName = entry.accountName ?? uniqueAccountName(profile, seenAccountNames);
|
|
seenAccountNames.add(accountName);
|
|
const configFile = resolved.configFile;
|
|
const authFile = resolved.authFile;
|
|
const configPath = join(codexDir, configFile);
|
|
const authPath = join(codexDir, authFile);
|
|
const base: CodexProfile = {
|
|
profile,
|
|
accountName,
|
|
configFile,
|
|
authFile,
|
|
provider: "",
|
|
baseUrl: "",
|
|
wireApi: null,
|
|
model: null,
|
|
envKey: null,
|
|
apiKey: null,
|
|
apiKeySource: null,
|
|
openaiResponsesWebSocketsV2Mode: entry.openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent: entry.upstreamUserAgent,
|
|
priority: entry.priority,
|
|
capacity: entry.capacity ?? pool.defaultAccountCapacity,
|
|
authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing",
|
|
ok: false,
|
|
error: null,
|
|
};
|
|
|
|
try {
|
|
if (!existsSync(configPath)) throw new Error(`config file ${configFile} is missing`);
|
|
const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as unknown;
|
|
if (!isRecord(parsed)) throw new Error("config is not a TOML object");
|
|
const providers = parsed.model_providers;
|
|
if (!isRecord(providers)) throw new Error("model_providers is missing");
|
|
const provider = stringValue(parsed.model_provider) ?? Object.keys(providers)[0] ?? "";
|
|
if (provider === "") throw new Error("model_provider is missing");
|
|
const providerConfig = providers[provider];
|
|
if (!isRecord(providerConfig)) throw new Error(`model provider ${provider} is missing`);
|
|
const baseUrl = normalizeBaseUrl(stringValue(providerConfig.base_url));
|
|
if (baseUrl === null) throw new Error(`model provider ${provider} base_url is missing or invalid`);
|
|
base.provider = provider;
|
|
base.baseUrl = baseUrl;
|
|
base.envKey = stringValue(providerConfig.env_key);
|
|
base.wireApi = stringValue(providerConfig.wire_api);
|
|
base.model = stringValue(parsed.model);
|
|
|
|
const auth = readAuthAPIKey(authPath);
|
|
base.authOpenAIKeyShape = auth.shape;
|
|
if (auth.apiKey !== null) {
|
|
base.apiKey = auth.apiKey;
|
|
base.apiKeySource = "auth-json";
|
|
} else if (base.envKey !== null && typeof process.env[base.envKey] === "string" && process.env[base.envKey]!.length > 0) {
|
|
base.apiKey = process.env[base.envKey]!;
|
|
base.apiKeySource = "env";
|
|
}
|
|
if (base.apiKey === null || base.apiKey.length === 0) {
|
|
throw new Error(base.envKey === null ? "auth OPENAI_API_KEY is missing or empty" : `auth OPENAI_API_KEY is missing and env ${base.envKey} is not present`);
|
|
}
|
|
base.ok = true;
|
|
return base;
|
|
} catch (error) {
|
|
base.error = error instanceof Error ? error.message : String(error);
|
|
return base;
|
|
}
|
|
});
|
|
}
|
|
|
|
function discoverCodexProfileConfigs(codexDir: string): 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,
|
|
priority: 1,
|
|
capacity: null,
|
|
};
|
|
});
|
|
}
|
|
|
|
function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig): { configFile: string; authFile: string } {
|
|
const configFile = existsSync(join(codexDir, profile.configFile)) || profile.fallbackConfigFile === null
|
|
? profile.configFile
|
|
: profile.fallbackConfigFile;
|
|
const authFile = existsSync(join(codexDir, profile.authFile)) || profile.fallbackAuthFile === null
|
|
? profile.authFile
|
|
: profile.fallbackAuthFile;
|
|
return { configFile, authFile };
|
|
}
|
|
|
|
function readCodexPoolConfig(): CodexPoolConfig {
|
|
const defaults = defaultCodexPoolConfig();
|
|
if (!existsSync(codexPoolConfigPath)) return defaults;
|
|
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
|
|
if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`);
|
|
const pool = parsed.pool;
|
|
if (!isRecord(pool)) throw new Error(`${codexPoolConfigPath}.pool must be a YAML object`);
|
|
const config: CodexPoolConfig = {
|
|
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,
|
|
defaultAccountCapacity: readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity"),
|
|
profiles: readProfileConfig(parsed.profiles, defaults.profiles),
|
|
publicExposure: readPublicExposureConfig(parsed.publicExposure, defaults.publicExposure),
|
|
localCodex: readLocalCodexConfig(parsed.localCodex, defaults.localCodex),
|
|
};
|
|
validateKubernetesName(config.groupName, "pool.groupName", false);
|
|
validateKubernetesName(config.apiKeySecretName, "pool.apiKeySecretName", true);
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(config.apiKeySecretKey)) {
|
|
throw new Error(`${codexPoolConfigPath}.pool.apiKeySecretKey must be a valid secret key name`);
|
|
}
|
|
if (config.minOwnerBalanceUsd <= 0) throw new Error(`${codexPoolConfigPath}.pool.minOwnerBalanceUsd must be > 0`);
|
|
return config;
|
|
}
|
|
|
|
function defaultCodexPoolConfig(): CodexPoolConfig {
|
|
return {
|
|
groupName: defaultPoolGroupName,
|
|
apiKeyName: defaultPoolApiKeyName,
|
|
apiKeySecretName: defaultPoolApiKeySecretName,
|
|
apiKeySecretKey: defaultPoolApiKeySecretKey,
|
|
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
|
|
defaultAccountCapacity: 5,
|
|
profiles: [],
|
|
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: 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",
|
|
},
|
|
},
|
|
localCodex: {
|
|
backupSuffix: "pre-sub2api",
|
|
providerName: "OpenAI",
|
|
wireApi: "responses",
|
|
},
|
|
};
|
|
}
|
|
|
|
function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[]): 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`);
|
|
return entries.map((entry, index) => {
|
|
if (!isRecord(entry)) throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] must be an object`);
|
|
const profile = stringValue(entry.profile);
|
|
const configFile = stringValue(entry.configFile);
|
|
const authFile = stringValue(entry.authFile);
|
|
if (profile === null || configFile === null || authFile === null) {
|
|
throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] requires profile, configFile, and authFile`);
|
|
}
|
|
const accountName = stringValue(entry.accountName);
|
|
if (accountName !== null) validateKubernetesName(accountName, `profiles.entries[${index}].accountName`, false);
|
|
validateCodexFileName(configFile, `profiles.entries[${index}].configFile`);
|
|
validateCodexFileName(authFile, `profiles.entries[${index}].authFile`);
|
|
const fallbackConfigFile = stringValue(entry.fallbackConfigFile);
|
|
const fallbackAuthFile = stringValue(entry.fallbackAuthFile);
|
|
if (fallbackConfigFile !== null) validateCodexFileName(fallbackConfigFile, `profiles.entries[${index}].fallbackConfigFile`);
|
|
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 capacity = entry.capacity === undefined || entry.capacity === null ? null : readAccountCapacity(entry.capacity, `profiles.entries[${index}].capacity`);
|
|
return {
|
|
profile,
|
|
accountName,
|
|
configFile,
|
|
authFile,
|
|
fallbackConfigFile,
|
|
fallbackAuthFile,
|
|
openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent,
|
|
priority,
|
|
capacity,
|
|
};
|
|
});
|
|
}
|
|
|
|
function readOpenAIResponsesWebSocketsV2Mode(value: unknown, key: string): OpenAIResponsesWebSocketsV2Mode | null {
|
|
if (value === undefined || value === null) return null;
|
|
const text = stringValue(value);
|
|
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`);
|
|
if (text === "off" || text === "ctx_pool" || text === "passthrough") return text;
|
|
throw new Error(`${codexPoolConfigPath}.${key} must be one of off, ctx_pool, passthrough`);
|
|
}
|
|
|
|
function readUpstreamUserAgent(value: unknown, key: string): string | null {
|
|
if (value === undefined || value === null) return null;
|
|
const text = stringValue(value);
|
|
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`);
|
|
if (text.length > 512) throw new Error(`${codexPoolConfigPath}.${key} must be at most 512 characters`);
|
|
if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
|
|
return text;
|
|
}
|
|
|
|
function readAccountPriority(value: unknown, key: string): number {
|
|
if (value === undefined || value === null) return 1;
|
|
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`);
|
|
}
|
|
return priority;
|
|
}
|
|
|
|
function readAccountCapacity(value: unknown, key: string): number {
|
|
const capacity = numberValue(value);
|
|
if (capacity === null || !Number.isInteger(capacity) || capacity < 1 || capacity > 1000) {
|
|
throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 1000`);
|
|
}
|
|
return capacity;
|
|
}
|
|
|
|
function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExposureConfig): CodexPoolPublicExposureConfig {
|
|
if (!isRecord(value)) return defaults;
|
|
const masterFrpsValue = isRecord(value.masterFrps) ? value.masterFrps : {};
|
|
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,
|
|
masterFrps: {
|
|
configPath: stringValue(masterFrpsValue.configPath) ?? defaults.masterFrps.configPath,
|
|
containerName: stringValue(masterFrpsValue.containerName) ?? defaults.masterFrps.containerName,
|
|
},
|
|
};
|
|
validateKubernetesName(config.configMapName, "publicExposure.configMapName", true);
|
|
validateKubernetesName(config.deploymentName, "publicExposure.deploymentName", true);
|
|
validateProxyName(config.proxyName, "publicExposure.proxyName");
|
|
validatePort(config.serverPort, "publicExposure.serverPort");
|
|
validatePort(config.remotePort, "publicExposure.remotePort");
|
|
validatePort(config.localPort, "publicExposure.localPort");
|
|
if (!/^[A-Za-z0-9._:/-]+$/u.test(config.frpcImage)) throw new Error(`${codexPoolConfigPath}.publicExposure.frpcImage has an unsupported format`);
|
|
if (!/^[A-Za-z0-9._:-]+$/u.test(config.serverAddr)) throw new Error(`${codexPoolConfigPath}.publicExposure.serverAddr has an unsupported format`);
|
|
if (!/^[A-Za-z0-9._:-]+$/u.test(config.localIP)) throw new Error(`${codexPoolConfigPath}.publicExposure.localIP has an unsupported format`);
|
|
if (!config.masterFrps.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterFrps.configPath must be absolute`);
|
|
validateProxyName(config.masterFrps.containerName, "publicExposure.masterFrps.containerName");
|
|
return config;
|
|
}
|
|
|
|
function readLocalCodexConfig(value: unknown, defaults: CodexPoolLocalCodexConfig): CodexPoolLocalCodexConfig {
|
|
if (!isRecord(value)) return defaults;
|
|
const config = {
|
|
backupSuffix: stringValue(value.backupSuffix) ?? defaults.backupSuffix,
|
|
providerName: stringValue(value.providerName) ?? defaults.providerName,
|
|
wireApi: stringValue(value.wireApi) ?? defaults.wireApi,
|
|
};
|
|
if (!/^[A-Za-z0-9._-]+$/u.test(config.backupSuffix)) throw new Error(`${codexPoolConfigPath}.localCodex.backupSuffix has an unsupported format`);
|
|
validateProxyName(config.providerName, "localCodex.providerName");
|
|
validateProxyName(config.wireApi, "localCodex.wireApi");
|
|
return config;
|
|
}
|
|
|
|
function validateCodexFileName(value: string, key: string): void {
|
|
if (!/^(config\.toml|config\.toml\.[A-Za-z0-9._-]+|auth\.json|auth\.json\.[A-Za-z0-9._-]+)$/u.test(value)) {
|
|
throw new Error(`${codexPoolConfigPath}.${key} has an unsupported file name`);
|
|
}
|
|
}
|
|
|
|
function validateProxyName(value: string, key: string): void {
|
|
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
|
|
}
|
|
|
|
function validatePort(value: number, key: string): void {
|
|
if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`);
|
|
}
|
|
|
|
function readAuthAPIKey(authPath: string): { apiKey: string | null; shape: string } {
|
|
if (!existsSync(authPath)) return { apiKey: null, shape: "missing" };
|
|
const parsed = JSON.parse(readFileSync(authPath, "utf8")) as unknown;
|
|
if (!isRecord(parsed)) return { apiKey: null, shape: "non-object" };
|
|
const value = parsed.OPENAI_API_KEY;
|
|
const shape = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
if (typeof value === "string" && value.length > 0) return { apiKey: value, shape };
|
|
return { apiKey: null, shape };
|
|
}
|
|
|
|
function uniqueAccountName(profile: string, seen: Set<string>): string {
|
|
const normalized = profile
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
.replace(/^-+|-+$/gu, "") || "default";
|
|
let candidate = `unidesk-codex-${normalized}`;
|
|
let counter = 2;
|
|
while (seen.has(candidate)) {
|
|
candidate = `unidesk-codex-${normalized}-${counter}`;
|
|
counter += 1;
|
|
}
|
|
seen.add(candidate);
|
|
return candidate;
|
|
}
|
|
|
|
function redactProfile(profile: CodexProfile): Record<string, unknown> {
|
|
return {
|
|
profile: profile.profile,
|
|
accountName: profile.accountName,
|
|
configFile: profile.configFile,
|
|
authFile: profile.authFile,
|
|
provider: profile.provider || null,
|
|
baseUrl: profile.baseUrl || null,
|
|
wireApi: profile.wireApi,
|
|
model: profile.model,
|
|
envKey: profile.envKey,
|
|
apiKeySource: profile.apiKeySource,
|
|
openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent: profile.upstreamUserAgent,
|
|
priority: profile.priority,
|
|
capacity: profile.capacity,
|
|
apiKeyPresent: profile.apiKey !== null && profile.apiKey.length > 0,
|
|
apiKeyBytes: profile.apiKey === null ? 0 : Buffer.byteLength(profile.apiKey, "utf8"),
|
|
apiKeyFingerprint: profile.apiKey === null ? null : fingerprint(profile.apiKey),
|
|
authOpenAIKeyShape: profile.authOpenAIKeyShape,
|
|
ok: profile.ok,
|
|
error: profile.error,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
|
|
return {
|
|
route: g14K3sRoute,
|
|
namespace,
|
|
service: serviceName,
|
|
serviceDns,
|
|
configPath: codexPoolConfigPath,
|
|
groupName: pool.groupName,
|
|
apiKeyName: pool.apiKeyName,
|
|
apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
|
defaultAccountCapacity: pool.defaultAccountCapacity,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function publicExposureSummary(pool: CodexPoolConfig): Record<string, unknown> {
|
|
return {
|
|
enabled: pool.publicExposure.enabled,
|
|
proxyName: pool.publicExposure.proxyName,
|
|
namespace,
|
|
configMapName: pool.publicExposure.configMapName,
|
|
deploymentName: pool.publicExposure.deploymentName,
|
|
frpcImage: pool.publicExposure.frpcImage,
|
|
frps: {
|
|
serverAddr: pool.publicExposure.serverAddr,
|
|
serverPort: pool.publicExposure.serverPort,
|
|
remotePort: pool.publicExposure.remotePort,
|
|
masterConfigPath: pool.publicExposure.masterFrps.configPath,
|
|
masterContainerName: pool.publicExposure.masterFrps.containerName,
|
|
},
|
|
upstream: {
|
|
localIP: pool.publicExposure.localIP,
|
|
localPort: pool.publicExposure.localPort,
|
|
serviceDns,
|
|
},
|
|
urls: {
|
|
publicBaseUrl: pool.publicExposure.publicBaseUrl,
|
|
masterBaseUrl: pool.publicExposure.masterBaseUrl,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function applyMasterFrpsAllowPort(pool: CodexPoolConfig): Promise<Record<string, unknown>> {
|
|
const path = pool.publicExposure.masterFrps.configPath;
|
|
const port = pool.publicExposure.remotePort;
|
|
const container = pool.publicExposure.masterFrps.containerName;
|
|
if (!existsSync(path)) {
|
|
return { ok: false, error: "master-frps-config-missing", path, valuesPrinted: false };
|
|
}
|
|
const before = readFileSync(path, "utf8");
|
|
const alreadyAllowed = frpsAllowPortExists(before, port);
|
|
let backupPath: string | null = null;
|
|
let action = "kept-existing";
|
|
if (!alreadyAllowed) {
|
|
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
|
|
backupPath = `${path}.bak-sub2api-${stamp}`;
|
|
copyFileSync(path, backupPath);
|
|
const next = before.replace(/\s*$/u, "") + `\n\n[[allowPorts]]\nstart = ${port}\nend = ${port}\n`;
|
|
writeFileSync(path, next, "utf8");
|
|
chmodSync(path, statSync(backupPath).mode & 0o777);
|
|
action = "added-allow-port";
|
|
}
|
|
const restart = alreadyAllowed ? null : Bun.spawnSync(["docker", "restart", container]);
|
|
const inspect = Bun.spawnSync(["docker", "inspect", "-f", "{{.State.Running}}", container]);
|
|
const ok = alreadyAllowed || (restart?.exitCode === 0 && inspect.exitCode === 0 && String(inspect.stdout).trim() === "true");
|
|
return {
|
|
ok,
|
|
action,
|
|
path,
|
|
backupPath,
|
|
remotePort: port,
|
|
containerName: container,
|
|
restart: restart === null ? null : {
|
|
exitCode: restart.exitCode,
|
|
stdoutTail: Buffer.from(restart.stdout).toString("utf8").slice(-1000),
|
|
stderrTail: Buffer.from(restart.stderr).toString("utf8").slice(-1000),
|
|
},
|
|
inspect: {
|
|
exitCode: inspect.exitCode,
|
|
running: String(inspect.stdout).trim() === "true",
|
|
stderrTail: Buffer.from(inspect.stderr).toString("utf8").slice(-1000),
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function frpsAllowPortExists(toml: string, port: number): boolean {
|
|
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
|
|
return sections.some((section) => {
|
|
if (!section.includes("[[allowPorts]]")) return false;
|
|
const start = section.match(/^\s*start\s*=\s*(\d+)\s*$/mu);
|
|
const end = section.match(/^\s*end\s*=\s*(\d+)\s*$/mu);
|
|
return Number(start?.[1]) === port && Number(end?.[1]) === port;
|
|
});
|
|
}
|
|
|
|
function exposeScript(pool: CodexPoolConfig): string {
|
|
const manifest = frpcManifest(pool);
|
|
const encoded = Buffer.from(manifest, "utf8").toString("base64");
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
manifest="$tmp/sub2api-frpc.yaml"
|
|
printf '%s' '${encoded}' | base64 -d > "$manifest"
|
|
ns_out="$tmp/ns.out"
|
|
ns_err="$tmp/ns.err"
|
|
apply_out="$tmp/apply.out"
|
|
apply_err="$tmp/apply.err"
|
|
rollout_out="$tmp/rollout.out"
|
|
rollout_err="$tmp/rollout.err"
|
|
kubectl get namespace ${namespace} >"$ns_out" 2>"$ns_err"
|
|
ns_rc=$?
|
|
apply_rc=1
|
|
rollout_rc=1
|
|
if [ "$ns_rc" -eq 0 ]; then
|
|
kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
|
|
apply_rc=$?
|
|
if [ "$apply_rc" -eq 0 ]; then
|
|
kubectl -n ${namespace} rollout status deployment/${pool.publicExposure.deploymentName} --timeout=30s >"$rollout_out" 2>"$rollout_err"
|
|
rollout_rc=$?
|
|
else
|
|
printf '%s\\n' 'skipped because apply failed' >"$rollout_err"
|
|
fi
|
|
else
|
|
: >"$apply_out"
|
|
printf '%s\\n' 'skipped because namespace is missing' >"$apply_err"
|
|
: >"$rollout_out"
|
|
printf '%s\\n' 'skipped because namespace is missing' >"$rollout_err"
|
|
fi
|
|
pods_json="$tmp/pods.json"
|
|
pods_err="$tmp/pods.err"
|
|
kubectl -n ${namespace} get pods -l app.kubernetes.io/name=${pool.publicExposure.deploymentName} -o json >"$pods_json" 2>"$pods_err"
|
|
pods_rc=$?
|
|
logs_out="$tmp/logs.out"
|
|
logs_err="$tmp/logs.err"
|
|
kubectl -n ${namespace} logs deployment/${pool.publicExposure.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
|
|
logs_rc=$?
|
|
python3 - "$tmp" "$ns_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
tmp = sys.argv[1]
|
|
ns_rc, apply_rc, rollout_rc, pods_rc, logs_rc = [int(value) for value in sys.argv[2:]]
|
|
|
|
def text(name, limit=4000):
|
|
path = os.path.join(tmp, name)
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
|
|
def load_json(name):
|
|
path = os.path.join(tmp, name)
|
|
try:
|
|
return json.load(open(path, encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
|
|
pods_obj = load_json("pods.json")
|
|
pods = []
|
|
if isinstance(pods_obj, dict):
|
|
for item in pods_obj.get("items") or []:
|
|
status = item.get("status") or {}
|
|
statuses = status.get("containerStatuses") or []
|
|
pods.append({
|
|
"name": item.get("metadata", {}).get("name"),
|
|
"phase": status.get("phase"),
|
|
"ready": all(cs.get("ready") is True for cs in statuses) if statuses else False,
|
|
"restarts": sum(int(cs.get("restartCount") or 0) for cs in statuses),
|
|
})
|
|
|
|
logs = text("logs.out", 4000)
|
|
payload = {
|
|
"ok": ns_rc == 0 and apply_rc == 0 and rollout_rc == 0 and pods_rc == 0 and len(pods) > 0 and all(p["ready"] for p in pods),
|
|
"namespace": "${namespace}",
|
|
"proxy": {
|
|
"name": "${pool.publicExposure.proxyName}",
|
|
"remotePort": ${JSON.stringify(pool.publicExposure.remotePort)},
|
|
"publicBaseUrl": "${pool.publicExposure.publicBaseUrl}",
|
|
"masterBaseUrl": "${pool.publicExposure.masterBaseUrl}",
|
|
"localIP": "${pool.publicExposure.localIP}",
|
|
"localPort": ${JSON.stringify(pool.publicExposure.localPort)},
|
|
},
|
|
"resources": {
|
|
"configMap": "${pool.publicExposure.configMapName}",
|
|
"deployment": "${pool.publicExposure.deploymentName}",
|
|
"image": "${pool.publicExposure.frpcImage}",
|
|
},
|
|
"steps": {
|
|
"namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")},
|
|
"apply": {"exitCode": apply_rc, "stdout": text("apply.out"), "stderr": text("apply.err")},
|
|
"rollout": {"exitCode": rollout_rc, "stdout": text("rollout.out"), "stderr": text("rollout.err")},
|
|
"pods": {"exitCode": pods_rc, "items": pods, "stderr": text("pods.err")},
|
|
"logs": {
|
|
"exitCode": logs_rc,
|
|
"startProxySuccess": "start proxy success" in logs,
|
|
"stdoutTail": logs,
|
|
"stderrTail": text("logs.err"),
|
|
},
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
function frpcManifest(pool: CodexPoolConfig): string {
|
|
return `apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: ${pool.publicExposure.configMapName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
data:
|
|
frpc.toml: |
|
|
serverAddr = "${pool.publicExposure.serverAddr}"
|
|
serverPort = ${pool.publicExposure.serverPort}
|
|
loginFailExit = true
|
|
|
|
[[proxies]]
|
|
name = "${pool.publicExposure.proxyName}"
|
|
type = "tcp"
|
|
localIP = "${pool.publicExposure.localIP}"
|
|
localPort = ${pool.publicExposure.localPort}
|
|
remotePort = ${pool.publicExposure.remotePort}
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${pool.publicExposure.deploymentName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
spec:
|
|
containers:
|
|
- name: frpc
|
|
image: ${pool.publicExposure.frpcImage}
|
|
imagePullPolicy: IfNotPresent
|
|
args:
|
|
- -c
|
|
- /etc/frp/frpc.toml
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /etc/frp
|
|
readOnly: true
|
|
volumes:
|
|
- name: config
|
|
configMap:
|
|
name: ${pool.publicExposure.configMapName}
|
|
`;
|
|
}
|
|
|
|
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig): Promise<{ apiKey: string | null; error: string | null }> {
|
|
const result = await capture(config, g14K3sRoute, ["script"], `
|
|
set -u
|
|
kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
|
|
`);
|
|
if (result.exitCode !== 0) {
|
|
return { apiKey: null, error: `read pool API key secret failed: ${result.stderr.slice(-1000)}` };
|
|
}
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
const data = isRecord(parsed?.data) ? parsed.data : null;
|
|
const encoded = typeof data?.[pool.apiKeySecretKey] === "string" ? data[pool.apiKeySecretKey] : null;
|
|
if (encoded === null) return { apiKey: null, error: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
|
|
try {
|
|
const apiKey = Buffer.from(encoded, "base64").toString("utf8");
|
|
return apiKey.length > 0 ? { apiKey, error: null } : { apiKey: null, error: "decoded API key is empty" };
|
|
} catch (error) {
|
|
return { apiKey: null, error: error instanceof Error ? error.message : String(error) };
|
|
}
|
|
}
|
|
|
|
function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string): Record<string, unknown> {
|
|
const codexDir = join(homedir(), ".codex");
|
|
mkdirSync(codexDir, { recursive: true, mode: 0o700 });
|
|
const configPath = join(codexDir, "config.toml");
|
|
const authPath = join(codexDir, "auth.json");
|
|
const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`);
|
|
const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`);
|
|
if (!existsSync(configPath)) throw new Error(`${configPath} missing`);
|
|
if (!existsSync(authPath)) throw new Error(`${authPath} missing`);
|
|
const configBackupAction = copyIfMissing(configPath, backupConfigPath);
|
|
const authBackupAction = copyIfMissing(authPath, backupAuthPath);
|
|
const currentToml = readFileSync(configPath, "utf8");
|
|
const nextToml = updateCodexConfigToml(currentToml, pool);
|
|
writeFileSync(configPath, nextToml, { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(configPath, 0o600);
|
|
writeFileSync(authPath, `${JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(authPath, 0o600);
|
|
return {
|
|
ok: true,
|
|
codexDir,
|
|
config: {
|
|
path: configPath,
|
|
bytes: statSync(configPath).size,
|
|
backupPath: backupConfigPath,
|
|
backupAction: configBackupAction,
|
|
backupBytes: statSync(backupConfigPath).size,
|
|
},
|
|
auth: {
|
|
path: authPath,
|
|
bytes: statSync(authPath).size,
|
|
backupPath: backupAuthPath,
|
|
backupAction: authBackupAction,
|
|
backupBytes: statSync(backupAuthPath).size,
|
|
openaiApiKeyShape: "string",
|
|
openaiApiKeyBytes: Buffer.byteLength(apiKey, "utf8"),
|
|
openaiApiKeyFingerprint: fingerprint(apiKey),
|
|
valuesPrinted: false,
|
|
},
|
|
provider: {
|
|
name: pool.localCodex.providerName,
|
|
baseUrl: pool.publicExposure.masterBaseUrl,
|
|
wireApi: pool.localCodex.wireApi,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function copyIfMissing(source: string, target: string): "created" | "kept-existing" {
|
|
if (existsSync(target)) return "kept-existing";
|
|
copyFileSync(source, target);
|
|
chmodSync(target, 0o600);
|
|
return "created";
|
|
}
|
|
|
|
function updateCodexConfigToml(current: string, pool: CodexPoolConfig): string {
|
|
let next = upsertTopLevelTomlString(current, "model_provider", pool.localCodex.providerName);
|
|
next = upsertProviderSection(next, pool.localCodex.providerName, pool.publicExposure.masterBaseUrl, pool.localCodex.wireApi);
|
|
return next.endsWith("\n") ? next : `${next}\n`;
|
|
}
|
|
|
|
function upsertTopLevelTomlString(text: string, key: string, value: string): string {
|
|
const lines = text.split(/\r?\n/u);
|
|
const firstSection = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
const end = firstSection === -1 ? lines.length : firstSection;
|
|
for (let index = 0; index < end; index += 1) {
|
|
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) {
|
|
lines[index] = `${key} = ${tomlString(value)}`;
|
|
return lines.join("\n");
|
|
}
|
|
}
|
|
lines.splice(0, 0, `${key} = ${tomlString(value)}`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function upsertProviderSection(text: string, providerName: string, baseUrl: string, wireApi: string): string {
|
|
const header = `[model_providers.${providerName}]`;
|
|
const lines = text.split(/\r?\n/u);
|
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
const canonical = [
|
|
`name = ${tomlString(providerName)}`,
|
|
`base_url = ${tomlString(baseUrl)}`,
|
|
`wire_api = ${tomlString(wireApi)}`,
|
|
"requires_openai_auth = true",
|
|
];
|
|
if (start === -1) {
|
|
const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== "";
|
|
return [...lines, ...(needsBlank ? [""] : []), header, ...canonical].join("\n");
|
|
}
|
|
let end = lines.length;
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
if (/^\s*\[/.test(lines[index])) {
|
|
end = index;
|
|
break;
|
|
}
|
|
}
|
|
const preserved = lines.slice(start + 1, end).filter((line) => {
|
|
return !/^\s*(name|base_url|wire_api|requires_openai_auth|env_key)\s*=/u.test(line);
|
|
});
|
|
const replacement = [header, ...canonical, ...preserved.filter((line) => line.trim() !== "")];
|
|
return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n");
|
|
}
|
|
|
|
async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string): Promise<Record<string, unknown>> {
|
|
const probe = await probePublicModels(pool, "with-api-key", apiKey);
|
|
return {
|
|
...probe,
|
|
ok: probe.ok === true,
|
|
apiKeyFingerprint: fingerprint(apiKey),
|
|
keyPreview: apiKeyPreview(apiKey),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string): Promise<Record<string, unknown>> {
|
|
const url = `${pool.publicExposure.masterBaseUrl.replace(/\/+$/u, "")}/v1/models`;
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
|
|
signal: controller.signal,
|
|
});
|
|
const body = await response.text();
|
|
let parsed: unknown = null;
|
|
try {
|
|
parsed = body.trim().length > 0 ? JSON.parse(body) : null;
|
|
} catch {
|
|
parsed = null;
|
|
}
|
|
const modelCount = isRecord(parsed) && Array.isArray(parsed.data) ? parsed.data.length : null;
|
|
return {
|
|
ok: response.ok,
|
|
mode,
|
|
method: "GET /v1/models",
|
|
baseUrl: pool.publicExposure.masterBaseUrl,
|
|
httpStatus: response.status,
|
|
modelCount,
|
|
bodyPreview: response.ok ? "" : body.slice(0, 500),
|
|
valuesPrinted: false,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
mode,
|
|
method: "GET /v1/models",
|
|
baseUrl: pool.publicExposure.masterBaseUrl,
|
|
httpStatus: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
valuesPrinted: false,
|
|
};
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
function apiKeyPreview(apiKey: string): string {
|
|
if (apiKey.length <= 14) return "***";
|
|
return `${apiKey.slice(0, 10)}...${apiKey.slice(-4)}`;
|
|
}
|
|
|
|
function tomlString(value: string): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
}
|
|
|
|
function normalizeBaseUrl(value: string | null): string | null {
|
|
if (value === null) return null;
|
|
const trimmed = value.trim().replace(/\/+$/u, "");
|
|
if (trimmed.length === 0) return null;
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
return parsed.toString().replace(/\/+$/u, "");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function stringValue(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
}
|
|
|
|
function numberValue(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function booleanValue(value: unknown): boolean | null {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "string") {
|
|
if (value.trim() === "true") return true;
|
|
if (value.trim() === "false") return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function validateKubernetesName(value: string, key: string, strictDns: boolean): void {
|
|
const pattern = strictDns ? /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u : /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
if (!pattern.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function fingerprint(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
}
|
|
|
|
function syncScript(payload: unknown, pool: CodexPoolConfig): string {
|
|
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
return remotePythonScript("sync", encoded, pool);
|
|
}
|
|
|
|
function validateScript(pool: CodexPoolConfig): string {
|
|
return remotePythonScript("validate", "", pool);
|
|
}
|
|
|
|
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);
|
|
const capacities: Record<string, number> = {};
|
|
for (const entry of configs) {
|
|
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
|
|
seenAccountNames.add(accountName);
|
|
capacities[accountName] = entry.capacity ?? pool.defaultAccountCapacity;
|
|
}
|
|
return capacities;
|
|
}
|
|
|
|
function remotePythonScript(mode: "sync" | "validate", encodedPayload: string, pool: CodexPoolConfig): string {
|
|
return `
|
|
set -u
|
|
python3 - <<'PY'
|
|
import base64
|
|
import json
|
|
import secrets
|
|
import string
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from urllib.parse import quote
|
|
|
|
NAMESPACE = "${namespace}"
|
|
SERVICE_NAME = "${serviceName}"
|
|
SERVICE_DNS = "${serviceDns}"
|
|
FIELD_MANAGER = "${fieldManager}"
|
|
APP_SECRET_NAME = "${appSecretName}"
|
|
POOL_GROUP_NAME = "${pool.groupName}"
|
|
POOL_API_KEY_NAME = "${pool.apiKeyName}"
|
|
POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
|
|
POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}"
|
|
MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)}
|
|
POOL_DEFAULT_ACCOUNT_CAPACITY = ${JSON.stringify(pool.defaultAccountCapacity)}
|
|
EXPECTED_ACCOUNT_CAPACITIES = ${JSON.stringify(desiredAccountCapacityMap(pool))}
|
|
MODE = "${mode}"
|
|
PAYLOAD_B64 = "${encodedPayload}"
|
|
|
|
def run(cmd, input_bytes=None):
|
|
return subprocess.run(cmd, input=input_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
def text(data, limit=4000):
|
|
if isinstance(data, bytes):
|
|
data = data.decode("utf-8", errors="replace")
|
|
return data[-limit:]
|
|
|
|
def kubectl(args, input_obj=None):
|
|
if isinstance(input_obj, str):
|
|
input_bytes = input_obj.encode("utf-8")
|
|
else:
|
|
input_bytes = input_obj
|
|
return run(["kubectl", *args], input_bytes)
|
|
|
|
def require_kubectl(args, input_obj=None, label="kubectl"):
|
|
proc = kubectl(args, input_obj)
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"{label} failed: {text(proc.stderr, 1000)}")
|
|
return proc.stdout
|
|
|
|
def kube_json(args, label):
|
|
raw = require_kubectl([*args, "-o", "json"], label=label)
|
|
return json.loads(raw.decode("utf-8"))
|
|
|
|
def decode_secret_value(name, key):
|
|
data = kube_json(["-n", NAMESPACE, "get", "secret", name], f"secret/{name}").get("data") or {}
|
|
if key not in data:
|
|
return None
|
|
return base64.b64decode(data[key]).decode("utf-8")
|
|
|
|
def get_config_value(name, key):
|
|
data = kube_json(["-n", NAMESPACE, "get", "configmap", name], f"configmap/{name}").get("data") or {}
|
|
value = data.get(key)
|
|
return value if isinstance(value, str) and value else None
|
|
|
|
def select_app_pod():
|
|
pods = kube_json(["-n", NAMESPACE, "get", "pods", "-l", "app.kubernetes.io/name=sub2api"], "sub2api pods").get("items") or []
|
|
for pod in pods:
|
|
status = pod.get("status") or {}
|
|
if status.get("phase") != "Running":
|
|
continue
|
|
statuses = status.get("containerStatuses") or []
|
|
if statuses and all(item.get("ready") is True for item in statuses):
|
|
return pod["metadata"]["name"]
|
|
if pods:
|
|
return pods[0]["metadata"]["name"]
|
|
raise RuntimeError("sub2api app pod not found")
|
|
|
|
APP_POD = select_app_pod()
|
|
|
|
def parse_curl_output(proc):
|
|
stdout = proc.stdout.decode("utf-8", errors="replace")
|
|
marker = "\\n__HTTP_CODE__:"
|
|
pos = stdout.rfind(marker)
|
|
if pos < 0:
|
|
return {
|
|
"ok": False,
|
|
"httpStatus": 0,
|
|
"json": None,
|
|
"body": stdout,
|
|
"stderr": text(proc.stderr, 1000),
|
|
"transportExitCode": proc.returncode,
|
|
}
|
|
body = stdout[:pos]
|
|
status_text = stdout[pos + len(marker):].strip()
|
|
try:
|
|
http_status = int(status_text[-3:])
|
|
except ValueError:
|
|
http_status = 0
|
|
try:
|
|
parsed = json.loads(body) if body.strip() else None
|
|
except json.JSONDecodeError:
|
|
parsed = None
|
|
return {
|
|
"ok": proc.returncode == 0 and 200 <= http_status < 300,
|
|
"httpStatus": http_status,
|
|
"json": parsed,
|
|
"body": body,
|
|
"stderr": text(proc.stderr, 1000),
|
|
"transportExitCode": proc.returncode,
|
|
}
|
|
|
|
def curl_api(method, path, bearer=None, payload=None):
|
|
body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
script = r'''
|
|
set -eu
|
|
method="$1"
|
|
url="$2"
|
|
token="\${3:-}"
|
|
tmp="$(mktemp)"
|
|
trap 'rm -f "$tmp"' EXIT
|
|
cat > "$tmp"
|
|
if [ -n "$token" ]; then
|
|
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
|
|
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H "Authorization: Bearer $token" "$url"
|
|
else
|
|
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" --data-binary @"$tmp" "$url"
|
|
fi
|
|
else
|
|
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
|
|
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" "$url"
|
|
else
|
|
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' --data-binary @"$tmp" "$url"
|
|
fi
|
|
fi
|
|
'''
|
|
proc = run([
|
|
"kubectl", "-n", NAMESPACE, "exec", "-i", APP_POD,
|
|
"--", "sh", "-c", script, "sh", method, f"http://127.0.0.1:8080{path}", bearer or "",
|
|
], body)
|
|
return parse_curl_output(proc)
|
|
|
|
def envelope_data(parsed):
|
|
if isinstance(parsed, dict) and "data" in parsed:
|
|
return parsed.get("data")
|
|
return parsed
|
|
|
|
def ensure_success(resp, label):
|
|
parsed = resp.get("json")
|
|
code = parsed.get("code") if isinstance(parsed, dict) else None
|
|
if not resp.get("ok") or (code is not None and code != 0):
|
|
message = parsed.get("message") if isinstance(parsed, dict) else text(resp.get("body", ""), 500)
|
|
raise RuntimeError(f"{label} failed: http={resp.get('httpStatus')} message={message}")
|
|
return envelope_data(parsed)
|
|
|
|
def extract_items(data):
|
|
if isinstance(data, list):
|
|
return data
|
|
if isinstance(data, dict):
|
|
if isinstance(data.get("items"), list):
|
|
return data["items"]
|
|
for key in ("groups", "accounts", "api_keys", "keys"):
|
|
if isinstance(data.get(key), list):
|
|
return data[key]
|
|
return []
|
|
|
|
def find_access_token(data):
|
|
if isinstance(data, dict):
|
|
for key in ("access_token", "token"):
|
|
if isinstance(data.get(key), str) and data[key]:
|
|
return data[key]
|
|
for value in data.values():
|
|
token = find_access_token(value)
|
|
if token:
|
|
return token
|
|
return None
|
|
|
|
def login():
|
|
admin_email = get_config_value("sub2api-config", "ADMIN_EMAIL") or "admin@sub2api.platform-infra.local"
|
|
admin_password = decode_secret_value(APP_SECRET_NAME, "ADMIN_PASSWORD")
|
|
if not admin_password:
|
|
raise RuntimeError("ADMIN_PASSWORD missing from sub2api-secrets")
|
|
data = ensure_success(curl_api("POST", "/api/v1/auth/login", payload={"email": admin_email, "password": admin_password}), "admin login")
|
|
token = find_access_token(data)
|
|
if not token:
|
|
raise RuntimeError("admin login response did not contain access_token")
|
|
return admin_email, token
|
|
|
|
def group_payload():
|
|
return {
|
|
"name": POOL_GROUP_NAME,
|
|
"description": "UniDesk-managed Codex API-key pool for G14 k3s internal Sub2API clients.",
|
|
"platform": "openai",
|
|
"rate_multiplier": 1,
|
|
"is_exclusive": False,
|
|
"subscription_type": "standard",
|
|
"allow_messages_dispatch": True,
|
|
"require_oauth_only": False,
|
|
"require_privacy_set": False,
|
|
"rpm_limit": 0,
|
|
}
|
|
|
|
def ensure_group(token):
|
|
existing_data = ensure_success(curl_api("GET", "/api/v1/admin/groups/all?platform=openai", bearer=token), "list groups")
|
|
existing = next((item for item in extract_items(existing_data) if item.get("name") == POOL_GROUP_NAME), None)
|
|
payload = group_payload()
|
|
if existing is None:
|
|
created = ensure_success(curl_api("POST", "/api/v1/admin/groups", bearer=token, payload=payload), "create group")
|
|
return created, "created"
|
|
group_id = existing.get("id")
|
|
if group_id is None:
|
|
raise RuntimeError("existing group has no id")
|
|
payload["status"] = "active"
|
|
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/groups/{group_id}", bearer=token, payload=payload), "update group")
|
|
return updated if isinstance(updated, dict) else existing, "updated"
|
|
|
|
def list_accounts(token):
|
|
path = "/api/v1/admin/accounts?page=1&page_size=200&platform=openai&type=apikey&search=" + quote("unidesk-codex-")
|
|
data = ensure_success(curl_api("GET", path, bearer=token), "list accounts")
|
|
return extract_items(data)
|
|
|
|
def account_payload(profile, group_id):
|
|
extra = {
|
|
"openai_responses_mode": "force_responses",
|
|
"unidesk_codex_profile": profile["profile"],
|
|
"unidesk_managed": True,
|
|
}
|
|
ws_mode = profile.get("openaiResponsesWebSocketsV2Mode")
|
|
if ws_mode:
|
|
extra["openai_apikey_responses_websockets_v2_mode"] = ws_mode
|
|
extra["openai_apikey_responses_websockets_v2_enabled"] = ws_mode != "off"
|
|
credentials = {
|
|
"api_key": profile["apiKey"],
|
|
"base_url": profile["baseUrl"],
|
|
}
|
|
upstream_user_agent = profile.get("upstreamUserAgent")
|
|
if upstream_user_agent:
|
|
credentials["user_agent"] = upstream_user_agent
|
|
return {
|
|
"name": profile["accountName"],
|
|
"notes": f"UniDesk-managed Codex profile {profile['profile']} from {profile['configFile']} and {profile['authFile']}; secret source={profile['apiKeySource']}; fingerprint={profile['apiKeyFingerprint']}.",
|
|
"platform": "openai",
|
|
"type": "apikey",
|
|
"credentials": credentials,
|
|
"extra": extra,
|
|
"concurrency": int(profile.get("capacity", 5) or 5),
|
|
"priority": int(profile.get("priority", 1) or 1),
|
|
"rate_multiplier": 1,
|
|
"load_factor": 1,
|
|
"group_ids": [group_id],
|
|
"confirm_mixed_channel_risk": True,
|
|
}
|
|
|
|
def ensure_accounts(token, profiles, group_id):
|
|
existing_accounts = list_accounts(token)
|
|
existing = {item.get("name"): item for item in existing_accounts}
|
|
desired_names = {profile["accountName"] for profile in profiles}
|
|
results = []
|
|
for profile in profiles:
|
|
payload = account_payload(profile, group_id)
|
|
current = existing.get(profile["accountName"])
|
|
if current and current.get("id") is not None:
|
|
account_id = current["id"]
|
|
update_payload = dict(payload)
|
|
update_payload.pop("platform", None)
|
|
update_payload["status"] = "active"
|
|
data = ensure_success(curl_api("PUT", f"/api/v1/admin/accounts/{account_id}", bearer=token, payload=update_payload), f"update account {profile['accountName']}")
|
|
action = "updated"
|
|
else:
|
|
data = ensure_success(curl_api("POST", "/api/v1/admin/accounts", bearer=token, payload=payload), f"create account {profile['accountName']}")
|
|
action = "created"
|
|
results.append({
|
|
"profile": profile["profile"],
|
|
"accountName": profile["accountName"],
|
|
"accountId": data.get("id") if isinstance(data, dict) else None,
|
|
"action": action,
|
|
"baseUrl": profile["baseUrl"],
|
|
"apiKeySource": profile["apiKeySource"],
|
|
"apiKeyFingerprint": profile["apiKeyFingerprint"],
|
|
"openaiResponsesWebSocketsV2Mode": profile.get("openaiResponsesWebSocketsV2Mode"),
|
|
"priority": int(profile.get("priority", 1) or 1),
|
|
"capacity": int(profile.get("capacity", 5) or 5),
|
|
"runtimeConcurrency": data.get("concurrency") if isinstance(data, dict) else None,
|
|
"upstreamUserAgentConfigured": bool(profile.get("upstreamUserAgent")),
|
|
"valuesPrinted": False,
|
|
})
|
|
prune_results = prune_removed_accounts(token, existing_accounts, desired_names)
|
|
return results, prune_results
|
|
|
|
def prune_removed_accounts(token, existing_accounts, desired_names):
|
|
results = []
|
|
for account in existing_accounts:
|
|
name = account.get("name")
|
|
account_id = account.get("id")
|
|
extra = account.get("extra") if isinstance(account.get("extra"), dict) else {}
|
|
if not isinstance(name, str) or not name.startswith("unidesk-codex-"):
|
|
continue
|
|
if extra.get("unidesk_managed") is not True:
|
|
continue
|
|
if name in desired_names:
|
|
continue
|
|
if account_id is None:
|
|
raise RuntimeError(f"removed account {name} has no id")
|
|
ensure_success(curl_api("DELETE", f"/api/v1/admin/accounts/{account_id}", bearer=token), f"delete removed account {name}")
|
|
results.append({
|
|
"accountName": name,
|
|
"accountId": account_id,
|
|
"profile": extra.get("unidesk_codex_profile"),
|
|
"action": "deleted",
|
|
"reason": "removed-from-yaml",
|
|
"valuesPrinted": False,
|
|
})
|
|
return results
|
|
|
|
def generate_api_key():
|
|
alphabet = string.ascii_letters + string.digits
|
|
return "sk-unidesk-codex-" + "".join(secrets.choice(alphabet) for _ in range(48))
|
|
|
|
def ensure_api_key_secret(group_id):
|
|
existing = None
|
|
try:
|
|
existing = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
|
|
except Exception:
|
|
existing = None
|
|
api_key = existing if existing else generate_api_key()
|
|
secret_action = "kept-existing" if existing else "created"
|
|
manifest = {
|
|
"apiVersion": "v1",
|
|
"kind": "Secret",
|
|
"metadata": {
|
|
"name": POOL_API_KEY_SECRET_NAME,
|
|
"namespace": NAMESPACE,
|
|
"labels": {
|
|
"app.kubernetes.io/name": "sub2api",
|
|
"app.kubernetes.io/part-of": "platform-infra",
|
|
"app.kubernetes.io/managed-by": "unidesk",
|
|
"unidesk.ai/secret-purpose": "sub2api-codex-pool-api-key",
|
|
},
|
|
},
|
|
"type": "Opaque",
|
|
"stringData": {
|
|
POOL_API_KEY_SECRET_KEY: api_key,
|
|
"GROUP_ID": str(group_id),
|
|
"GROUP_NAME": POOL_GROUP_NAME,
|
|
"SERVICE_DNS": SERVICE_DNS,
|
|
},
|
|
}
|
|
proc = kubectl(["apply", "--server-side", "--force-conflicts", f"--field-manager={FIELD_MANAGER}", "-f", "-"], json.dumps(manifest))
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"apply API key secret failed: {text(proc.stderr, 1000)}")
|
|
return api_key, secret_action, text(proc.stdout, 1000)
|
|
|
|
def list_user_keys(token):
|
|
data = ensure_success(curl_api("GET", "/api/v1/keys?page=1&page_size=200", bearer=token), "list user keys")
|
|
return extract_items(data)
|
|
|
|
def ensure_sub2api_api_key(token, api_key, group_id):
|
|
keys = list_user_keys(token)
|
|
existing = next((item for item in keys if item.get("key") == api_key), None)
|
|
action = "kept-existing"
|
|
if existing is None:
|
|
existing = next((item for item in keys if item.get("name") == POOL_API_KEY_NAME), None)
|
|
if existing is None or existing.get("key") != api_key:
|
|
payload = {
|
|
"name": POOL_API_KEY_NAME,
|
|
"group_id": group_id,
|
|
"custom_key": api_key,
|
|
"quota": 0,
|
|
"rate_limit_5h": 0,
|
|
"rate_limit_1d": 0,
|
|
"rate_limit_7d": 0,
|
|
}
|
|
created = ensure_success(curl_api("POST", "/api/v1/keys", bearer=token, payload=payload), "create pool API key")
|
|
existing = created if isinstance(created, dict) else existing
|
|
action = "created"
|
|
elif existing.get("id") is not None and existing.get("group_id") != group_id:
|
|
updated = ensure_success(curl_api("PUT", f"/api/v1/keys/{existing['id']}", bearer=token, payload={"name": POOL_API_KEY_NAME, "group_id": group_id}), "update pool API key group")
|
|
existing = updated if isinstance(updated, dict) else existing
|
|
action = "updated-group"
|
|
return {
|
|
"action": action,
|
|
"id": existing.get("id") if isinstance(existing, dict) else None,
|
|
"name": existing.get("name") if isinstance(existing, dict) else POOL_API_KEY_NAME,
|
|
"groupId": existing.get("group_id") if isinstance(existing, dict) else group_id,
|
|
"userId": existing.get("user_id") if isinstance(existing, dict) else None,
|
|
}
|
|
|
|
def get_admin_user(token, user_id):
|
|
data = ensure_success(curl_api("GET", f"/api/v1/admin/users/{user_id}", bearer=token), "get API key owner")
|
|
if not isinstance(data, dict):
|
|
raise RuntimeError("API key owner response is not an object")
|
|
return data
|
|
|
|
def ensure_pool_owner_balance(token, user_id):
|
|
user = get_admin_user(token, user_id)
|
|
current = float(user.get("balance") or 0)
|
|
if current >= MIN_OWNER_BALANCE_USD:
|
|
return {
|
|
"action": "kept-existing",
|
|
"userId": user_id,
|
|
"balanceBefore": current,
|
|
"balanceAfter": current,
|
|
"minimumBalanceUsd": MIN_OWNER_BALANCE_USD,
|
|
}
|
|
updated = ensure_success(curl_api("POST", f"/api/v1/admin/users/{user_id}/balance", bearer=token, payload={
|
|
"balance": MIN_OWNER_BALANCE_USD,
|
|
"operation": "set",
|
|
"notes": "UniDesk Sub2API Codex pool internal API key bootstrap balance.",
|
|
}), "set API key owner balance")
|
|
after = float(updated.get("balance") or MIN_OWNER_BALANCE_USD) if isinstance(updated, dict) else MIN_OWNER_BALANCE_USD
|
|
return {
|
|
"action": "set",
|
|
"userId": user_id,
|
|
"balanceBefore": current,
|
|
"balanceAfter": after,
|
|
"minimumBalanceUsd": MIN_OWNER_BALANCE_USD,
|
|
}
|
|
|
|
def validate_gateway(api_key):
|
|
resp = curl_api("GET", "/v1/models", bearer=api_key)
|
|
parsed = resp.get("json")
|
|
model_count = None
|
|
if isinstance(parsed, dict) and isinstance(parsed.get("data"), list):
|
|
model_count = len(parsed["data"])
|
|
return {
|
|
"ok": resp.get("ok"),
|
|
"httpStatus": resp.get("httpStatus"),
|
|
"transportExitCode": resp.get("transportExitCode"),
|
|
"modelCount": model_count,
|
|
"bodyPreview": text(resp.get("body", ""), 500) if not resp.get("ok") else "",
|
|
"stderr": resp.get("stderr", ""),
|
|
"method": "GET /v1/models",
|
|
"serviceDns": SERVICE_DNS,
|
|
"valuesPrinted": False,
|
|
}
|
|
|
|
def account_capacity_status(token):
|
|
accounts = list_accounts(token)
|
|
by_name = {item.get("name"): item for item in accounts if isinstance(item.get("name"), str)}
|
|
items = []
|
|
missing = []
|
|
mismatched = []
|
|
for name in sorted(EXPECTED_ACCOUNT_CAPACITIES):
|
|
expected = int(EXPECTED_ACCOUNT_CAPACITIES[name])
|
|
account = by_name.get(name)
|
|
if account is None:
|
|
missing.append(name)
|
|
items.append({
|
|
"accountName": name,
|
|
"accountId": None,
|
|
"expectedCapacity": expected,
|
|
"runtimeConcurrency": None,
|
|
"ok": False,
|
|
})
|
|
continue
|
|
runtime = account.get("concurrency")
|
|
ok = runtime == expected
|
|
if not ok:
|
|
mismatched.append(name)
|
|
items.append({
|
|
"accountName": name,
|
|
"accountId": account.get("id"),
|
|
"expectedCapacity": expected,
|
|
"runtimeConcurrency": runtime,
|
|
"priority": account.get("priority"),
|
|
"status": account.get("status"),
|
|
"schedulable": account.get("schedulable"),
|
|
"ok": ok,
|
|
})
|
|
return {
|
|
"ok": len(missing) == 0 and len(mismatched) == 0,
|
|
"defaultAccountCapacity": POOL_DEFAULT_ACCOUNT_CAPACITY,
|
|
"desired": len(EXPECTED_ACCOUNT_CAPACITIES),
|
|
"missing": missing,
|
|
"mismatched": mismatched,
|
|
"items": items,
|
|
"valuesPrinted": False,
|
|
}
|
|
|
|
def api_key_preview(api_key):
|
|
if len(api_key) <= 14:
|
|
return "***"
|
|
return api_key[:10] + "..." + api_key[-4:]
|
|
|
|
def run_sync():
|
|
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8"))
|
|
profiles = payload.get("profiles") or []
|
|
if not profiles:
|
|
raise RuntimeError("sync payload has no profiles")
|
|
admin_email, token = login()
|
|
group, group_action = ensure_group(token)
|
|
group_id = group.get("id") if isinstance(group, dict) else None
|
|
if group_id is None:
|
|
raise RuntimeError("pool group id missing after ensure")
|
|
account_results, pruned_account_results = ensure_accounts(token, profiles, group_id)
|
|
capacity_status = account_capacity_status(token)
|
|
api_key, secret_action, secret_apply_stdout = ensure_api_key_secret(group_id)
|
|
api_key_result = ensure_sub2api_api_key(token, api_key, group_id)
|
|
owner_balance = ensure_pool_owner_balance(token, api_key_result["userId"])
|
|
gateway = validate_gateway(api_key)
|
|
return {
|
|
"ok": gateway["ok"] is True and capacity_status["ok"] is True,
|
|
"mode": "sync",
|
|
"namespace": NAMESPACE,
|
|
"serviceDns": SERVICE_DNS,
|
|
"appPod": APP_POD,
|
|
"admin": {"email": admin_email, "tokenPrinted": False},
|
|
"pool": {"name": POOL_GROUP_NAME, "id": group_id, "action": group_action, "platform": group.get("platform") if isinstance(group, dict) else "openai"},
|
|
"accounts": {
|
|
"desired": len(profiles),
|
|
"created": sum(1 for item in account_results if item["action"] == "created"),
|
|
"updated": sum(1 for item in account_results if item["action"] == "updated"),
|
|
"pruned": len(pruned_account_results),
|
|
"items": account_results,
|
|
"prunedItems": pruned_account_results,
|
|
"valuesPrinted": False,
|
|
},
|
|
"capacity": capacity_status,
|
|
"apiKey": {
|
|
"name": POOL_API_KEY_NAME,
|
|
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
|
|
"secretAction": secret_action,
|
|
"secretApply": secret_apply_stdout,
|
|
"sub2apiAction": api_key_result["action"],
|
|
"sub2apiId": api_key_result["id"],
|
|
"groupId": api_key_result["groupId"],
|
|
"userId": api_key_result["userId"],
|
|
"keyPreview": api_key_preview(api_key),
|
|
"valuesPrinted": False,
|
|
},
|
|
"ownerBalance": owner_balance,
|
|
"validation": {"gatewayModels": gateway},
|
|
}
|
|
|
|
def run_validate():
|
|
api_key = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
|
|
if not api_key:
|
|
raise RuntimeError(f"{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY} missing")
|
|
admin_email, token = login()
|
|
key_item = next((item for item in list_user_keys(token) if item.get("key") == api_key), None)
|
|
owner_balance = None
|
|
if key_item is not None and key_item.get("user_id") is not None:
|
|
owner_balance = ensure_pool_owner_balance(token, key_item["user_id"])
|
|
capacity_status = account_capacity_status(token)
|
|
gateway = validate_gateway(api_key)
|
|
return {
|
|
"ok": gateway["ok"] is True and capacity_status["ok"] is True,
|
|
"mode": "validate",
|
|
"namespace": NAMESPACE,
|
|
"serviceDns": SERVICE_DNS,
|
|
"appPod": APP_POD,
|
|
"admin": {"email": admin_email, "tokenPrinted": False},
|
|
"apiKey": {
|
|
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
|
|
"sub2apiId": key_item.get("id") if isinstance(key_item, dict) else None,
|
|
"userId": key_item.get("user_id") if isinstance(key_item, dict) else None,
|
|
"keyPreview": api_key_preview(api_key),
|
|
"valuesPrinted": False,
|
|
},
|
|
"ownerBalance": owner_balance,
|
|
"capacity": capacity_status,
|
|
"validation": {"gatewayModels": gateway},
|
|
}
|
|
|
|
try:
|
|
result = run_sync() if MODE == "sync" else run_validate()
|
|
except Exception as exc:
|
|
result = {
|
|
"ok": False,
|
|
"mode": MODE,
|
|
"namespace": NAMESPACE,
|
|
"serviceDns": SERVICE_DNS,
|
|
"appPod": globals().get("APP_POD"),
|
|
"error": str(exc),
|
|
"valuesPrinted": False,
|
|
}
|
|
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if result.get("ok") else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
async function capture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise<SshCaptureResult> {
|
|
return await runSshCommandCapture(config, target, args, input);
|
|
}
|
|
|
|
function parseJsonOutput(stdout: string): Record<string, unknown> | null {
|
|
const trimmed = stdout.trim();
|
|
if (trimmed.length === 0) return null;
|
|
const start = trimmed.indexOf("{");
|
|
const end = trimmed.lastIndexOf("}");
|
|
if (start === -1 || end === -1 || end <= start) return null;
|
|
try {
|
|
const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown;
|
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function boolField(value: Record<string, unknown> | null, key: string, defaultValue: boolean): boolean {
|
|
if (value === null) return defaultValue;
|
|
const field = value[key];
|
|
return typeof field === "boolean" ? field : defaultValue;
|
|
}
|
|
|
|
function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record<string, unknown> {
|
|
const full = options.full ?? false;
|
|
return {
|
|
exitCode: result.exitCode,
|
|
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
|
|
stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
|
|
stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "",
|
|
stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "",
|
|
};
|
|
}
|