Files
openclaw/extensions/qa-lab/src/model-catalog.runtime.ts
2026-04-06 17:23:35 +01:00

127 lines
3.7 KiB
TypeScript

import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { buildQaGatewayConfig } from "./qa-gateway-config.js";
type ModelRow = {
key: string;
name: string;
input: string;
available: boolean | null;
missing: boolean;
};
export type QaRunnerModelOption = {
key: string;
name: string;
provider: string;
input: string;
preferred: boolean;
};
function splitModelKey(key: string) {
const slash = key.indexOf("/");
if (slash <= 0 || slash === key.length - 1) {
return null;
}
return {
provider: key.slice(0, slash),
model: key.slice(slash + 1),
};
}
export function selectQaRunnerModelOptions(rows: ModelRow[]): QaRunnerModelOption[] {
const options = rows
.filter((row) => row.available === true && !row.missing)
.map((row) => {
const parsed = splitModelKey(row.key);
return {
key: row.key,
name: row.name,
provider: parsed?.provider ?? "unknown",
input: row.input,
preferred: row.key === "openai/gpt-5.4",
} satisfies QaRunnerModelOption;
});
return options.toSorted((left, right) => {
if (left.preferred !== right.preferred) {
return left.preferred ? -1 : 1;
}
const providerCompare = left.provider.localeCompare(right.provider);
if (providerCompare !== 0) {
return providerCompare;
}
return left.name.localeCompare(right.name);
});
}
export async function loadQaRunnerModelOptions(params: { repoRoot: string }) {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qa-model-catalog-"));
const workspaceDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const homeDir = path.join(tempRoot, "home");
const configPath = path.join(tempRoot, "openclaw.json");
try {
await Promise.all([
fs.mkdir(workspaceDir, { recursive: true }),
fs.mkdir(stateDir, { recursive: true }),
fs.mkdir(homeDir, { recursive: true }),
]);
const cfg = buildQaGatewayConfig({
bind: "loopback",
gatewayPort: 0,
gatewayToken: "qa-model-catalog",
qaBusBaseUrl: "http://127.0.0.1:9",
workspaceDir,
providerMode: "live-openai",
primaryModel: "openai/gpt-5.4",
alternateModel: "openai/gpt-5.4",
controlUiEnabled: false,
});
await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
const child = spawn(
process.execPath,
["dist/index.js", "models", "list", "--all", "--json"],
{
cwd: params.repoRoot,
env: {
...process.env,
HOME: homeDir,
OPENCLAW_HOME: homeDir,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_OAUTH_DIR: path.join(stateDir, "credentials"),
},
stdio: ["ignore", "pipe", "pipe"],
},
);
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
child.once("error", reject);
child.once("exit", (code) => {
if (code === 0) {
resolve();
return;
}
reject(
new Error(
`qa model catalog failed (${code ?? "unknown"}): ${Buffer.concat(stderr).toString("utf8").trim()}`,
),
);
});
});
const payload = JSON.parse(Buffer.concat(stdout).toString("utf8")) as { models?: ModelRow[] };
return selectQaRunnerModelOptions(payload.models ?? []);
} finally {
await fs.rm(tempRoot, { recursive: true, force: true });
}
}