File size: 4,769 Bytes
5448d8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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 [];
}