Files
2026-07-13 13:39:12 +08:00

126 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const simulateSchema = [
{ key: "order", header: "#", width: 4 },
{ key: "provider", header: "Provider", width: 20 },
{ key: "model", header: "Model", width: 35 },
{ key: "probability", header: "Probability", formatter: (v) => `${Math.round(v * 100)}%` },
{ key: "estimatedCost", header: "Est. Cost", formatter: (v) => (v ? `$${v.toFixed(4)}` : "-") },
{ key: "healthStatus", header: "Breaker", width: 10 },
{ key: "quotaAvailable", header: "Quota %", formatter: (v) => `${v}%` },
];
export function registerSimulate(program) {
program
.command("simulate [prompt]")
.description(t("simulate.description"))
.option("--file <path>", t("simulate.file"))
.option("-m, --model <id>", t("simulate.model"), "auto")
.option("--combo <name>", t("simulate.combo"))
.option("--reasoning-effort <level>", t("simulate.reasoning"))
.option("--thinking-budget <n>", t("simulate.thinking"), parseInt)
.option("--explain", t("simulate.explain"))
.action(runSimulateCommand);
}
export async function runSimulateCommand(promptArg, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
let promptTokenEstimate = 100;
if (opts.file) {
try {
const raw = readFileSync(opts.file, "utf8");
const parsed = JSON.parse(raw);
const text = JSON.stringify(parsed);
promptTokenEstimate = Math.ceil(text.length / 4);
} catch {
promptTokenEstimate = 100;
}
} else if (promptArg) {
promptTokenEstimate = Math.ceil(promptArg.length / 4);
}
const [combosRes, healthRes, quotaRes] = await Promise.allSettled([
apiFetch("/api/combos", { timeout: globalOpts.timeout }).then((r) => r.json()),
apiFetch("/api/monitoring/health", { timeout: globalOpts.timeout }).then((r) => r.json()),
apiFetch("/api/usage/quota", { timeout: globalOpts.timeout }).then((r) => r.json()),
]);
if (combosRes.status === "rejected") {
process.stderr.write(t("common.serverOffline") + "\n");
process.exit(3);
}
const combos = normalizeCombos(combosRes.value);
const health = healthRes.status === "fulfilled" ? healthRes.value : {};
const quota = quotaRes.status === "fulfilled" ? quotaRes.value : {};
const targetCombo = opts.combo
? combos.find((c) => c.id === opts.combo || c.name === opts.combo)
: combos.find((c) => c.enabled !== false);
if (!targetCombo) {
process.stderr.write(t("simulate.noCombo") + "\n");
process.exit(1);
}
const models = getComboModels(targetCombo, opts.model);
const breakers = toArray(health.circuitBreakers ?? health.breakers);
const providers = toArray(quota.providers ?? quota.data);
const simulatedPath = models.map((m, idx) => {
const cb = breakers.find((b) => String(b.provider) === m.provider);
const q = providers.find((p) => p.provider === m.provider);
const inputCost = m.inputCostPer1M ?? 0;
const estimatedCost = Math.round((promptTokenEstimate / 1_000_000) * inputCost * 10000) / 10000;
return {
order: idx + 1,
provider: m.provider,
model: m.model || opts.model,
probability: idx === 0 ? 0.85 : 0.15 / Math.max(models.length - 1, 1),
estimatedCost,
healthStatus: String(cb?.state ?? "CLOSED"),
quotaAvailable: q?.percentRemaining ?? 100,
};
});
emit(simulatedPath, globalOpts, simulateSchema);
if (opts.explain && !globalOpts.quiet) {
const primary = simulatedPath[0];
const fallbacks = simulatedPath.slice(1).map((s) => s.provider);
process.stderr.write(`\nPrimary: ${primary?.provider} / ${primary?.model}\n`);
if (fallbacks.length > 0) {
process.stderr.write(`Fallbacks: ${fallbacks.join(" → ")}\n`);
}
const costs = simulatedPath.map((s) => s.estimatedCost);
process.stderr.write(
`Est. cost range: $${Math.min(...costs).toFixed(4)} $${Math.max(...costs).toFixed(4)}\n`
);
}
}
function normalizeCombos(raw) {
if (Array.isArray(raw)) return raw;
if (raw && Array.isArray(raw.combos)) return raw.combos;
if (raw && Array.isArray(raw.data)) return raw.data;
return [];
}
function getComboModels(combo, modelFallback) {
const steps = combo.steps ?? combo.models ?? combo.targets ?? [];
return steps.map((step) => ({
provider: step.provider ?? step.providerId ?? "",
model: step.model ?? step.modelId ?? modelFallback ?? "auto",
inputCostPer1M: step.inputCostPer1M ?? step.costPer1MInput ?? 0,
}));
}
function toArray(val) {
if (Array.isArray(val)) return val;
return [];
}