Merge remote-tracking branch 'origin/master' into feat/2891-hwpod-qemu

This commit is contained in:
pikastech
2026-07-23 06:05:03 +02:00
18 changed files with 1270 additions and 116 deletions
@@ -0,0 +1,759 @@
// SPEC: PJ2026-010603 YAML运维。独立于 Codex pool 的 Sub2API 上游分组。
import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
import { runSshCommandCapture } from "./ssh";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./platform-infra-sub2api-codex/runtime-target";
const configPath = rootPath("config/platform-infra/sub2api-upstream-groups.yaml");
type Protocol = "openai-chat";
interface AccountConfig {
id: string;
name: string;
protocol: Protocol;
responsesMode: "force_chat_completions";
modelPrefix: string;
baseUrl: string;
modelsPath: string;
smokeModel: string | null;
sourceRef: string;
priority: number;
capacity: number;
loadFactor: number;
}
interface GroupConfig {
id: string;
name: string;
description: string;
platform: "openai";
consumerApiKeyName: string;
consumerApiKeySourceRef: string;
accounts: AccountConfig[];
}
interface UpstreamGroupsConfig {
defaultTargetId: string;
groups: GroupConfig[];
}
interface CliOptions {
action: "plan" | "apply" | "status" | "smoke" | "credential-init";
targetId: string;
groupId: string | null;
accountId: string | null;
confirm: boolean;
json: boolean;
}
export async function runSub2ApiUpstreamGroupsCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const options = parseOptions(args);
const desired = readConfig();
const targetId = options.targetId || desired.defaultTargetId;
if (options.action === "credential-init") return credentialInit(desired, options);
const selected = selectGroups(desired, options.groupId);
const local = await localPlan(selected);
if (options.action === "plan") return renderResult(planResult(targetId, selected, local), options);
if (options.action === "smoke") return renderResult(await smokeGroups(config, targetId, selected, local), options);
if (options.action === "apply") {
if (!options.confirm || !local.ok) {
return renderResult({
...planResult(targetId, selected, local),
mode: options.confirm ? "blocked-invalid-secret-or-upstream" : "dry-run",
mutation: false,
next: options.confirm ? "修复缺失 Secret 或上游模型探针后重试。" : confirmCommand(options),
}, options);
}
return renderResult(await applyGroups(config, targetId, selected, local), options);
}
return renderResult(await statusGroups(config, targetId, selected, local), options);
}
function parseOptions(args: string[]): CliOptions {
const [actionRaw = "plan", ...rest] = args;
if (!["plan", "apply", "status", "smoke", "credential-init"].includes(actionRaw)) {
throw new Error("upstream-groups usage: plan|apply|status|smoke|credential-init [--group <id>] [--account <id>] [--target <id>] [--confirm] [--json]");
}
let targetId = defaultCodexPoolRuntimeTargetId();
let groupId: string | null = null;
let accountId: string | null = null;
let confirm = false;
let json = false;
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index]!;
const value = (name: string): string => {
const next = rest[index + 1];
if (next === undefined || next.startsWith("--")) throw new Error(`${name} requires a value`);
index += 1;
return next;
};
if (arg === "--confirm") confirm = true;
else if (arg === "--json") json = true;
else if (arg === "--target") targetId = value("--target");
else if (arg.startsWith("--target=")) targetId = arg.slice(9);
else if (arg === "--group") groupId = value("--group");
else if (arg.startsWith("--group=")) groupId = arg.slice(8);
else if (arg === "--account") accountId = value("--account");
else if (arg.startsWith("--account=")) accountId = arg.slice(10);
else throw new Error(`unsupported upstream-groups option: ${arg}`);
}
const action = actionRaw as CliOptions["action"];
if (action === "credential-init" && accountId === null) throw new Error("credential-init requires --account <id>");
if (action !== "credential-init" && accountId !== null) throw new Error("--account only supports credential-init");
if (action !== "apply" && action !== "credential-init" && confirm) throw new Error(`${action} does not accept --confirm`);
return { action, targetId, groupId, accountId, confirm, json };
}
function readConfig(): UpstreamGroupsConfig {
const raw = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown;
const root = record(raw, configPath);
const defaults = record(root.defaults, `${configPath}.defaults`);
const defaultTargetId = text(defaults.targetId, `${configPath}.defaults.targetId`);
if (!Array.isArray(root.groups) || root.groups.length === 0) throw new Error(`${configPath}.groups must be a non-empty list`);
const groups = root.groups.map((entry, groupIndex): GroupConfig => {
const group = record(entry, `${configPath}.groups[${groupIndex}]`);
const consumer = record(group.consumerApiKey, `${configPath}.groups[${groupIndex}].consumerApiKey`);
const platform = text(group.platform, `${configPath}.groups[${groupIndex}].platform`);
if (platform !== "openai") throw new Error(`${configPath}.groups[${groupIndex}].platform must be openai`);
if (!Array.isArray(group.accounts) || group.accounts.length === 0) throw new Error(`${configPath}.groups[${groupIndex}].accounts must be non-empty`);
return {
id: identifier(group.id, `${configPath}.groups[${groupIndex}].id`),
name: text(group.name, `${configPath}.groups[${groupIndex}].name`),
description: text(group.description, `${configPath}.groups[${groupIndex}].description`),
platform,
consumerApiKeyName: text(consumer.name, `${configPath}.groups[${groupIndex}].consumerApiKey.name`),
consumerApiKeySourceRef: absolutePath(consumer.sourceRef, `${configPath}.groups[${groupIndex}].consumerApiKey.sourceRef`),
accounts: group.accounts.map((item, accountIndex): AccountConfig => {
const account = record(item, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}]`);
const credential = record(account.credential, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].credential`);
const protocol = text(account.protocol, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].protocol`);
if (protocol !== "openai-chat") throw new Error(`${configPath}.groups[${groupIndex}].accounts[${accountIndex}].protocol must be openai-chat`);
const responsesMode = text(account.responsesMode, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].responsesMode`);
if (responsesMode !== "force_chat_completions") throw new Error(`${configPath}.groups[${groupIndex}].accounts[${accountIndex}].responsesMode must be force_chat_completions`);
const baseUrl = new URL(text(account.baseUrl, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].baseUrl`));
if (baseUrl.protocol !== "https:") throw new Error("upstream baseUrl must use https");
return {
id: identifier(account.id, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].id`),
name: text(account.name, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].name`),
protocol,
responsesMode,
modelPrefix: identifier(account.modelPrefix, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].modelPrefix`),
baseUrl: baseUrl.toString().replace(/\/$/u, ""),
modelsPath: pathValue(account.modelsPath, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].modelsPath`),
smokeModel: account.smokeModel === undefined ? null : text(account.smokeModel, "smokeModel"),
sourceRef: absolutePath(credential.sourceRef, `${configPath}.groups[${groupIndex}].accounts[${accountIndex}].credential.sourceRef`),
priority: boundedInteger(account.priority, "priority", 1, 1000),
capacity: boundedInteger(account.capacity, "capacity", 1, 100000),
loadFactor: boundedInteger(account.loadFactor, "loadFactor", 1, 1000),
};
}),
};
});
const ids = groups.flatMap((group) => [group.id, ...group.accounts.map((account) => account.id)]);
if (new Set(ids).size !== ids.length) throw new Error(`${configPath} group/account ids must be globally unique`);
return { defaultTargetId, groups };
}
function credentialInit(config: UpstreamGroupsConfig, options: CliOptions): Record<string, unknown> {
const account = config.groups.flatMap((group) => group.accounts).find((item) => item.id === options.accountId);
if (account === undefined) throw new Error(`unknown upstream account id: ${options.accountId}`);
if (!options.confirm) {
return {
ok: true,
action: "platform-infra-sub2api-upstream-groups-credential-init",
mode: "dry-run",
account: { id: account.id, name: account.name },
secret: secretSummary(account.sourceRef),
mutation: false,
valuesPrinted: false,
next: `将 API key 通过标准输入传给:bun scripts/cli.ts platform-infra sub2api upstream-groups credential-init --account ${account.id} --confirm`,
};
}
const value = readFileSync(0, "utf8").trim();
if (!/^sk-[A-Za-z0-9_=+/.-]{16,}$/u.test(value)) throw new Error("stdin must contain one supported API key");
mkdirSync(dirname(account.sourceRef), { recursive: true, mode: 0o700 });
writeFileSync(account.sourceRef, `${value}\n`, { encoding: "utf8", mode: 0o600 });
chmodSync(account.sourceRef, 0o600);
return {
ok: true,
action: "platform-infra-sub2api-upstream-groups-credential-init",
mode: "confirmed",
account: { id: account.id, name: account.name },
secret: secretSummary(account.sourceRef),
mutation: true,
valuesPrinted: false,
};
}
async function localPlan(groups: GroupConfig[]): Promise<Record<string, unknown>> {
const accounts = [];
for (const group of groups) {
for (const account of group.accounts) {
const secret = secretSummary(account.sourceRef);
const models = secret.present ? await probeModels(account, readSecret(account.sourceRef)) : { ok: false, status: null, models: [], error: "secret-missing" };
accounts.push({
groupId: group.id,
id: account.id,
name: account.name,
protocol: account.protocol,
baseUrl: account.baseUrl,
secret,
models: { ok: models.ok, status: models.status, count: models.models.length, sample: models.models.slice(0, 8), all: models.models, error: models.error },
});
}
}
return { ok: accounts.every((item) => item.secret.present === true && item.models.ok === true), accounts, valuesPrinted: false };
}
async function probeModels(account: AccountConfig, apiKey: string): Promise<{ ok: boolean; status: number | null; models: string[]; error: string | null }> {
try {
const response = await fetch(`${account.baseUrl}${account.modelsPath}`, {
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
signal: AbortSignal.timeout(20_000),
});
const body = await response.json() as unknown;
const models = modelIds(body);
return { ok: response.ok && models.length > 0, status: response.status, models, error: response.ok ? null : `http-${response.status}` };
} catch (error) {
return { ok: false, status: null, models: [], error: error instanceof Error ? error.message.slice(0, 160) : "probe-failed" };
}
}
function planResult(targetId: string, groups: GroupConfig[], local: Record<string, unknown>): Record<string, unknown> {
return {
ok: local.ok === true,
action: "platform-infra-sub2api-upstream-groups-plan",
mode: "dry-run",
targetId,
configPath,
groups: groups.map(redactGroup),
local,
isolation: { codexPoolMembership: false, codexSettingsApplied: false },
mutation: false,
valuesPrinted: false,
next: `bun scripts/cli.ts platform-infra sub2api upstream-groups apply --target ${targetId} --confirm`,
};
}
async function applyGroups(config: UniDeskConfig, targetId: string, groups: GroupConfig[], local: Record<string, unknown>): Promise<Record<string, unknown>> {
const target = codexPoolRuntimeTarget(targetId);
if (target.runtimeMode !== "host-docker" || target.hostDockerAppPort === null || target.hostDockerEnvPath === null) {
throw new Error("upstream-groups apply currently requires a YAML-selected host-docker target");
}
const payloadGroups = groups.map((group) => {
const consumerKey = ensureConsumerKey(group.consumerApiKeySourceRef);
return {
id: group.id,
name: group.name,
description: group.description,
platform: group.platform,
consumerApiKeyName: group.consumerApiKeyName,
consumerApiKey: consumerKey,
accounts: group.accounts.map((account) => {
const planned = Array.isArray(local.accounts)
? local.accounts.filter(isRecord).find((item) => item.id === account.id)
: undefined;
const modelProbe = planned !== undefined && isRecord(planned.models) ? planned.models : {};
const models = Array.isArray(modelProbe.all)
? modelProbe.all.filter((model): model is string => typeof model === "string")
: [];
return {
id: account.id,
name: account.name,
protocol: account.protocol,
responsesMode: account.responsesMode,
modelPrefix: account.modelPrefix,
baseUrl: account.baseUrl,
apiKey: readSecret(account.sourceRef),
sourceRef: account.sourceRef,
secretFingerprint: fingerprint(readSecret(account.sourceRef)),
modelMapping: Object.fromEntries(models.map((model) => [`${account.modelPrefix}/${model.toLowerCase()}`, model])),
priority: account.priority,
capacity: account.capacity,
loadFactor: account.loadFactor,
};
}),
};
});
const script = remoteApplyScript(target.hostDockerAppPort, target.hostDockerEnvPath, payloadGroups);
const result = await runSshCommandCapture(config, target.route, ["sh"], script);
if (result.exitCode !== 0) throw new Error(`upstream-groups apply failed: ${result.stderr.slice(-1200)}`);
const remote = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
return {
ok: remote.ok === true,
action: "platform-infra-sub2api-upstream-groups-apply",
mode: "confirmed",
targetId,
configPath,
groups: groups.map(redactGroup),
local,
remote,
isolation: { codexPoolMembership: false, codexSettingsApplied: false },
mutation: true,
valuesPrinted: false,
};
}
async function statusGroups(config: UniDeskConfig, targetId: string, groups: GroupConfig[], local: Record<string, unknown>): Promise<Record<string, unknown>> {
const target = codexPoolRuntimeTarget(targetId);
if (target.runtimeMode !== "host-docker" || target.hostDockerAppPort === null || target.hostDockerEnvPath === null) {
throw new Error("upstream-groups status currently requires a YAML-selected host-docker target");
}
const script = remoteStatusScript(target.hostDockerAppPort, target.hostDockerEnvPath, groups.map((group) => ({
id: group.id,
name: group.name,
accountNames: group.accounts.map((account) => account.name),
})));
const result = await runSshCommandCapture(config, target.route, ["sh"], script);
if (result.exitCode !== 0) throw new Error(`upstream-groups status failed: ${result.stderr.slice(-1200)}`);
const remote = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
return {
ok: local.ok === true && remote.ok === true,
action: "platform-infra-sub2api-upstream-groups-status",
targetId,
configPath,
local,
remote,
isolation: { codexPoolMembership: false, codexSettingsApplied: false },
mutation: false,
valuesPrinted: false,
};
}
async function smokeGroups(config: UniDeskConfig, targetId: string, groups: GroupConfig[], local: Record<string, unknown>): Promise<Record<string, unknown>> {
const target = codexPoolRuntimeTarget(targetId);
if (target.publicBaseUrl === null) throw new Error(`target ${targetId} has no enabled publicBaseUrl`);
const directPayload = groups.flatMap((group) => group.accounts.map((account) => ({
groupId: group.id,
account: account.name,
url: `${account.baseUrl}/chat/completions`,
apiKey: readSecret(account.sourceRef),
model: account.smokeModel,
})));
const directCapture = await runSshCommandCapture(config, target.route, ["sh"], remoteDirectSmokeScript(directPayload));
if (directCapture.exitCode !== 0) throw new Error(`upstream-groups direct smoke failed: ${directCapture.stderr.slice(-1200)}`);
const direct = JSON.parse(directCapture.stdout.trim()) as Record<string, unknown>;
const results = [];
for (const group of groups) {
const apiKey = readSecret(group.consumerApiKeySourceRef);
for (const account of group.accounts) {
if (account.smokeModel === null) {
results.push({ groupId: group.id, account: account.name, ok: false, model: null, status: null, error: "smokeModel-not-configured" });
continue;
}
try {
const response = await fetch(`${target.publicBaseUrl.replace(/\/+$/u, "")}/v1/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
model: `${account.modelPrefix}/${account.smokeModel.toLowerCase()}`,
messages: [{ role: "user", content: "Reply with exactly: OK" }],
stream: false,
max_tokens: 16,
}),
signal: AbortSignal.timeout(90_000),
});
const body = await response.json().catch(() => ({})) as unknown;
const root = isRecord(body) ? body : {};
const choices = Array.isArray(root.choices) ? root.choices : [];
const first = choices.find(isRecord);
const message = first !== undefined && isRecord(first.message) ? first.message : {};
const content = typeof message.content === "string" ? message.content.replace(/\s+/gu, " ").trim().slice(0, 120) : null;
const requestId = response.headers.get("x-request-id") ?? (typeof root.id === "string" ? root.id : null);
results.push({
groupId: group.id,
account: account.name,
ok: response.ok && content !== null,
model: `${account.modelPrefix}/${account.smokeModel.toLowerCase()}`,
status: response.status,
requestId,
responsePreview: content,
error: response.ok ? null : `http-${response.status}`,
});
} catch (error) {
results.push({
groupId: group.id,
account: account.name,
ok: false,
model: `${account.modelPrefix}/${account.smokeModel.toLowerCase()}`,
status: null,
requestId: null,
responsePreview: null,
error: error instanceof Error ? error.message.slice(0, 160) : "smoke-failed",
});
}
}
}
return {
ok: local.ok === true && results.every((item) => item.ok),
action: "platform-infra-sub2api-upstream-groups-smoke",
targetId,
publicBaseUrl: target.publicBaseUrl,
local,
direct,
results,
isolation: { codexPoolMembership: false, codexSettingsApplied: false },
mutation: false,
valuesPrinted: false,
};
}
function remoteDirectSmokeScript(accounts: unknown): string {
const payload = Buffer.from(JSON.stringify(accounts), "utf8").toString("base64");
return `
set -eu
python3 - <<'PY'
import base64, json, subprocess
accounts = json.loads(base64.b64decode(${JSON.stringify(payload)}).decode())
results = []
for account in accounts:
if not account.get("model"):
results.append({"groupId": account["groupId"], "account": account["account"], "ok": False, "status": None, "model": None, "error": "smokeModel-not-configured"})
continue
body = json.dumps({"model": account["model"], "messages": [{"role": "user", "content": "Reply with exactly: OK"}], "stream": False, "max_tokens": 16}, separators=(",", ":"))
proc = subprocess.run(["curl", "-sS", "--max-time", "90", "-w", "\\n__HTTP__:%{http_code}", "-H", "Authorization: Bearer " + account["apiKey"], "-H", "Content-Type: application/json", "--data-binary", body, account["url"]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = proc.stdout.decode("utf-8", errors="replace")
marker = "\\n__HTTP__:"
content, status = output.rsplit(marker, 1) if marker in output else ("", "000")
parsed = {}
try:
parsed = json.loads(content)
except Exception:
pass
choices = parsed.get("choices") if isinstance(parsed, dict) else None
message = choices[0].get("message") if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
preview = message.get("content") if isinstance(message, dict) else None
error = parsed.get("error") if isinstance(parsed, dict) else None
error_message = error.get("message") if isinstance(error, dict) else None
results.append({"groupId": account["groupId"], "account": account["account"], "ok": proc.returncode == 0 and status.startswith("2"), "status": int(status) if status.isdigit() else None, "model": account["model"], "responsePreview": str(preview)[:120] if preview is not None else None, "error": str(error_message)[:160] if error_message is not None else (None if status.startswith("2") else "http-" + status)})
print(json.dumps({"ok": all(item["ok"] for item in results), "results": results, "valuesPrinted": False}, ensure_ascii=False))
PY
`;
}
function remoteApplyScript(appPort: number, envPath: string, groups: unknown): string {
const payload = Buffer.from(JSON.stringify(groups), "utf8").toString("base64");
return remotePython(appPort, envPath, `
groups = json.loads(base64.b64decode(${JSON.stringify(payload)}).decode())
token = login()
results = []
for desired in groups:
listed = items(api("GET", "/api/v1/admin/groups/all?platform=openai", token))
current = next((item for item in listed if item.get("name") == desired["name"]), None)
group_payload = {"name": desired["name"], "description": desired["description"], "platform": "openai", "rate_multiplier": 1, "is_exclusive": False, "subscription_type": "standard", "allow_messages_dispatch": False, "require_oauth_only": False, "require_privacy_set": False, "rpm_limit": 0}
if current is None:
current = data(api("POST", "/api/v1/admin/groups", token, group_payload))
group_action = "created"
else:
current = data(api("PUT", "/api/v1/admin/groups/" + str(current["id"]), token, {**group_payload, "status": "active"}))
group_action = "updated"
group_id = current["id"]
account_results = []
for account in desired["accounts"]:
found = items(api("GET", "/api/v1/admin/accounts?page=1&page_size=50&platform=openai&search=" + quote(account["name"]), token))
existing = next((item for item in found if item.get("name") == account["name"]), None)
account_payload = {
"name": account["name"],
"notes": "UniDesk YAML-managed independent upstream; id=" + account["id"] + "; source=" + account["sourceRef"] + "; fingerprint=" + account["secretFingerprint"],
"platform": "openai",
"type": "apikey",
"credentials": {"api_key": account["apiKey"], "base_url": account["baseUrl"], "model_mapping": account["modelMapping"]},
"extra": {"unidesk_managed_upstream_group": True, "unidesk_upstream_id": account["id"], "openai_responses_mode": account["responsesMode"]},
"concurrency": account["capacity"],
"priority": account["priority"],
"rate_multiplier": 1,
"load_factor": account["loadFactor"],
"group_ids": [group_id],
"confirm_mixed_channel_risk": True,
}
if existing and existing.get("id") is not None:
account_payload.pop("platform", None)
account_payload["status"] = "active"
saved = data(api("PUT", "/api/v1/admin/accounts/" + str(existing["id"]), token, account_payload))
action = "updated"
else:
saved = data(api("POST", "/api/v1/admin/accounts", token, account_payload))
action = "created"
saved = data(api("POST", "/api/v1/admin/accounts/" + str(saved["id"]) + "/schedulable", token, {"schedulable": True}))
account_results.append({"id": saved.get("id"), "name": account["name"], "action": action, "groupId": group_id})
keys = items(api("GET", "/api/v1/keys?page=1&page_size=200", token))
key = next((item for item in keys if item.get("name") == desired["consumerApiKeyName"]), None)
if key is None:
key = data(api("POST", "/api/v1/keys", token, {"name": desired["consumerApiKeyName"], "group_id": group_id, "custom_key": desired["consumerApiKey"], "quota": 0, "rate_limit_5h": 0, "rate_limit_1d": 0, "rate_limit_7d": 0}))
key_action = "created"
elif key.get("id") is not None and key.get("group_id") != group_id:
key = data(api("PUT", "/api/v1/keys/" + str(key["id"]), token, {"name": desired["consumerApiKeyName"], "group_id": group_id}))
key_action = "updated"
else:
key_action = "kept"
results.append({"groupId": group_id, "groupName": desired["name"], "groupAction": group_action, "accounts": account_results, "apiKey": {"id": key.get("id"), "name": desired["consumerApiKeyName"], "groupId": group_id, "action": key_action}})
print(json.dumps({"ok": True, "groups": results, "valuesPrinted": False}, ensure_ascii=False))
`);
}
function remoteStatusScript(appPort: number, envPath: string, groups: unknown): string {
const payload = Buffer.from(JSON.stringify(groups), "utf8").toString("base64");
return remotePython(appPort, envPath, `
desired = json.loads(base64.b64decode(${JSON.stringify(payload)}).decode())
token = login()
all_groups = items(api("GET", "/api/v1/admin/groups/all?platform=openai", token))
results = []
for item in desired:
group = next((candidate for candidate in all_groups if candidate.get("name") == item["name"]), None)
if group is None:
results.append({"id": item["id"], "name": item["name"], "present": False, "accounts": []})
continue
raw_accounts = items(api("GET", "/api/v1/admin/accounts?group_id=" + str(group["id"]) + "&page=1&page_size=500&platform=openai", token))
expected_names = set(item["accountNames"])
accounts = [account for account in raw_accounts if account.get("name") in expected_names]
results.append({"id": item["id"], "name": item["name"], "present": True, "groupId": group.get("id"), "expectedAccountCount": len(expected_names), "filteredAccountCount": len(accounts), "rawReturnedCount": len(raw_accounts), "nativeFilterOverbroad": len(raw_accounts) > len(accounts), "accounts": [{"id": account.get("id"), "name": account.get("name"), "status": account.get("status"), "schedulable": account.get("schedulable"), "priority": account.get("priority")} for account in accounts]})
print(json.dumps({"ok": all(item.get("present") and item.get("filteredAccountCount") == item.get("expectedAccountCount") for item in results), "groups": results, "valuesPrinted": False}, ensure_ascii=False))
`);
}
function remotePython(appPort: number, envPath: string, body: string): string {
return `
set -eu
python3 - <<'PY'
import base64, json, subprocess
from urllib.parse import quote
APP_PORT = ${JSON.stringify(appPort)}
ENV_PATH = ${JSON.stringify(envPath)}
def env_values():
try:
raw = open(ENV_PATH, "r", encoding="utf-8").read()
except PermissionError:
proc = subprocess.run(["sudo", "-n", "cat", ENV_PATH], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
raise RuntimeError("cannot read host env source")
raw = proc.stdout.decode()
result = {}
for line in raw.splitlines():
text = line.strip()
if not text or text.startswith("#") or "=" not in text:
continue
key, value = text.split("=", 1)
value = value.strip()
if len(value) > 1 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1]
result[key.strip()] = value
return result
def api(method, path, token=None, payload=None):
command = ["curl", "-sS", "-w", "\\n__HTTP__:%{http_code}", "-X", method, "-H", "Content-Type: application/json"]
if token:
command += ["-H", "Authorization: Bearer " + token]
if payload is not None:
command += ["--data-binary", json.dumps(payload, separators=(",", ":"))]
command += ["http://127.0.0.1:" + str(APP_PORT) + path]
proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = proc.stdout.decode("utf-8", errors="replace")
marker = "\\n__HTTP__:"
content, status = output.rsplit(marker, 1)
parsed = json.loads(content) if content.strip() else {}
code = parsed.get("code") if isinstance(parsed, dict) else None
if proc.returncode != 0 or int(status) >= 400 or (code is not None and code != 0):
message = parsed.get("message") if isinstance(parsed, dict) else "request failed"
raise RuntimeError("Sub2API request failed: http=" + status + " message=" + str(message)[:300])
return parsed
def data(value):
return value.get("data") if isinstance(value, dict) and "data" in value else value
def items(value):
value = data(value)
if isinstance(value, list):
return value
if isinstance(value, dict):
if isinstance(value.get("items"), list):
return value["items"]
for key in ("groups", "accounts", "api_keys", "keys"):
if isinstance(value.get(key), list):
return value[key]
return []
def login():
env = env_values()
email = env.get("ADMIN_EMAIL") or "admin@sub2api.platform-infra.local"
password = env.get("ADMIN_PASSWORD")
if not password:
raise RuntimeError("ADMIN_PASSWORD missing")
value = data(api("POST", "/api/v1/auth/login", payload={"email": email, "password": password}))
token = value.get("access_token") or value.get("token")
if not token:
raise RuntimeError("admin login token missing")
return token
${body}
PY
`;
}
function selectGroups(config: UpstreamGroupsConfig, groupId: string | null): GroupConfig[] {
if (groupId === null) return config.groups;
const selected = config.groups.find((group) => group.id === groupId);
if (selected === undefined) throw new Error(`unknown upstream group id: ${groupId}`);
return [selected];
}
function redactGroup(group: GroupConfig): Record<string, unknown> {
return {
id: group.id,
name: group.name,
platform: group.platform,
consumerApiKey: { name: group.consumerApiKeyName, ...secretSummary(group.consumerApiKeySourceRef) },
accounts: group.accounts.map((account) => ({
id: account.id,
name: account.name,
protocol: account.protocol,
responsesMode: account.responsesMode,
modelPrefix: account.modelPrefix,
baseUrl: account.baseUrl,
priority: account.priority,
capacity: account.capacity,
loadFactor: account.loadFactor,
credential: secretSummary(account.sourceRef),
})),
};
}
function renderResult(result: Record<string, unknown>, options: CliOptions): Record<string, unknown> | RenderedCliResult {
if (options.json) return result;
const local = isRecord(result.local) ? result.local : {};
const accounts = Array.isArray(local.accounts) ? local.accounts.filter(isRecord) : [];
const rows = accounts.map((account) => {
const secret = isRecord(account.secret) ? account.secret : {};
const models = isRecord(account.models) ? account.models : {};
return [
String(account.name ?? "-"),
String(account.protocol ?? "-"),
secret.present === true ? "present" : "missing",
String(secret.fingerprint ?? "-"),
models.ok === true ? "ok" : "failed",
String(models.count ?? 0),
Array.isArray(models.sample) ? models.sample.slice(0, 3).join(",") : "-",
];
});
const remote = isRecord(result.remote) ? result.remote : {};
const direct = isRecord(result.direct) ? result.direct : {};
const smokeResults = Array.isArray(result.results) ? result.results.filter(isRecord) : [];
const lines = [
`SUB2API UPSTREAM GROUPS ${String(result.mode ?? options.action).toUpperCase()} ok=${result.ok === true}`,
`TARGET ${String(result.targetId ?? options.targetId)} CONFIG ${configPath}`,
...table(["ACCOUNT", "PROTOCOL", "SECRET", "FINGERPRINT", "MODELS", "COUNT", "SAMPLE"], rows),
`ISOLATION codexPoolMembership=false codexSettingsApplied=false valuesPrinted=false`,
];
if (Object.keys(remote).length > 0) lines.push(`REMOTE groups=${Array.isArray(remote.groups) ? remote.groups.length : 0} ok=${remote.ok === true}`);
if (Object.keys(direct).length > 0) lines.push(`DIRECT ok=${direct.ok === true}`);
if (smokeResults.length > 0) {
lines.push(...table(
["SMOKE_ACCOUNT", "MODEL", "STATUS", "REQUEST_ID", "RESULT"],
smokeResults.map((item) => [
String(item.account ?? "-"),
String(item.model ?? "-"),
String(item.status ?? "-"),
String(item.requestId ?? "-"),
item.ok === true ? "ok" : String(item.error ?? "failed"),
]),
));
}
if (typeof result.next === "string") lines.push(`NEXT ${result.next}`);
return {
ok: result.ok === true,
command: `platform-infra sub2api upstream-groups ${options.action}`,
renderedText: `${lines.join("\n")}\n`,
contentType: "text/plain",
projection: result,
};
}
function confirmCommand(options: CliOptions): string {
return `bun scripts/cli.ts platform-infra sub2api upstream-groups apply --target ${options.targetId}${options.groupId ? ` --group ${options.groupId}` : ""} --confirm`;
}
function ensureConsumerKey(path: string): string {
if (!existsSync(path)) {
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
writeFileSync(path, `sk-unidesk-upstream-${randomBytes(32).toString("base64url")}\n`, { mode: 0o600 });
chmodSync(path, 0o600);
}
return readSecret(path);
}
function readSecret(path: string): string {
const value = readFileSync(path, "utf8").trim();
if (!value) throw new Error(`Secret source is empty: ${path}`);
return value;
}
function secretSummary(path: string): Record<string, unknown> {
if (!existsSync(path)) return { sourceRef: path, present: false, bytes: 0, fingerprint: null, valuesPrinted: false };
const stat = statSync(path);
const value = readSecret(path);
return { sourceRef: path, present: true, bytes: stat.size, mode: (stat.mode & 0o777).toString(8).padStart(4, "0"), fingerprint: fingerprint(value), valuesPrinted: false };
}
function fingerprint(value: string): string {
return `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 12)}`;
}
function modelIds(value: unknown): string[] {
const root = isRecord(value) ? value : {};
const data = Array.isArray(root.data) ? root.data : Array.isArray(root.models) ? root.models : [];
return [...new Set(data.map((item) => isRecord(item) ? item.id ?? item.name : null).filter((item): item is string => typeof item === "string" && item.length > 0))].sort();
}
function record(value: unknown, key: string): Record<string, unknown> {
if (!isRecord(value)) throw new Error(`${key} must be an object`);
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function text(value: unknown, key: string): string {
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string`);
return value.trim();
}
function identifier(value: unknown, key: string): string {
const result = text(value, key);
if (!/^[a-z0-9][a-z0-9-]{1,62}$/u.test(result)) throw new Error(`${key} must be a lowercase identifier`);
return result;
}
function absolutePath(value: unknown, key: string): string {
const result = text(value, key);
if (!result.startsWith("/") || /[\r\n]/u.test(result)) throw new Error(`${key} must be an absolute path`);
return result;
}
function pathValue(value: unknown, key: string): string {
const result = text(value, key);
if (!result.startsWith("/") || result.includes("..")) throw new Error(`${key} must be an absolute URL path`);
return result;
}
function boundedInteger(value: unknown, key: string, min: number, max: number): number {
if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) throw new Error(`${key} must be an integer from ${min} to ${max}`);
return value as number;
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
return [renderRow(headers), ...rows.map(renderRow)];
}
+4
View File
@@ -147,6 +147,10 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
const { runCodexPoolCommand } = await import("../platform-infra-sub2api-codex");
return await runCodexPoolCommand(config, args.slice(2));
}
if (action === "upstream-groups") {
const { runSub2ApiUpstreamGroupsCommand } = await import("../platform-infra-sub2api-upstream-groups");
return await runSub2ApiUpstreamGroupsCommand(config, args.slice(2));
}
return unsupported(args);
}
+68 -10
View File
@@ -8,6 +8,7 @@ import { startJob } from "./jobs";
import type { SshCaptureResult } from "./ssh";
import { capture, fingerprintValues, parseJsonOutput } from "./platform-infra-public-service";
import {
captureLocal,
yamlBooleanField,
yamlIntegerArrayField,
yamlIntegerField,
@@ -70,9 +71,23 @@ interface RawSourceFileConfig {
type SourceFileConfig = EnvSourceFileConfig | RawSourceFileConfig;
type DistributionTargetExecution = {
executionPlane: "local-k3s";
route: null;
kubeconfig: string;
timeoutSeconds: number;
configPath: string;
} | {
executionPlane: "route";
route: string;
kubeconfig: null;
timeoutSeconds: number;
configPath: string;
};
interface DistributionTarget {
id: string;
route: string;
execution: DistributionTargetExecution;
namespace: string;
scope: string;
enabled: boolean;
@@ -235,8 +250,9 @@ function plan(options: SecretsOptions): Record<string, unknown> {
action: "secrets-plan",
mutation: false,
config: configSummary(distribution, options),
localSources: sourceSummary(sources),
desiredSecrets: desired.map(desiredSecretSummary),
...(options.full
? { localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary) }
: { localSources: compactSourceSummary(sources), desiredSecrets: desired.map(compactDesiredSecretSummary) }),
policy: {
sourceAuthority: "local YAML-declared managed and external sourceRef files",
runtimeReverseEngineering: false,
@@ -304,8 +320,9 @@ async function status(config: UniDeskConfig, options: SecretsOptions): Promise<R
action: "secrets-status",
mutation: false,
config: configSummary(distribution, options),
localSources: sourceSummary(sources),
desiredSecrets: desired.map(desiredSecretSummary),
...(options.full
? { localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary) }
: { localSources: compactSourceSummary(sources), desiredSecrets: desired.map(compactDesiredSecretSummary) }),
targets: perTarget,
valuesPrinted: false,
};
@@ -393,6 +410,30 @@ function parseExternalSourceFile(record: Record<string, unknown>, path: string):
}
function parseTarget(record: Record<string, unknown>, path: string): DistributionTarget {
const executionPath = `${path}.execution`;
const execution = objectField(record, "execution", path);
const executionPlane = stringField(execution, "executionPlane", executionPath);
if (executionPlane !== "local-k3s" && executionPlane !== "route") {
throw new Error(`${executionPath}.executionPlane must be local-k3s or route`);
}
const executionTimeoutSeconds = integerField(execution, "timeoutSeconds", executionPath);
if (executionTimeoutSeconds <= 0) throw new Error(`${executionPath}.timeoutSeconds must be positive`);
let targetExecution: DistributionTargetExecution;
if (executionPlane === "local-k3s") {
if (execution.route !== undefined) throw new Error(`${executionPath}.route is only valid for executionPlane=route`);
const kubeconfig = stringField(execution, "kubeconfig", executionPath);
if (!isAbsolute(kubeconfig)) throw new Error(`${executionPath}.kubeconfig must be an absolute path`);
targetExecution = { executionPlane, route: null, kubeconfig, timeoutSeconds: executionTimeoutSeconds, configPath: executionPath };
} else {
if (execution.kubeconfig !== undefined) throw new Error(`${executionPath}.kubeconfig is only valid for executionPlane=local-k3s`);
targetExecution = {
executionPlane,
route: stringField(execution, "route", executionPath),
kubeconfig: null,
timeoutSeconds: executionTimeoutSeconds,
configPath: executionPath,
};
}
const rollout = record.consumerRollout === undefined ? null : objectField(record, "consumerRollout", path);
const deployments = rollout === null
? []
@@ -404,7 +445,7 @@ function parseTarget(record: Record<string, unknown>, path: string): Distributio
}
return {
id: simpleId(stringField(record, "id", path), `${path}.id`),
route: stringField(record, "route", path),
execution: targetExecution,
namespace: kubernetesNameField(record, "namespace", path),
scope: simpleId(stringField(record, "scope", path), `${path}.scope`),
enabled: booleanField(record, "enabled", path),
@@ -627,7 +668,7 @@ function selectedSourceRefs(config: SecretDistributionConfig, options: SecretsOp
async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" };
const yaml = renderSecretManifest(target, secrets);
const result = await capture(config, target.route, ["sh"], applySecretScript(target, secrets, yaml));
const result = await captureTarget(config, target, applySecretScript(target, secrets, yaml));
const parsed = parseJsonOutput(result.stdout);
const applied = result.exitCode === 0 && boolField(parsed, "ok", false);
const consumerRolloutStatus = applied && target.consumerRollout !== null
@@ -646,7 +687,7 @@ async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTar
}
async function statusTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
const result = await capture(config, target.route, ["sh"], statusSecretScript(target, secrets));
const result = await captureTarget(config, target, statusSecretScript(target, secrets));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
@@ -755,7 +796,7 @@ async function waitForConsumerRollout(config: UniDeskConfig, target: Distributio
let lastRemote: Record<string, unknown> | null = null;
while (true) {
attempts += 1;
const result = await capture(config, target.route, ["sh"], consumerRolloutStatusScript(target));
const result = await captureTarget(config, target, consumerRolloutStatusScript(target));
const parsed = parseJsonOutput(result.stdout);
lastSummary = parsed;
lastRemote = secretCaptureSummary(result);
@@ -977,7 +1018,24 @@ function remoteSecretSummary(secret: DesiredSecret): Record<string, unknown> {
}
function targetSummary(target: DistributionTarget): Record<string, unknown> {
return { id: target.id, route: target.route, namespace: target.namespace, scope: target.scope, consumerRollout: target.consumerRollout };
return {
id: target.id,
execution: target.execution,
namespace: target.namespace,
scope: target.scope,
consumerRollout: target.consumerRollout,
};
}
async function captureTarget(config: UniDeskConfig, target: DistributionTarget, script: string): Promise<SshCaptureResult> {
const timeoutMs = target.execution.timeoutSeconds * 1000;
if (target.execution.executionPlane === "local-k3s") {
return await captureLocal(["sh"], script, {
runtimeTimeoutMs: timeoutMs,
env: { ...process.env, KUBECONFIG: target.execution.kubeconfig },
});
}
return await capture(config, target.execution.route, ["sh"], script, { runtimeTimeoutMs: timeoutMs });
}
function readOptionValue(args: string[], index: number, option: string): string {