UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

341 lines (340 loc) 9.07 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { writeFileSecure, ensureSecureDir } from "../../hooks/secure-fs.js"; import { ModelRouterConfigSchema, parseConfigSafe } from "../../hooks/schemas.js"; const CONFIG_PATH = join(homedir(), ".stackmemory", "model-router.json"); const DEFAULT_CONFIG = { enabled: false, defaultProvider: "anthropic", taskRouting: {}, fallback: { enabled: true, // Fallback enabled by default provider: "qwen", onRateLimit: true, onError: true, onTimeout: true, maxRetries: 2, retryDelayMs: 1e3 }, providers: { anthropic: { provider: "anthropic", model: "claude-sonnet-4-20250514", apiKeyEnv: "ANTHROPIC_API_KEY" }, qwen: { provider: "qwen", model: "qwen3-max-2025-01-23", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", apiKeyEnv: "DASHSCOPE_API_KEY", params: { enable_thinking: true, thinking_budget: 1e4 } } }, thinkingMode: { enabled: true, budget: 1e4, temperature: 0.6, topP: 0.95 } }; function loadModelRouterConfig() { try { if (existsSync(CONFIG_PATH)) { const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); return parseConfigSafe( ModelRouterConfigSchema, { ...DEFAULT_CONFIG, ...data }, DEFAULT_CONFIG, "model-router" ); } } catch { } return { ...DEFAULT_CONFIG }; } function saveModelRouterConfig(config) { try { ensureSecureDir(join(homedir(), ".stackmemory")); writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2)); } catch { } } function getModelForTask(taskType) { const config = loadModelRouterConfig(); if (!config.enabled) { return null; } const routedProvider = config.taskRouting[taskType]; const provider = routedProvider || config.defaultProvider; return config.providers[provider] || null; } function buildModelEnv(modelConfig) { const env = {}; const apiKey = process.env[modelConfig.apiKeyEnv]; if (!apiKey) { console.warn(`[model-router] API key not found: ${modelConfig.apiKeyEnv}`); return env; } env["ANTHROPIC_MODEL"] = modelConfig.model; env["ANTHROPIC_SMALL_FAST_MODEL"] = modelConfig.model; env["ANTHROPIC_AUTH_TOKEN"] = apiKey; if (modelConfig.baseUrl) { env["ANTHROPIC_BASE_URL"] = modelConfig.baseUrl; } return env; } function isPlanningContext(input) { const planPatterns = [ /\bplan\b/i, /\barchitect/i, /\bdesign\b/i, /\bstrateg/i, /\bimplement.*approach/i, /\bhow.*should.*we/i, /\bthink.*through/i, /\breason.*about/i, /\banalyze.*options/i, /\btrade-?offs?/i ]; return planPatterns.some((pattern) => pattern.test(input)); } function requiresDeepThinking(input) { const thinkPatterns = [ /\bcomplex/i, /\bdifficult/i, /\btricky/i, /\bcareful/i, /\bstep.*by.*step/i, /\bthink.*hard/i, /\bultrathink/i, /\b--think/i, /\b--think-hard/i ]; return thinkPatterns.some((pattern) => pattern.test(input)); } class ModelRouter { config; currentProvider; inFallbackMode = false; fallbackReason; constructor() { this.config = loadModelRouterConfig(); this.currentProvider = this.config.defaultProvider; } /** * Route a task to the appropriate model */ route(taskType, input) { if (!this.config.enabled) { return { provider: "anthropic", env: {}, switched: false }; } let detectedType = taskType; if (input && taskType === "default") { if (isPlanningContext(input)) { detectedType = "plan"; } else if (requiresDeepThinking(input)) { detectedType = "think"; } } const modelConfig = getModelForTask(detectedType); if (!modelConfig) { return { provider: "anthropic", env: {}, switched: false }; } const switched = modelConfig.provider !== this.currentProvider; this.currentProvider = modelConfig.provider; return { provider: modelConfig.provider, env: buildModelEnv(modelConfig), switched }; } /** * Get current provider */ getCurrentProvider() { return this.currentProvider; } /** * Force switch to a specific provider */ switchTo(provider) { const modelConfig = this.config.providers[provider]; if (!modelConfig) { console.warn(`[model-router] Provider not configured: ${provider}`); return {}; } this.currentProvider = provider; return buildModelEnv(modelConfig); } /** * Reset to default provider */ reset() { this.currentProvider = this.config.defaultProvider; this.inFallbackMode = false; this.fallbackReason = void 0; } /** * Check if fallback is enabled and configured */ isFallbackEnabled() { if (!this.config.fallback?.enabled) return false; const fallbackProvider = this.config.providers[this.config.fallback.provider]; if (!fallbackProvider) return false; const apiKey = process.env[fallbackProvider.apiKeyEnv]; return !!apiKey; } /** * Check if error should trigger fallback */ shouldFallback(error) { if (!this.isFallbackEnabled()) return false; if (this.inFallbackMode) return false; const fallback = this.config.fallback; if (fallback.onRateLimit && error.status === 429) { return true; } if (fallback.onError && error.status && error.status >= 500) { return true; } if (fallback.onTimeout) { const isTimeout = error.code === "ETIMEDOUT" || error.code === "ESOCKETTIMEDOUT" || error.message?.toLowerCase().includes("timeout"); if (isTimeout) return true; } if (error.message?.toLowerCase().includes("overloaded")) { return true; } return false; } /** * Activate fallback mode */ activateFallback(reason) { if (!this.isFallbackEnabled()) { console.warn("[model-router] Fallback not available"); return {}; } const fallbackProvider = this.config.fallback.provider; const modelConfig = this.config.providers[fallbackProvider]; if (!modelConfig) { console.warn( `[model-router] Fallback provider not configured: ${fallbackProvider}` ); return {}; } this.inFallbackMode = true; this.fallbackReason = reason; this.currentProvider = fallbackProvider; console.log( `[model-router] Fallback activated: ${reason} -> ${fallbackProvider}` ); return buildModelEnv(modelConfig); } /** * Get fallback configuration */ getFallbackConfig() { return this.config.fallback; } /** * Check if currently in fallback mode */ isInFallbackMode() { return this.inFallbackMode; } /** * Get reason for fallback */ getFallbackReason() { return this.fallbackReason; } /** * Get fallback environment variables (for pre-configuring) */ getFallbackEnv() { if (!this.isFallbackEnabled()) return {}; const fallbackProvider = this.config.fallback.provider; const modelConfig = this.config.providers[fallbackProvider]; if (!modelConfig) return {}; return buildModelEnv(modelConfig); } } let routerInstance = null; function getModelRouter() { if (!routerInstance) { routerInstance = new ModelRouter(); } return routerInstance; } function getPlanModeEnv() { const router = getModelRouter(); const result = router.route("plan"); return result.env; } function getThinkingModeEnv() { const router = getModelRouter(); const result = router.route("think"); return result.env; } function isFallbackAvailable() { const router = getModelRouter(); return router.isFallbackEnabled(); } function getFallbackStatus() { const router = getModelRouter(); const config = loadModelRouterConfig(); if (!config.fallback?.enabled) { return { enabled: false, provider: null, hasApiKey: false, inFallback: false }; } const fallbackProvider = config.providers[config.fallback.provider]; const hasApiKey = fallbackProvider ? !!process.env[fallbackProvider.apiKeyEnv] : false; return { enabled: true, provider: config.fallback.provider, hasApiKey, inFallback: router.isInFallbackMode(), reason: router.getFallbackReason() }; } function triggerFallback(reason = "manual") { const router = getModelRouter(); return router.activateFallback(reason); } function resetFallback() { const router = getModelRouter(); router.reset(); } export { ModelRouter, buildModelEnv, getFallbackStatus, getModelForTask, getModelRouter, getPlanModeEnv, getThinkingModeEnv, isFallbackAvailable, isPlanningContext, loadModelRouterConfig, requiresDeepThinking, resetFallback, saveModelRouterConfig, triggerFallback }; //# sourceMappingURL=model-router.js.map