import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; import { loadRuntimeState, runtimeStatePath } from "./runtime-state.ts"; import type { BifrostConfig } from "./config.ts"; import type { ThinkingLevel } from "./thinking.ts"; import { DEFAULT_RULES, loadConfig, readJson } from "./config.ts"; import type { CacheEntry } from "./cache.ts"; import { cachePath, loadCache, saveCache, DEFAULT_MAX_ENTRIES, DEFAULT_THRESHOLD } from "./cache.ts"; import type { ClassificationPipeline } from "./classification-pipeline.ts"; import { setupDebug, debug, debugMeasure } from "./debug.ts"; import { runProbe, PROBE_PROMPT_TEXT, type ProbeResult } from "./probe.ts"; import { buildDiscoveryMetadata, discoverModels, parseDiscoveryOptions, reconcileDiscoveredModels, type DiscoveryOptions, type DiscoveryResult, } from "./discovery.ts"; import { setBifrostModeStatus, setBifrostStatus } from "./ux-status.ts"; import { showBifrostResult } from "./result-viewer.ts"; import { getStrategy, guessTier, modelKey, resolveModelWithFallback, diagnoseCandidates, type HealthyModelResolution, type RoutingStrategy, } from "./routing.ts"; import { fetchFreeModelRanking, capFreeModels, sortTierModels, FREE_MODEL_LIMIT } from "./collection-ranking.ts"; import type { ReliabilityStore } from "./reliability-store.ts"; import { formatDiagnostic, patternUnresolvable, classifierModelMissing, type BifrostDiagnostic, } from "./diagnostics.ts"; // ── Mutable state shared across commands ──────────────────── export interface BifrostState { config: BifrostConfig; enabled: boolean; classifierEnabled: boolean; thinkingMode: "off" | "advisory" | "apply"; thinkingPinned: boolean; thinkingLevel: ThinkingLevel; lastThinkingDecision?: { score: number; level: ThinkingLevel; reasons: string[] }; previewThinking?: ( prompt: string, selectedTier: string, model: { reasoning?: boolean; thinkingLevelMap?: Record } | undefined, ) => { level: ThinkingLevel; mode: string; summary: string }; pinned: boolean; silent: boolean; cacheEntries: CacheEntry[]; reliabilityStore: ReliabilityStore; extensionDir: string; getPipeline: (ctx: ExtensionContext) => ClassificationPipeline; invalidatePipeline: () => void; /** Persist runtime mode toggles (enabled/classifierEnabled/silent) to disk. */ saveModeState: () => void; lastRegistryRefreshAt?: number; forceRegistryRefresh?: boolean; } const silentContexts = new WeakSet(); export function setBifrostSilent(ctx: ExtensionContext, silent: boolean): void { if (silent) silentContexts.add(ctx); else silentContexts.delete(ctx); } function isBifrostSilent(ctx: ExtensionContext): boolean { return silentContexts.has(ctx); } let overwriteActive = false; export function logOverwrite( ctx: ExtensionContext, message: string, ): void { if (isBifrostSilent(ctx)) return; if (ctx.mode === "tui") { if (ctx.hasUI) { ctx.ui.setWorkingMessage(message); ctx.ui.setWorkingVisible(true); } return; } process.stderr.write(`\r\x1b[2K[bifrost] ${message}`); overwriteActive = true; } export function finalizeOverwrite(): void { if (overwriteActive) { process.stderr.write("\n"); overwriteActive = false; } } export function log( ctx: ExtensionContext, message: string, type?: "info" | "warning" | "error", force = false ) { if (!force && isBifrostSilent(ctx)) return; finalizeOverwrite(); if (ctx.mode !== "tui") { console.error(`[bifrost] ${message}`); } if (ctx.hasUI) ctx.ui.notify(message, type ?? "info"); } // Rainbow letters mirror the status-line `bifrost` word. const BIFROST_RAINBOW = ["255;0;0", "255;127;0", "255;255;0", "0;255;0", "65;105;255", "105;65;165", "180;70;225"]; const TIER_COLORS: Record = { quick: "\x1b[32m", // green general: "\x1b[36m", // cyan frontier: "\x1b[38;5;208m", // orange }; const HOT_PINK = "\x1b[38;2;255;105;180m"; // pinned category slot function rainbowWord(word: string): string { return [...word] .map((ch, i) => `\x1b[38;2;${BIFROST_RAINBOW[i % BIFROST_RAINBOW.length]}m${ch}`) .join("") + "\x1b[0m"; } /** `Bifrost: (suffix)` with rainbow word, white arrow, violet model. When `pinned`, the category slot shows `pinned` in hot pink. */ export function formatBifrostRouting(tier: string, model: string, suffix = "", pinned = false): string { const category = pinned ? `${HOT_PINK}pinned\x1b[0m` : `${TIER_COLORS[tier] ?? "\x1b[0m"}${tier}\x1b[0m`; const arrow = "\x1b[37m→\x1b[0m"; const tail = suffix ? ` \x1b[90m(${suffix})\x1b[0m` : ""; return `${rainbowWord("Bifrost")}: ${category} ${arrow} \x1b[38;2;180;70;225m${model}\x1b[0m${tail}`; } export function uiBusy(ctx: ExtensionContext, message: string) { if (isBifrostSilent(ctx)) return; finalizeOverwrite(); if (ctx.mode === "tui") { if (ctx.hasUI) { ctx.ui.setWorkingMessage(message); ctx.ui.setWorkingVisible(true); } } else { console.error(`[bifrost] ${message}`); } } export function uiDone(ctx: ExtensionContext) { if (ctx.mode === "tui" && ctx.hasUI) { ctx.ui.setWorkingMessage(undefined); } } function uiOutput(ctx: ExtensionContext, lines: string[]) { if (isBifrostSilent(ctx)) return; finalizeOverwrite(); if (ctx.mode === "tui") { if (ctx.hasUI) { ctx.ui.setWidget("bifrost-output", lines); } } else { for (const line of lines) console.error(`[bifrost] ${line}`); } } async function uiResult(ctx: ExtensionContext, title: string, lines: string[]): Promise { if (isBifrostSilent(ctx)) return; finalizeOverwrite(); if (await showBifrostResult(ctx, title, lines)) return; if (ctx.mode !== "tui") { for (const line of lines) console.error(`[bifrost] ${line}`); } } export function clearBifrostWidgets(ctx: ExtensionContext) { if (ctx.mode === "tui" && ctx.hasUI) { ctx.ui.setWidget("bifrost-output", []); ctx.ui.setWidget("bifrost-probe", []); } } export function syncBifrostModeStatus(ctx: ExtensionContext, state: Pick) { setBifrostModeStatus(ctx, state); } function openCircuitCount(state: BifrostState, now = Date.now()): number { return state.reliabilityStore.openCircuitCount(now); } /** Feed probe results into the reliability store as circuit-breaker outcomes. */ function applyProbeOutcomes(state: BifrostState, results: ProbeResult[]): void { state.reliabilityStore.applyOutcomes( results.map((r) => r.status === "ok" ? { model: `${r.provider}/${r.model}`, ok: true as const, source: "probe" } : { model: `${r.provider}/${r.model}`, ok: false as const, source: "probe", reason: r.error ?? r.status } ), Date.now(), ); } function formatCandidateLines( resolution: HealthyModelResolution, selectedKey: string | undefined, ): string[] { const skipped = new Map(resolution.skipped.map((item) => [item.key, item])); return resolution.candidates.map((m) => { const key = modelKey(m); const skippedCandidate = skipped.get(key); if (skippedCandidate) { const until = skippedCandidate.openUntil ? new Date(skippedCandidate.openUntil).toISOString() : "unknown"; return `xx ${key} (open circuit until ${until})`; } const marker = key === selectedKey ? "=>" : " "; return `${marker} ${key} ($${(m.cost.input + m.cost.output).toFixed(2)}/1M tokens, ctx ${m.contextWindow})`; }); } // ── Shared tier-resolution + display ─────────────────────── function resolveTierDisplay( tier: string, state: BifrostState, ctx: ExtensionContext, ) { const pattern = state.config.models?.[tier] ?? tier; const strategy = getStrategy(state.config.categoryStrategies, state.config.strategy, tier); const defaultTier = state.config.default; const defaultPattern = defaultTier ? (state.config.models?.[defaultTier] ?? defaultTier) : undefined; const defaultStrategy = defaultTier ? getStrategy(state.config.categoryStrategies, state.config.strategy, defaultTier) : strategy; const resolved = resolveModelWithFallback(ctx, { requestedTier: tier, requestedPattern: pattern, requestedStrategy: strategy, defaultTier, defaultPattern, defaultStrategy, reliabilityState: state.reliabilityStore.getState(), reliabilityConfig: state.config.reliability, }); const selectedKey = resolved.selected ? modelKey(resolved.selected) : undefined; const requestedCandidateLines = formatCandidateLines( resolved.primary, resolved.selectedTier === tier ? selectedKey : undefined, ); const fallbackCandidateLines = resolved.fallback ? formatCandidateLines( resolved.fallback, resolved.selectedTier && resolved.selectedTier !== tier ? selectedKey : undefined, ) : []; return { strategy, selected: selectedKey ?? "none", selectedModel: resolved.selected, selectedTier: resolved.selectedTier ?? "none", fallbackReason: resolved.fallbackReason, requestedCandidateLines, fallbackCandidateLines, defaultTier, }; } // ── Init proposal builder (exported for tests) ────────────── /** Default strategy per tier when generating init proposals. */ const PROPOSAL_STRATEGIES: Record = { quick: "first", general: "first", frontier: "first", economical: "cheapest", }; export function buildInitProposal( models: Record, classifierModel: string, extensionDir: string, discovery?: BifrostConfig["discovery"], ): Record { const tierKeys = Object.keys(models); // Pick the first populated tier as default, or fall back to first key. const defaultTier = tierKeys.length > 0 ? tierKeys[0] : "general"; const categoryStrategies: Record = {}; for (const t of tierKeys) { categoryStrategies[t] = PROPOSAL_STRATEGIES[t] ?? "first"; } return { $schema: `${extensionDir.replace(/\/$/, "")}/schema.json`, enabled: true, default: defaultTier, strategy: "first" as RoutingStrategy, categoryStrategies, classifier: { enabled: true, model: classifierModel, method: "auto" as const, }, models, rules: DEFAULT_RULES, ...(discovery ? { discovery } : {}), }; } // ── Discovery helpers ─────────────────────────────────────── function usesDiscovery(options: DiscoveryOptions): boolean { return options.scoped || options.free; } function discoverySourceLine(discovery: DiscoveryResult): string { const scoped = discovery.sourceModels.scoped?.length ?? 0; const free = discovery.sourceModels.free?.length ?? 0; return `scoped=${scoped}, free=${free}, deduplicated=${discovery.duplicateCount}`; } async function refreshAndDiscover( ctx: ExtensionContext, state: BifrostState, options: DiscoveryOptions, ): Promise { uiBusy(ctx, "Refreshing Pi model registry..."); try { await ctx.modelRegistry.refresh(); state.lastRegistryRefreshAt = Date.now(); state.forceRegistryRefresh = false; state.invalidatePipeline(); } catch (err) { log(ctx, `Model registry refresh failed; using current snapshot: ${String(err).slice(0, 200)}`, "warning"); } finally { uiDone(ctx); } const discovery = discoverModels(ctx, options); for (const message of discovery.messages) log(ctx, message, "warning"); log(ctx, `Discovery sources: ${discoverySourceLine(discovery)}.`); for (const item of discovery.skipped) log(ctx, `Skipped: ${item}.`, "warning"); return discovery; } function writeAndReloadConfig(config: BifrostConfig, state: BifrostState): void { const dir = join(process.cwd(), CONFIG_DIR_NAME); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "bifrost.json"), JSON.stringify(config, null, 2)); state.config = loadConfig(process.cwd(), state.extensionDir); const runtimeState = loadRuntimeState(runtimeStatePath(process.cwd()), { enabled: state.config.enabled ?? true, pinned: false, classifierEnabled: state.config.classifier?.enabled ?? true, thinkingMode: state.config.thinking?.mode ?? "off", silent: state.config.silent ?? false, }); state.enabled = runtimeState.enabled; state.pinned = runtimeState.pinned; state.classifierEnabled = runtimeState.classifierEnabled; state.thinkingMode = runtimeState.thinkingMode ?? "off"; state.silent = runtimeState.silent; state.reliabilityStore.reload(state.config.reliability, process.cwd()); state.invalidatePipeline(); } // ── Command handlers ──────────────────────────────────────── async function handleInit( args: string, ctx: ExtensionContext, state: BifrostState, ): Promise { clearBifrostWidgets(ctx); const discoveryOptions = parseDiscoveryOptions(args); const discoveryEnabled = usesDiscovery(discoveryOptions); const discovery = discoveryEnabled ? await refreshAndDiscover(ctx, state, discoveryOptions) : undefined; const selectedModels = discovery?.candidates; if (discoveryEnabled && selectedModels?.length === 0) { log(ctx, "Discovery returned no candidates; config not changed.", "error"); return; } // Keep legacy cached-probe behavior only when no discovery flags are used. const probePath = join(process.cwd(), ".pi", "bifrost-probe.json"); let workingModels: { provider: string; model: string; cost: { input: number; output: number }; duration_ms: number }[] = []; let probeLoaded = false; let probeAge = ""; if (!discoveryEnabled && existsSync(probePath)) { try { const probeData = JSON.parse(readFileSync(probePath, "utf-8")); const probeStat = statSync(probePath); const ageMs = Date.now() - probeStat.mtimeMs; const ageMin = Math.round(ageMs / 60000); if (ageMs < 3600_000) { workingModels = probeData .filter((r: any) => r.status === "ok") .map((r: any) => ({ provider: r.provider, model: r.model, cost: { input: r.cost_input ?? 0, output: r.cost_output ?? 0 }, duration_ms: r.duration_ms ?? 0, })); probeAge = `${ageMin}m ago`; probeLoaded = true; } } catch { // Corrupt — will re-probe below. } } const collectionRankingPromise = discoveryOptions.free ? fetchFreeModelRanking() : Promise.resolve(null); // If no fresh probe data, run probe inline. if (!probeLoaded) { const available = selectedModels ?? ctx.modelRegistry.getAvailable(); const availableCount = available.length; log(ctx, `Probing ${availableCount} discovered model(s) to find working ones...`); let okCount = 0; let errCount = 0; const lastModels: string[] = []; uiBusy(ctx, `Probing ${availableCount} models...`); const { results } = await runProbe(ctx, (done, total, last) => { if (last.status === "ok") okCount++; // models parameter passed below else if (last.status === "error" || last.status === "timeout") errCount++; lastModels.push(`${last.provider}/${last.model}: ${last.status} (${last.duration_ms}ms)`); if (lastModels.length > 5) lastModels.shift(); if (ctx.hasUI) { ctx.ui.setWidget("bifrost-probe", [ `Probing models: ${done}/${total}`, ` ok: ${okCount} errors: ${errCount}`, "", ...lastModels, ]); } }, available); uiDone(ctx); applyProbeOutcomes(state, results); workingModels = results .filter((r) => r.status === "ok") .map((r) => ({ provider: r.provider, model: r.model, cost: { input: r.cost_input ?? 0, output: r.cost_output ?? 0 }, duration_ms: r.duration_ms ?? 0, })); probeLoaded = true; probeAge = "just now"; const ok = results.filter((r) => r.status === "ok").length; const errors = results.filter((r) => r.status === "error").length; const timeouts = results.filter((r) => r.status === "timeout").length; const skipped = results.filter((r) => r.status === "skipped").length; log(ctx, `Probe complete: ok=${ok} error=${errors} timeout=${timeouts} skipped=${skipped}.`); if (ok === 0) { log(ctx, "No usable models found. Check API keys, network, and credits.", "error"); if (discoveryEnabled) { log(ctx, "Discovery init stopped; config not changed.", "warning"); return; } log(ctx, "Proceeding with full registry — most models will likely be unreachable.", "warning"); probeLoaded = false; } } if (probeLoaded && workingModels.length > 0) { log(ctx, `Using ${workingModels.length} probe-verified models (${probeAge}).`); } let available = selectedModels ?? ctx.modelRegistry.getAvailable(); const durationByKey = new Map(workingModels.map((w) => [`${w.provider}/${w.model}`, w.duration_ms])); const collectionRanking = await collectionRankingPromise; if (discoveryOptions.free && discovery?.sourceModels.free) { const originalFree = discovery.sourceModels.free; const originalFreeKeys = new Set(originalFree.map((m) => `${m.provider}/${m.id}`)); const verifiedKeys = new Set(workingModels.map((w) => `${w.provider}/${w.model}`)); discovery.sourceModels.free = capFreeModels( originalFree, (m) => `${m.provider}/${m.id}`, verifiedKeys, collectionRanking, durationByKey, ); const keptFreeKeys = new Set(discovery.sourceModels.free.map((m) => `${m.provider}/${m.id}`)); available = available.filter((m) => { const key = `${m.provider}/${m.id}`; return !originalFreeKeys.has(key) || keptFreeKeys.has(key); }); if (collectionRanking) { log(ctx, `Imported top ${discovery.sourceModels.free.length} free model(s) by collection ranking.`); } else { log(ctx, `Collection ranking unavailable; capped to ${FREE_MODEL_LIMIT} fastest free models.`, "warning"); } } const models: Record = {}; for (const m of available) { const key = `${m.provider}/${m.id}`; // If probe data is loaded, skip models that failed or timed out. if (probeLoaded && workingModels.length > 0) { const working = workingModels.find( (w) => w.provider === m.provider && w.model === m.id, ); if (!working) continue; // known-broken, silently skip } const tier = guessTier(m); models[tier] = models[tier] ?? []; models[tier].push(key); } // Order every tier: non-free by probe duration, then free by collection rank. const freeKeys = new Set((discovery?.sourceModels.free ?? []).map((m) => `${m.provider}/${m.id}`)); for (const tier of Object.keys(models)) { sortTierModels(models[tier], collectionRanking, freeKeys, durationByKey); } // Pick a classifier default: fastest cheap working model. let classifierModel: string | undefined; if (probeLoaded && workingModels.length > 0) { const cheapWorking = workingModels .filter((w) => (w.cost.input + w.cost.output) < 2) .sort((a, b) => a.duration_ms - b.duration_ms); if (cheapWorking.length > 0) { classifierModel = `${cheapWorking[0].provider}/${cheapWorking[0].model}`; } } if (!classifierModel) { // Fallback: any working model, or a sensible default. if (workingModels.length > 0) { classifierModel = `${workingModels[0].provider}/${workingModels[0].model}`; } else { classifierModel = "opencode/mimo-v2.5-free"; log(ctx, "No working models found for classifier. Using default — it may not work.", "warning"); } } const includedKeys = new Set(Object.values(models).flat()); const discoveryMetadata = discovery ? buildDiscoveryMetadata(discovery.sourceModels, includedKeys) : undefined; const proposal = buildInitProposal(models, classifierModel, state.extensionDir, discoveryMetadata); const totalAssigned = Object.values(models).reduce((s, v) => s + v.length, 0); const probeErrors = probeLoaded ? workingModels.length < available.length ? available .filter((m) => !workingModels.some((w) => w.provider === m.provider && w.model === m.id)) .map((m) => `${m.provider}/${m.id}`) : [] : []; const summaryLines: string[] = [ "--- init summary ---", `source: ${probeLoaded ? `probe (${workingModels.length} working)` : `registry (${available.length} listed)`}`, ...(discovery ? [`discovery: ${discoverySourceLine(discovery)}`] : []), "", ]; for (const [tier, tierModels] of Object.entries(models)) { summaryLines.push(`[${tier}] (${tierModels.length} model${tierModels.length === 1 ? "" : "s"})`); for (const key of tierModels) summaryLines.push(` ${key}`); } summaryLines.push("", `classifier: ${classifierModel}`); if (discovery?.skipped.length) { summaryLines.push("", "skipped (discovery):"); for (const item of discovery.skipped) summaryLines.push(` ${item}`); } if (probeErrors.length > 0) { summaryLines.push("", `errors (${probeErrors.length} model${probeErrors.length === 1 ? "" : "s"} failed probe):`); for (const key of probeErrors) summaryLines.push(` ${key}`); } if (!probeLoaded) summaryLines.push("", "warning: no probe data — run /bifrost probe first to filter unreachable models"); summaryLines.push("--------------------"); uiOutput(ctx, summaryLines); const writeWithoutPrompt = args.trim().split(/\s+/).includes("--write"); if (!ctx.hasUI && !writeWithoutPrompt) { log(ctx, "run in TUI or use --write to persist", "warning"); return; } const confirmBody = [ `${totalAssigned} model${totalAssigned === 1 ? "" : "s"} across ${Object.keys(models).length} tier${Object.keys(models).length === 1 ? "" : "s"}`, probeErrors.length > 0 ? `${probeErrors.length} model${probeErrors.length === 1 ? "" : "s"} failed probe` : "", ].filter(Boolean).join(". "); const ok = writeWithoutPrompt || await ctx.ui.confirm( "Write config to .pi/bifrost.json?", `${confirmBody}.`, ); if (!ok) { log(ctx, "config not written"); return; } writeAndReloadConfig(proposal as BifrostConfig, state); log(ctx, "wrote .pi/bifrost.json and reloaded config"); log(ctx, `Bifrost active with ${Object.keys(state.config.models ?? {}).length} tier(s). Try a prompt.`); // Clear the init widget so it doesn't persist in the TUI. if (ctx.hasUI) { ctx.ui.setWidget("bifrost-output", []); ctx.ui.setWidget("bifrost-probe", []); } } async function handleUpdate( args: string, ctx: ExtensionContext, state: BifrostState, ): Promise { clearBifrostWidgets(ctx); const requested = parseDiscoveryOptions(args); if (!usesDiscovery(requested)) { log(ctx, "usage: /bifrost update --scoped [--free] [--write]", "warning"); return; } const discovery = await refreshAndDiscover(ctx, state, requested); const selected: DiscoveryOptions = { scoped: requested.scoped, free: requested.free && !discovery.unavailableSources.includes("free"), }; if (!usesDiscovery(selected)) { log(ctx, "No requested discovery source is available; config not changed.", "error"); return; } const updateRankingPromise = selected.free ? fetchFreeModelRanking() : Promise.resolve(null); uiBusy(ctx, `Probing ${discovery.candidates.length} discovered model(s)...`); const { results } = await runProbe(ctx, undefined, discovery.candidates); uiDone(ctx); applyProbeOutcomes(state, results); const verifiedKeys = new Set( results.filter((result) => result.status === "ok").map((result) => `${result.provider}/${result.model}`), ); const durationByKey = new Map(results.map((r) => [`${r.provider}/${r.model}`, r.duration_ms])); const updateRanking = await updateRankingPromise; if (selected.free && discovery.sourceModels.free) { const originalFree = discovery.sourceModels.free; const originalFreeKeys = new Set(originalFree.map((m) => `${m.provider}/${m.id}`)); discovery.sourceModels.free = capFreeModels( originalFree, (m) => `${m.provider}/${m.id}`, verifiedKeys, updateRanking, durationByKey, ); const keptFreeKeys = new Set(discovery.sourceModels.free.map((m) => `${m.provider}/${m.id}`)); discovery.candidates = discovery.candidates.filter((m) => { const key = `${m.provider}/${m.id}`; return !originalFreeKeys.has(key) || keptFreeKeys.has(key); }); } const configPath = join(process.cwd(), CONFIG_DIR_NAME, "bifrost.json"); const current = readJson(configPath) ?? state.config; const diff = reconcileDiscoveredModels(current, discovery, selected, verifiedKeys); // Order every tier: non-free by probe duration, then free by collection rank. const freeKeys = new Set((discovery.sourceModels.free ?? []).map((m) => `${m.provider}/${m.id}`)); for (const tierModels of Object.values(diff.config.models ?? {})) { if (Array.isArray(tierModels)) sortTierModels(tierModels, updateRanking, freeKeys, durationByKey); } const probeSkipped = results .filter((result) => result.status !== "ok") .map((result) => `${result.provider}/${result.model} (${result.status}${result.error ? `: ${result.error}` : ""})`) .sort(); uiOutput(ctx, [ "--- update ---", `discovery: ${discoverySourceLine(discovery)}`, `probe: ${verifiedKeys.size} working, ${probeSkipped.length} skipped`, ...discovery.skipped.map((item) => `skipped discovery: ${item}`), ...probeSkipped.map((item) => `skipped probe: ${item}`), "add:", ...(diff.added.length > 0 ? diff.added.map((item) => ` + ${item.model} -> ${item.tier}`) : [" (none)"]), "remove:", ...(diff.removed.length > 0 ? diff.removed.map((item) => ` - ${item.model} <- ${item.tier}`) : [" (none)"]), "proposed config:", JSON.stringify(diff.config, null, 2), "--------------", ]); const writeWithoutPrompt = args.trim().split(/\s+/).includes("--write"); if (!ctx.hasUI && !writeWithoutPrompt) { log(ctx, "run in TUI or use --write to persist", "warning"); return; } const ok = writeWithoutPrompt || await ctx.ui.confirm( "Write config update?", `Add ${diff.added.length} and remove ${diff.removed.length} discovery-managed model(s)?`, ); if (!ok) { log(ctx, "config not written"); return; } writeAndReloadConfig(diff.config, state); log(ctx, `Updated .pi/bifrost.json: +${diff.added.length} -${diff.removed.length}.`); } async function handleBenchmark( args: string, ctx: ExtensionContext, state: BifrostState, ): Promise { const prompt = args.slice("benchmark".length).trim() || "Write a short Python function to reverse a string and explain it briefly."; const categories = Object.keys(state.config.models ?? {}); if (categories.length === 0) { log(ctx, "no categories configured; run /bifrost init first", "warning"); return; } clearBifrostWidgets(ctx); setBifrostStatus(ctx, "benchmarking prompt...", "accent"); uiBusy(ctx, "Classifying benchmark prompt..."); let classification; try { classification = await state.getPipeline(ctx).classify(prompt); } finally { uiDone(ctx); syncBifrostModeStatus(ctx, state); } const tier = classification.kind !== "unclassified" ? classification.tier : undefined; const source = classification.kind === "classified" ? classification.source : "fallback"; const lines = [ "--- benchmark ---", `prompt: ${prompt}`, `tier: ${tier ?? "none"}`, `source: ${source}`, "per-tier selection:", ]; for (const tierName of categories) { const display = resolveTierDisplay(tierName, state, ctx); lines.push(` ${tierName} (${display.strategy} → ${display.selectedTier}):`); if (display.fallbackReason) lines.push(` fallback: ${display.fallbackReason}`); lines.push(...display.requestedCandidateLines.map((line) => ` ${line}`)); if (display.fallbackCandidateLines.length > 0 && display.defaultTier && display.defaultTier !== tierName) { lines.push(` fallback candidates (${display.defaultTier}):`); lines.push(...display.fallbackCandidateLines.map((line) => ` ${line}`)); } } lines.push("-----------------"); await uiResult(ctx, "Bifrost benchmark", lines); } async function handleSync( args: string, ctx: ExtensionContext, state: BifrostState, ): Promise { clearBifrostWidgets(ctx); const dryRun = args.includes("--dry-run"); const githubPath = process.env.GITHUB_PATH ?? join(process.env.USERPROFILE ?? "", "Github"); const profileDir = join(githubPath, "pi-profile"); const scriptPath = join(profileDir, "scripts", "sync-bifrost.ps1"); if (!existsSync(scriptPath)) { log(ctx, `Sync script not found: ${scriptPath} (set GITHUB_PATH to override)`, "error"); return; } setBifrostStatus(ctx, "Syncing bifrost config to pi-profile...", "accent"); uiBusy(ctx, "Running sync-bifrost.ps1..."); const { stdout, stderr, exitCode } = await new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => { const child = spawn("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...(dryRun ? ["-DryRun"] : [])], { cwd: profileDir, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (d) => (stdout += d.toString())); child.stderr.on("data", (d) => (stderr += d.toString())); child.on("close", (code) => resolve({ stdout, stderr, exitCode: code ?? 0 })); }); uiDone(ctx); syncBifrostModeStatus(ctx, state); if (exitCode !== 0) { log(ctx, `Sync failed (exit ${exitCode}): ${stderr.slice(0, 500)}`, "error"); return; } if (stdout.trim()) { uiOutput(ctx, stdout.trim().split("\n")); } log(ctx, dryRun ? "Dry-run complete — no changes written" : "Sync complete"); } async function handlePreview( args: string, ctx: ExtensionContext, state: BifrostState, ): Promise { const prompt = args.slice("preview".length).trim(); if (!prompt) { log(ctx, "usage: /bifrost preview ", "warning"); return; } clearBifrostWidgets(ctx); setBifrostStatus(ctx, "previewing prompt...", "accent"); uiBusy(ctx, "Classifying preview prompt..."); let classification; try { classification = await state.getPipeline(ctx).classify(prompt); } finally { uiDone(ctx); syncBifrostModeStatus(ctx, state); } if (classification.kind === "unclassified") { log(ctx, "no tier matched", "warning"); return; } const tier = classification.tier; const source = classification.kind === "classified" ? classification.source : "fallback"; const display = resolveTierDisplay(tier, state, ctx); const thinking = state.previewThinking?.(prompt, display.selectedTier, display.selectedModel) ?? { level: state.thinkingLevel, mode: state.thinkingMode, summary: state.thinkingPinned ? "manual thinking level is pinned" : "thinking preview unavailable", }; const selection = display.selectedTier !== tier ? `${source} chose ${tier}; ${display.fallbackReason ?? "fallback"} selected ${display.selected} from ${display.selectedTier}` : `${source} chose ${tier}; ${display.strategy} selected ${display.selected}`; const lines = [ "--- preview ---", `prompt: ${prompt}`, `source: ${source}`, `tier: ${tier}`, `strategy: ${display.strategy}`, `selected tier: ${display.selectedTier}`, ...(display.fallbackReason ? [`fallback: ${display.fallbackReason}`] : []), `requested candidates (${tier}):`, ...display.requestedCandidateLines, ]; if (display.fallbackCandidateLines.length > 0 && display.defaultTier && display.defaultTier !== tier) { lines.push(`fallback candidates (${display.defaultTier}):`); lines.push(...display.fallbackCandidateLines); } lines.push(`selected: ${display.selected}`); lines.push(`thinking: ${thinking.level} (${thinking.mode})`); lines.push(`why model: ${selection}`); lines.push(`why thinking: ${thinking.summary}`); lines.push("---------------"); await uiResult(ctx, "Bifrost preview", lines); } // ── Command type ──────────────────────────────────────────── type CommandFn = (args: string, ctx: ExtensionContext) => void | Promise; interface CommandSpec { readonly value: string; readonly description: string; readonly argumentHint?: string; } interface CommandEntry extends CommandSpec { readonly match: (sub: string) => boolean; readonly handler: CommandFn; } function exact(word: string, description: string, handler: CommandFn): CommandEntry { return { value: word, description, match: (sub) => sub === word, handler }; } function prefix(word: string, description: string, handler: CommandFn, argumentHint = ""): CommandEntry { return { value: word, description, argumentHint, match: (sub) => sub.startsWith(word), handler }; } export const BIFROST_COMMAND_OPTIONS: readonly CommandSpec[] = [ { value: "on", description: "Enable routing" }, { value: "off", description: "Disable routing" }, { value: "pin", description: "Lock current model" }, { value: "unpin", description: "Resume routing" }, { value: "silence", description: "Hide Bifrost output" }, { value: "unsilence", description: "Show Bifrost output" }, { value: "reload", description: "Reload config after editing" }, { value: "providers", description: "List available providers" }, { value: "probe", description: "Probe models (optional --scoped and/or --free)" }, { value: "init", description: "Generate config (optional --scoped and/or --free)" }, { value: "update", description: "Reconcile discovery-managed models", argumentHint: "--scoped [--free]" }, { value: "sync", description: "Sync live bifrost.json to pi-profile repo", argumentHint: "[--dry-run]" }, { value: "benchmark", description: "Classify a benchmark prompt", argumentHint: "" }, { value: "cache stats", description: "Show classification cache" }, { value: "cache clear", description: "Clear classification cache" }, { value: "classifier on", description: "Enable LLM classifier" }, { value: "classifier off", description: "Disable LLM classifier" }, { value: "classifier status", description: "Show classifier state" }, { value: "thinking", description: "Show or set thinking mode", argumentHint: "[off|advisory|apply|status]" }, { value: "debug", description: "Show config and routing state" }, { value: "doctor", description: "Validate config against available models" }, { value: "preview", description: "Preview routing for a prompt", argumentHint: "" }, ] as const; export function getBifrostCommandCompletions(prefix: string) { const normalized = prefix.trim().toLowerCase(); const items = BIFROST_COMMAND_OPTIONS.filter((command) => command.value.startsWith(normalized)).map((command) => ({ value: command.value, label: command.value, description: command.description, })); return items.length > 0 ? items : null; } function formatBifrostCommandChoice(command: CommandSpec): string { const hint = command.argumentHint ? ` ${command.argumentHint}` : ""; return `/bifrost ${command.value}${hint} — ${command.description}`; } function dashboardCommands(state: Pick): CommandSpec[] { const values = [ state.enabled ? "off" : "on", state.pinned ? "unpin" : "pin", state.silent ? "unsilence" : "silence", "preview", "thinking", "providers", "probe", "init", "classifier status", "reload", ]; return values.map((value) => BIFROST_COMMAND_OPTIONS.find((command) => command.value === value)!); } async function pickBifrostCommand( ctx: ExtensionContext, title = "Bifrost commands", options: readonly CommandSpec[] = BIFROST_COMMAND_OPTIONS, ): Promise { if (!ctx.hasUI) return undefined; const selected = await ctx.ui.select(title, options.map(formatBifrostCommandChoice)); if (!selected) return undefined; return options.find((command) => formatBifrostCommandChoice(command) === selected); } // ── Route table ───────────────────────────────────────────── export function createCommandRouter( state: BifrostState, ): (args: string, ctx: ExtensionContext) => Promise { const routes: CommandEntry[] = [ exact("on", "Enable routing", (_, ctx) => { state.enabled = true; state.saveModeState(); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); log(ctx, "Bifrost enabled"); }), exact("off", "Disable routing", (_, ctx) => { state.enabled = false; state.saveModeState(); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); log(ctx, "Bifrost disabled"); }), exact("pin", "Lock current model", (_, ctx) => { state.pinned = true; state.saveModeState(); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); log(ctx, "Bifrost pinned"); }), exact("unpin", "Resume routing", (_, ctx) => { state.pinned = false; state.saveModeState(); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); log(ctx, "Bifrost unpinned"); }), exact("silence", "Hide Bifrost output", (_, ctx) => { state.silent = true; state.saveModeState(); setBifrostSilent(ctx, true); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); }), exact("unsilence", "Show Bifrost output", (_, ctx) => { state.silent = false; state.saveModeState(); setBifrostSilent(ctx, false); syncBifrostModeStatus(ctx, state); log(ctx, "Bifrost output enabled"); }), exact("reload", "Reload config after editing", (_, ctx) => { const done = debugMeasure("command", "reload"); state.config = loadConfig(process.cwd(), state.extensionDir); // Re-init debug — user may have updated debug config since startup. setupDebug(state.config.debug ?? { enabled: false }, process.cwd()); const runtimeState = loadRuntimeState(runtimeStatePath(process.cwd()), { enabled: state.config.enabled ?? true, pinned: false, classifierEnabled: state.config.classifier?.enabled ?? true, thinkingMode: state.config.thinking?.mode ?? "off", silent: state.config.silent ?? false, }); state.enabled = runtimeState.enabled; state.classifierEnabled = runtimeState.classifierEnabled; state.thinkingMode = runtimeState.thinkingMode ?? "off"; state.pinned = runtimeState.pinned; state.silent = runtimeState.silent; setBifrostSilent(ctx, state.silent); state.cacheEntries = loadCache(cachePath(process.cwd(), state.config.cache?.path)); state.reliabilityStore.reload(state.config.reliability, process.cwd()); state.invalidatePipeline(); syncBifrostModeStatus(ctx, state); clearBifrostWidgets(ctx); done(); debug("command", "reloaded", { enabled: state.enabled, classifierEnabled: state.classifierEnabled, tiers: Object.keys(state.config.models ?? {}).join(","), }); log(ctx, "Bifrost config reloaded"); }), // Providers exact("providers", "List available providers", (_, ctx) => { uiBusy(ctx, "Loading providers..."); const available = ctx.modelRegistry.getAvailable(); const counts = new Map(); for (const m of available) { counts.set(m.provider, (counts.get(m.provider) ?? 0) + 1); } uiDone(ctx); uiOutput(ctx, [ "available providers:", ...Array.from(counts.entries()).map( ([provider, count]) => ` ${provider}: ${count} model(s)`, ), ]); }), // Probe — test every model with a tiny prompt { value: "probe", description: "Probe models (optional --scoped and/or --free)", match: (sub) => sub === "probe" || sub.startsWith("probe "), handler: async (args, ctx) => { const options = parseDiscoveryOptions(args); const discovery = usesDiscovery(options) ? await refreshAndDiscover(ctx, state, options) : undefined; const available = discovery?.candidates ?? ctx.modelRegistry.getAvailable(); if (available.length === 0) { log(ctx, "No models available for selected discovery sources.", "warning"); return; } clearBifrostWidgets(ctx); uiBusy(ctx, `Probing ${available.length} models...`); log(ctx, `Probing ${available.length} model(s) with "${PROBE_PROMPT_TEXT}"...`); const { results, path } = await runProbe(ctx, undefined, discovery ? available : undefined); uiDone(ctx); applyProbeOutcomes(state, results); const ok = results.filter((r) => r.status === "ok"); const errs = results.filter((r) => r.status === "error"); const timeouts = results.filter((r) => r.status === "timeout"); const skipped = results.filter((r) => r.status === "skipped"); const lines = [ `--- probe results (${results.length} models) ---`, ` ok: ${ok.length}`, ` error: ${errs.length}`, ` timeout: ${timeouts.length}`, ` skipped: ${skipped.length}`, "", ]; if (errs.length > 0) { lines.push("errors:"); for (const e of errs.slice(0, 10)) { lines.push(` ${e.provider}/${e.model} — ${e.error}`); } if (errs.length > 10) lines.push(` ... and ${errs.length - 10} more`); } if (timeouts.length > 0) { lines.push("timeouts:"); for (const t of timeouts) { lines.push(` ${t.provider}/${t.model}`); } } lines.push("", `full results → ${path}`); uiOutput(ctx, lines); if (ok.length < results.length) { log( ctx, `${ok.length}/${results.length} models responded. Check ${path} for details.`, "warning", ); } else if (ok.length > 0) { log(ctx, `All ${ok.length} models responded successfully.`); } // Clear the probe widget so results don't persist in the TUI. if (ctx.hasUI) { ctx.ui.setWidget("bifrost-probe", []); ctx.ui.setWidget("bifrost-output", []); } }, }, // Init { value: "init", description: "Probe models and generate config", match: (sub) => sub === "init" || sub.startsWith("init "), handler: (args, ctx) => handleInit(args, ctx, state), }, { value: "update", description: "Reconcile discovery-managed models", match: (sub) => sub === "update" || sub.startsWith("update "), handler: (args, ctx) => handleUpdate(args, ctx, state), }, { value: "sync", description: "Sync live bifrost.json to pi-profile repo", match: (sub) => sub === "sync" || sub.startsWith("sync "), handler: (args, ctx) => handleSync(args, ctx, state), }, // Benchmark prefix("benchmark", "Classify a benchmark prompt", (args, ctx) => handleBenchmark(args, ctx, state), ""), // Cache exact("cache stats", "Show classification cache", (_, ctx) => { const path = cachePath(process.cwd(), state.config.cache?.path); const entries = loadCache(path); log( ctx, `cache: ${entries.length} entries (cap ${state.config.cache?.maxEntries ?? DEFAULT_MAX_ENTRIES}, threshold ${state.config.cache?.threshold ?? DEFAULT_THRESHOLD})`, ); }), exact("cache clear", "Clear classification cache", (_, ctx) => { const path = cachePath(process.cwd(), state.config.cache?.path); saveCache(path, []); state.cacheEntries = []; state.invalidatePipeline(); log(ctx, "cache cleared"); }), // Classifier exact("classifier on", "Enable LLM classifier", (_, ctx) => { state.classifierEnabled = true; state.saveModeState(); state.invalidatePipeline(); syncBifrostModeStatus(ctx, state); debug("command", "classifier_toggle", { enabled: true }); log(ctx, "LLM classifier enabled"); }), exact("classifier off", "Disable LLM classifier", (_, ctx) => { state.classifierEnabled = false; state.saveModeState(); state.invalidatePipeline(); syncBifrostModeStatus(ctx, state); debug("command", "classifier_toggle", { enabled: false }); log(ctx, "LLM classifier disabled; regex fallback active"); }), { value: "thinking", description: "Show or set thinking mode", match: (sub) => sub === "thinking" || sub.startsWith("thinking "), handler: (args, ctx) => { const mode = args.replace(/^thinking\b/i, "").trim().toLowerCase(); if (!mode || mode === "status") { const decision = state.lastThinkingDecision; log(ctx, `thinking: mode=${state.thinkingMode} level=${state.thinkingLevel} pinned=${state.thinkingPinned}${decision ? ` last=${decision.level} score=${decision.score} reasons=${decision.reasons.join(",")}` : ""}`, "info", true); return; } if (mode !== "off" && mode !== "advisory" && mode !== "apply") { log(ctx, "Usage: /bifrost thinking [off|advisory|apply|status]", "warning", true); return; } state.thinkingMode = mode as any; state.saveModeState(); syncBifrostModeStatus(ctx, state); log(ctx, `thinking mode set to ${mode}`, "info", true); }, }, exact("classifier status", "Show classifier state", (_, ctx) => { const rawModel = state.config.classifier?.model; const modelId = Array.isArray(rawModel) ? rawModel.join(", ") : (rawModel ?? "none"); log( ctx, `classifier: enabled=${state.classifierEnabled} model=${modelId} endpoint=${state.config.classifier?.endpoint ?? "registry"} method=${state.config.classifier?.method ?? "auto"}`, ); }), // Debug — show loaded config state exact("debug", "Show config and routing state", (_, ctx) => { const rules = state.config.rules ?? []; const tiers = Object.keys(state.config.models ?? {}); const lines = [ "--- config ---", `cwd: ${process.cwd()}`, `enabled: ${state.enabled}`, `pinned: ${state.pinned}`, `classifierEnabled: ${state.classifierEnabled}`, `silent: ${state.silent}`, `default: ${state.config.default}`, `strategy: ${state.config.strategy}`, `tiers: ${tiers.join(", ")}`, `debug: ${JSON.stringify(state.config.debug)}`, `cache: ${state.cacheEntries.length} entries`, `reliability: ${JSON.stringify(state.config.reliability ?? {})}`, `openCircuits: ${openCircuitCount(state)}`, "", `rules (${rules.length}):`, ...rules.map((r, i) => ` ${i}: "${r.pattern}" → "${r.model}"`), "---", ]; uiOutput(ctx, lines); log(ctx, "debug info printed above"); }), exact("doctor", "Validate config against available models", async (_, ctx) => { clearBifrostWidgets(ctx); uiBusy(ctx, "Running diagnostics..."); const diagnostics: BifrostDiagnostic[] = []; for (const [tier, patterns] of Object.entries(state.config.models ?? {})) { const { unresolved } = diagnoseCandidates(ctx, patterns); for (const p of unresolved) { diagnostics.push(patternUnresolvable(tier, p)); } } const classifierPattern = state.config.classifier?.model; if (classifierPattern && state.classifierEnabled) { const classified = diagnoseCandidates(ctx, classifierPattern); if (classified.candidates.length === 0) { const patternStr = Array.isArray(classifierPattern) ? classifierPattern[0] : classifierPattern; diagnostics.push(classifierModelMissing(patternStr)); } } uiDone(ctx); if (diagnostics.length === 0) { log(ctx, "All model patterns resolve correctly. No issues found."); return; } const lines = [ `--- doctor (${diagnostics.length} issue${diagnostics.length === 1 ? "" : "s"}) ---`, ...diagnostics.map(d => ` [${d.severity}] ${formatDiagnostic(d)}`), "---", ]; uiOutput(ctx, lines); }), prefix("preview", "Preview routing for a prompt", (args, ctx) => handlePreview(args, ctx, state), ""), ]; return async (args: string, ctx: ExtensionContext) => { setBifrostSilent(ctx, state.silent); const trimmed = args.trim(); const sub = trimmed.toLowerCase(); if (!trimmed) { debug("command", "dashboard"); if (!ctx.hasUI) { log( ctx, `Bifrost: enabled=${state.enabled} pinned=${state.pinned} current=${modelKey(ctx.model)}`, ); return; } const mode = !state.enabled ? "off" : state.pinned ? "pinned" : "on"; const selected = await pickBifrostCommand( ctx, `Bifrost · ${mode} · model ${modelKey(ctx.model)}${openCircuitCount(state) > 0 ? ` · circuits ${openCircuitCount(state)} open` : ""}`, dashboardCommands(state), ); if (!selected) return; if (selected.argumentHint) { ctx.ui.setEditorText(`/bifrost ${selected.value} `); log(ctx, `Prefilled /bifrost ${selected.value}. Usage: /bifrost ${selected.value} ${selected.argumentHint}`); return; } const route = routes.find((entry) => entry.value === selected.value); if (!route) return; await route.handler(route.value, ctx); return; } for (const route of routes) { if (route.match(sub)) { debug("command", "dispatch", { command: sub }); await route.handler(trimmed, ctx); return; } } debug("command", "picker", { command: sub }); if (!ctx.hasUI) { log(ctx, `Unknown /bifrost subcommand: ${trimmed}`, "warning", true); return; } const selected = await pickBifrostCommand(ctx); if (!selected) return; if (selected.argumentHint) { ctx.ui.setEditorText(`/bifrost ${selected.value} `); log(ctx, `Prefilled /bifrost ${selected.value}`); return; } const route = routes.find((entry) => entry.value === selected.value); if (!route) return; debug("command", "dispatch", { command: route.value }); await route.handler(route.value, ctx); }; }