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 ", t("simulate.file")) .option("-m, --model ", t("simulate.model"), "auto") .option("--combo ", t("simulate.combo")) .option("--reasoning-effort ", t("simulate.reasoning")) .option("--thinking-budget ", 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 []; }