feat: sync OpenRouter models on dashboard startup and add mission interview module

- Add openrouterModelSync setting to eagerly fetch OpenRouter model catalog at
  dashboard startup so the model picker shows all available models
- Add toggle in Settings → Models to disable the sync
- Add mission-interview session management module for AI-guided mission specs
- Include tests for the sync opt-out behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-04 00:39:17 -07:00
parent 38a095cc25
commit e46d357a6a
6 changed files with 586 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ function makeMockStore() {
maxWorktrees: 2,
autoMerge: false,
pollIntervalMs: 60_000,
openrouterModelSync: true,
}),
listTasks: vi.fn().mockResolvedValue([]),
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
@@ -104,7 +105,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
// ── Mock @mariozechner/pi-coding-agent ──────────────────────────────
const mockAuthStorage = { getAuth: vi.fn(), setAuth: vi.fn() };
const mockAuthStorage = { getAuth: vi.fn(), setAuth: vi.fn(), getApiKey: vi.fn() };
const mockModelRegistry = {
getModels: vi.fn().mockResolvedValue([]),
registerProvider: vi.fn(),
@@ -252,4 +253,22 @@ describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
expect(mockModelRegistry.refresh).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it("skips OpenRouter model sync when openrouterModelSync is false", async () => {
const { TaskStore } = await import("@fusion/core");
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
...makeMockStore(),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 1,
maxWorktrees: 2,
autoMerge: false,
pollIntervalMs: 60_000,
openrouterModelSync: false,
}),
}));
await runDashboard(0, {});
expect(mockAuthStorage.getApiKey).not.toHaveBeenCalled();
});
});

View File

@@ -504,6 +504,49 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
extensionsResult.runtime.pendingProviderRegistrations = [];
modelRegistry.refresh();
// Eagerly sync OpenRouter models — the pi-openrouter-realtime extension
// only registers providers on session_start (TUI-only event), so kick off
// a fetch here so the dashboard model list is populated. Respects the
// openrouterModelSync setting (defaults to true).
(async () => {
try {
const settings = await store.getSettings();
if (settings.openrouterModelSync === false) return;
const hasOrAuth = await authStorage.getApiKey("openrouter");
const headers: Record<string, string> = {};
if (hasOrAuth) headers["Authorization"] = `Bearer ${hasOrAuth}`;
const res = await fetch("https://openrouter.ai/api/v1/models", { headers });
if (!res.ok) return;
const json = await res.json() as { data?: Array<{ id: string; name: string; context_length?: number; top_provider?: { max_completion_tokens?: number }; pricing?: Record<string, string>; architecture?: { modality?: string; input_modalities?: string[] } }> };
const orModels = (json.data || []).map((m: any) => {
const id = (m.id || "").toLowerCase();
const name = (m.name || "").toLowerCase();
const reasoning = id.includes(":thinking") || id.includes("-r1") || id.includes("/r1") || id.includes("o1-") || id.includes("o3-") || id.includes("o4-") || id.includes("reasoner") || name.includes("thinking") || name.includes("reasoner");
const hasVision = m.architecture?.input_modalities?.includes("image") ?? m.architecture?.modality?.includes("multimodal") ?? false;
function parseCost(v?: string) { const n = parseFloat(v || "0"); return isNaN(n) ? 0 : n * 1_000_000; }
return {
id: m.id,
name: m.name || m.id,
reasoning,
input: (hasVision ? ["text", "image"] : ["text"]) as ("text" | "image")[],
cost: { input: parseCost(m.pricing?.prompt), output: parseCost(m.pricing?.completion), cacheRead: parseCost(m.pricing?.input_cache_read), cacheWrite: parseCost(m.pricing?.input_cache_write) },
contextWindow: m.context_length || 128000,
maxTokens: m.top_provider?.max_completion_tokens || 16384,
};
});
modelRegistry.registerProvider("openrouter", {
baseUrl: "https://openrouter.ai/api/v1",
apiKey: "OPENROUTER_API_KEY",
api: "openai-completions",
models: orModels,
});
console.log(`[openrouter] Synced ${orModels.length} models from OpenRouter API`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.log(`[openrouter] Failed to sync models: ${message}`);
}
})();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`[extensions] Failed to discover extensions: ${message}`);