@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
JavaScript
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