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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user