import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { RefreshModelsContext } from "@earendil-works/pi-ai"; import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { loadSettings, resolveApiKeys, type ApiKey } from "./settings.js"; import { fetchPricing } from "./fetch.js"; import { fetchModelsDev } from "./fetch.js"; import { computeModels, type ComputedModels } from "./computeModels.js"; import { agentrouterConfigCommand } from "./command.js"; import { agentrouterMigrateCommand } from "./migrate.js"; function readAuthJson(): Record | null { const authPath = join(homedir(), ".pi", "agent", "auth.json"); if (!existsSync(authPath)) return null; try { const raw = readFileSync(authPath, "utf-8"); const parsed = JSON.parse(raw); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null; return parsed as Record; } catch { return null; } } interface AuthEntry { type: string; key: string; } function resolveAuthKeys( settings: { provider_prefix?: string }, debug = false ): Array<{ id: string; key: string }> { const authData = readAuthJson(); if (!authData) return []; const prefix = settings.provider_prefix ?? "agentrouter"; const matches = Object.entries(authData) .filter(([name]) => name === prefix || name.startsWith(`${prefix}_`)) .filter(([, entry]) => entry.type === "api_key" && typeof entry.key === "string" && entry.key.length > 0) .map(([name, entry]) => ({ id: name, key: entry.key })); if (matches.length > 0 && debug) { console.log("[agentrouter] auth.json entries:", matches.map((m) => m.id).join(", ")); } return matches; } export default async function (pi: ExtensionAPI) { const settings = loadSettings(process.cwd()); if (settings.debug) { console.log("[agentrouter] settings loaded:", JSON.stringify(settings, null, 2)); } const apiKeys = resolveApiKeys(settings); if (settings.debug) { console.log("[agentrouter] api keys:", JSON.stringify(apiKeys, null, 2)); } // ── auth.json discovery (hard cutover) ────────────────────────────────── const authKeys = resolveAuthKeys(settings, settings.debug); const usingAuthJson = authKeys.length > 0; if (usingAuthJson) { console.warn( "[agentrouter] api_keys in settings.json is deprecated. Using credentials from auth.json instead." ); } // auth.json wins if it has matching entries; otherwise fall back to settings.json const keys = usingAuthJson ? authKeys : apiKeys; // If no API keys are configured, register a command to configure them if (keys.length === 0) { pi.registerCommand("agentrouter-config", { description: "Configure Agent Router settings", handler: async (args, ctx) => { await agentrouterConfigCommand(args, ctx); }, }); return; } // ── Fetch initial models at startup ───────────────────────────────────── let initialModels: ComputedModels = { anthropic: [], openai: [] }; try { const pricing = await fetchPricing(); const modelsDev = await fetchModelsDev(); initialModels = computeModels(pricing, modelsDev, settings); } catch (err) { console.error("[agentrouter] initial model fetch failed:", err); } // ── Register providers ────────────────────────────────────────────────── // auth.json entries have meaningful names (e.g. "agentrouter_work"), // so we use the entry id directly as the provider name. // Fallback keys from settings.json keep the existing single/multi naming. const singleKey = !usingAuthJson && keys.length === 1; for (const { id, key } of keys) { const baseName = usingAuthJson ? id : singleKey ? "agentrouter" : `agentrouter_${id}`; const displayName = usingAuthJson ? id : singleKey ? "Agent Router" : `Agent Router (${id})`; if (settings.debug) { console.log(`[agentrouter] registering providers ${baseName} and ${baseName}_openai`); } // ── Anthropic provider ────────────────────────────────────────────── const anthropicProviderName = baseName; const anthropicConfig = { name: displayName, baseUrl: settings.api_base, api: "anthropic-messages" as const, models: initialModels.anthropic, authHeader: true, apiKey: key, }; if (settings.debug) { console.log(`[agentrouter] provider config for ${anthropicProviderName}:`, JSON.stringify(anthropicConfig, null, 2)); } pi.registerProvider(anthropicProviderName, { ...anthropicConfig, async refreshModels(_context: RefreshModelsContext) { try { const pricing = await fetchPricing(); const modelsDev = await fetchModelsDev(); return computeModels(pricing, modelsDev, settings).anthropic; } catch (err) { console.error(`[agentrouter] refreshModels failed for ${anthropicProviderName}:`, err); return []; } }, }); // ── OpenAI provider ──────────────────────────────────────────────── const openaiProviderName = `${baseName}_openai`; const openaiConfig = { name: `${displayName} (OpenAI)`, baseUrl: `${settings.api_base}/v1`, api: "openai-completions" as const, models: initialModels.openai, authHeader: true, apiKey: key, }; if (settings.debug) { console.log(`[agentrouter] provider config for ${openaiProviderName}:`, JSON.stringify(openaiConfig, null, 2)); } pi.registerProvider(openaiProviderName, { ...openaiConfig, async refreshModels(_context: RefreshModelsContext) { try { const pricing = await fetchPricing(); const modelsDev = await fetchModelsDev(); return computeModels(pricing, modelsDev, settings).openai; } catch (err) { console.error(`[agentrouter] refreshModels failed for ${openaiProviderName}:`, err); return []; } }, }); } // Register the config command pi.registerCommand("agentrouter-config", { description: "Configure Agent Router settings", handler: async (args, ctx) => { await agentrouterConfigCommand(args, ctx); }, }); // Register migration command only when settings.json has api_keys to migrate const hasSettingsKeys = apiKeys.length > 0; if (hasSettingsKeys) { pi.registerCommand("agentrouter-migrate", { description: "Migrate api_keys from settings.json to auth.json", handler: async (args, ctx) => { await agentrouterMigrateCommand(args, ctx); }, }); } }