/** * pi-router v0.4.0 * Intelligent routing layer for pi coding agent * * Routes channels (same model, different providers) with optional fallback models. * Real model identity end-to-end — zero protocol coupling with pi-cache-optimizer. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { Context, SimpleStreamOptions, AssistantMessageEvent, AssistantMessageEventStream } from "@earendil-works/pi-ai"; import { type RouterCustomRouteConfig, type RouterRouteConfig } from "./router-routes.js"; type PiModel = { id: string; name: string; provider: string; api: string; baseUrl?: string; headers?: Record; compat?: Record; reasoning?: boolean; thinkingLevelMap?: Record; contextWindow?: number; maxTokens?: number; cost?: { input: number; output: number; cacheRead: number; cacheWrite: number; }; input?: string[]; deprecated?: boolean; status?: string; state?: string; lifecycle?: string; }; type PiRouteSnapshot = { virtualProvider: string; virtualModelId: string; provider: string; modelId: string; api?: string; canonicalModelId?: string; routeLabel?: string; status?: "planned" | "trying" | "selected" | "success" | "failed"; sessionIdHash?: string; requestId?: string; timestamp: number; }; type RouterConfig = { strategy?: "channelFirst" | "custom"; auto?: boolean; sortBy?: "manual" | "capabilityFirst" | "costFirst" | "latency" | "cost"; request?: { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs?: number; maxTokens?: number; }; models?: RouterModelConfig[]; /** * Legacy/global alias map kept for migration compatibility. Prefer per-model * `models[].aliases` so each canonical router model owns its upstream IDs. */ modelAliases?: Record; customOrder?: string[]; customRoutes?: RouterCustomRouteConfig[]; failover?: { on?: string[]; cooldownMs?: number; }; sticky?: boolean; stickyRecords?: Record; intent?: "suggest" | "auto" | "off"; logDir?: string | null; autoSync?: boolean; lastSyncHash?: string; contextTransfer?: "none" | "summary" | "full"; summaryModel?: string; summaryPrompt?: string; summaryMaxTokens?: number; healthProbe?: { enabled?: boolean; intervalMs?: number; timeoutMs?: number; probeMessage?: string; }; footer?: { /** * Default true. When the replacement footer is disabled, keep route status * on pi's built-in extension status line. Set false to suppress that * fallback status item. */ statusLine?: boolean; /** * Default true. pi-router replaces pi's built-in footer while router * status is active so the route can be right-aligned with other extension * statuses. Set false to keep pi's built-in footer layout. */ rightAlignRoute?: boolean; }; }; type StickyRecord = { modelId: string; channel: string; /** Distinguishes same-provider routes with different upstream model names. */ routeKey?: string; /** Exact upstream model id for this route when it differs from modelId. */ upstreamModelId?: string; successCount: number; lastSuccess: number; lastUpdate: number; }; type RouterModelConfig = { id: string; channels: string[]; /** Optional route list; needed when the same provider appears with multiple upstream model IDs. */ routes?: RouterRouteConfig[]; sortBy?: "config" | "latency" | "cost"; failover?: { on?: string[]; cooldownMs?: number; }; fallbackModels?: Array<{ id: string; channels: string[]; /** Optional route list for duplicate provider/model-variant fallback routes. */ routes?: RouterRouteConfig[]; /** Upstream model IDs considered equivalent to this fallback id. */ aliases?: string[]; /** Per-channel upstream model id when it differs from the canonical id. */ modelByChannel?: Record; }>; /** Upstream model IDs considered equivalent to this canonical router id. */ aliases?: string[]; /** Per-channel upstream model id when it differs from the canonical id. */ modelByChannel?: Record; fallbackMode?: "switch" | "inline"; sticky?: boolean; contextTransfer?: "none" | "summary" | "full"; }; /** * Context transfer strategies */ type ContextTransferStrategy = "none" | "summary" | "full"; /** * Get effective pricing for a specific model@channel */ declare function getChannelPricing(modelId: string, channel: string): { input: number; output: number; cacheRead: number; cacheWrite: number; } | null; /** * Calculate cost for a request (estimated) */ declare function estimateRequestCost(modelId: string, channel: string, inputTokens: number, outputTokens: number, cacheReadTokens?: number, cacheWriteTokens?: number): number; /** * Diff types for models.json changes */ type ModelDiff = { added: Array<{ id: string; channels: string[]; aliases?: string[]; modelByChannel?: Record; routes?: RouterRouteConfig[]; }>; removed: Array<{ id: string; channels: string[]; }>; modified: Array<{ id: string; channelsAdded: string[]; channelsRemoved: string[]; propsChanged: string[]; }>; }; /** * Expand provider models from models.json or modelOverrides with builtin fallback * FIX #7: Better error handling for getBuiltinPiAiModels cast * FIX #8: Warn user when modelOverrides entry has no builtin model * FIX #13: Respect explicit models:[] to disable provider (no builtin fallback) */ declare function expandProviderModels(providerName: string, provider: any): PiModel[]; declare function modelsFromRegistry(modelRegistry: any): PiModel[] | undefined; declare function filterConfigurableModels(models: PiModel[], allowedProviders: Set): PiModel[]; declare function buildModelMap(models: PiModel[]): Map; /** * Group models by id (collect all channels for same model) */ declare function groupModelsByChannels(models: PiModel[]): Map; /** * Detect changes between current models.json and config */ declare function detectModelChanges(config: RouterConfig, currentModels: PiModel[]): ModelDiff; declare function updateFooterStatus(modelId: string, channel: string, actualModelId?: string, phase?: RouterStatusPhase, attemptedChannels?: string[], error?: string, api?: string): void; declare function formatFooterStatus(theme: any, status: NonNullable): string; declare function formatRightAlignedStatusLine(theme: any, width: number, leftStatus: string, rightStatus: string): string | undefined; /** * Main extension export * * Performance optimization: Lazy load models.json only when needed * Auto-sync check is enabled by default and deferred to first use or background (30s after startup). * It only checks local models.json/auth.json changes; health probes are controlled by healthProbe.enabled. */ export default function (pi: ExtensionAPI): void; /** * Generate simple text-based summary (no AI required) * * This is the ultimate fallback when no AI model is available. * Extracts key information from the conversation without using AI. */ declare function estimateContextTokens(context: any): number; declare function shouldSummarizeForTarget(context: any, targetModel: PiModel): boolean; declare function generateSimpleTextSummary(messages: any[], fromModel: PiModel, toModel: PiModel): string; /** * Sanitize context for model switch * * @param context - Original context * @param fromModel - Source model * @param toModel - Target model * @param transferStrategy - How to transfer context * @param summary - Generated summary (if strategy is 'summary') */ declare function sanitizeContextForSwitch(context: any, fromModel: PiModel, toModel: PiModel, transferStrategy: ContextTransferStrategy, summary?: string): any; /** * Router state for tracking active channels and cooldowns */ type RouterStatusPhase = "trying" | "success" | "failed" | "fallback" | "aborted"; type RouterState = { activeChannels: Map; cooldowns: Map; lastFailures: Map; currentUi?: any; currentTheme?: any; currentModel?: any; currentModelProvider?: string; currentThinkingLevel?: string; currentSessionManager?: any; currentSessionHash?: string; currentRouterModelId?: string; currentGetContextUsage?: (() => { tokens: number | null; contextWindow: number; percent: number | null; } | undefined); currentModelRegistry?: any; customFooterInstalled?: boolean; customFooterEnabled?: boolean; footerStatusLineEnabled?: boolean; lastStatusUpdate?: { modelId: string; channel: string; actualModelId?: string; api?: string; phase: RouterStatusPhase; attemptedChannels?: string[]; error?: string; timestamp: number; }; activeRouteSnapshots: Map; routeListeners: Set<(event: PiRouteSnapshot) => void>; unregisterRoutingAdapter?: () => void; }; declare function createMirrorModels(configuredModels: RouterModelConfig[], modelMap: Map): any[]; declare function determineChannelOrder(modelId: string, modelConfig: RouterModelConfig, config: RouterConfig): string[]; declare function getStreamEventFailure(event: AssistantMessageEvent): string | undefined; declare function isAbortError(error: string): boolean; declare function applyRouterRequestOptions(options: SimpleStreamOptions | undefined, config?: RouterConfig): SimpleStreamOptions | undefined; /** * Create a failover stream that tries channels in order */ declare function createFailoverStream(modelId: string, channelOrder: string[], context: Context, options: SimpleStreamOptions | undefined, config: RouterConfig, modelConfig: RouterModelConfig, modelMap: Map): AssistantMessageEventStream; /** * Record a channel failure and apply cooldown */ declare function recordFailure(modelId: string, channel: string, error: string, config: RouterConfig, modelConfig: RouterModelConfig): void; /** * Resolve the model used for AI summary generation. * * config.summaryModel supports either: * - model-id * - model-id@provider * * When unset or not found, the target model is used. */ declare function resolveSummaryModel(configuredSummaryModel: string | undefined, modelMap: Map, targetModel: PiModel): PiModel; /** * Sort channels by cost (lower cost first) */ declare function sortChannelsByCost(modelId: string, channels: string[]): string[]; /** * Latency tracking for channel performance */ type LatencyRecord = { channel: string; latencyMs: number; timestamp: number; }; /** * Record latency for a channel */ declare function recordLatency(modelId: string, channel: string, latencyMs: number): void; /** * Get average latency for a channel */ declare function getAverageLatency(modelId: string, channel: string): number | null; /** * Sort channels by latency (lower is better) */ declare function sortChannelsByLatency(modelId: string, channels: string[]): string[]; /** * Health check system for periodic channel monitoring */ type HealthCheckStatus = { channel: string; healthy: boolean; lastCheck: number; consecutiveFailures: number; }; /** * Mark channel health status */ declare function updateHealthStatus(modelId: string, channel: string, healthy: boolean): void; /** * Circuit breaker for fast-fail on consistently failing channels */ type CircuitState = "closed" | "open" | "half-open"; type CircuitBreakerStatus = { state: CircuitState; failureCount: number; lastFailureTime: number; nextRetryTime: number; }; /** * Check if circuit breaker allows request */ declare function canAttemptChannel(modelId: string, channel: string): boolean; /** * Record circuit breaker outcome */ declare function recordCircuitOutcome(modelId: string, channel: string, success: boolean): void; /** * Decision logger for observability */ type RoutingDecision = { timestamp: number; modelId: string; selectedChannel: string; attemptedChannels: string[]; sortStrategy: string; latencyMs?: number; fallbackUsed: boolean; fallbackModel?: string; reason: string; estimatedCost?: number; }; declare function __testResetInternalState(): void; declare function __testSetPiConfigDir(configDir: string | null): void; declare function __testLoadModelsJson(): PiModel[]; declare function __testLoadConfig(): RouterConfig; declare function __testSaveConfig(config: RouterConfig): void; declare function __testRefreshConfigFromDisk(): RouterConfig; declare function __testGetCachedModelMap(modelRegistry?: any): Map; declare function __testGetConfigurableModels(modelRegistry?: any, forceRefresh?: boolean): PiModel[]; declare function __testGetSyncModels(): PiModel[]; declare function __testRegisterRoutingAdapter(): void; declare function __testStartHealthProbes(config: RouterConfig): void; declare function __testGetHealthProbeTimerKeys(): string[]; declare function __testCalculateFileHash(filePath: string): string; declare function __testGetInternalState(): { activeChannels: Map; cooldowns: Map; failures: Map; latencies: Map; health: Map; circuits: Map; lastStatusUpdate: { modelId: string; channel: string; actualModelId?: string; api?: string; phase: RouterStatusPhase; attemptedChannels?: string[]; error?: string; timestamp: number; }; decisions: RoutingDecision[]; }; export { getChannelPricing, estimateRequestCost, groupModelsByChannels, detectModelChanges, estimateContextTokens, shouldSummarizeForTarget, generateSimpleTextSummary, sanitizeContextForSwitch, recordFailure, resolveSummaryModel, recordLatency, getAverageLatency, sortChannelsByLatency, sortChannelsByCost, updateHealthStatus, canAttemptChannel, recordCircuitOutcome, updateFooterStatus, formatFooterStatus, formatRightAlignedStatusLine, applyRouterRequestOptions, getStreamEventFailure, isAbortError, createMirrorModels, createFailoverStream, determineChannelOrder, expandProviderModels, modelsFromRegistry, filterConfigurableModels, buildModelMap, __testResetInternalState, __testGetInternalState, __testSetPiConfigDir, __testLoadModelsJson, __testLoadConfig, __testSaveConfig, __testRefreshConfigFromDisk, __testGetCachedModelMap, __testGetConfigurableModels, __testGetSyncModels, __testRegisterRoutingAdapter, __testStartHealthProbes, __testGetHealthProbeTimerKeys, __testCalculateFileHash, }; //# sourceMappingURL=index.d.ts.map