UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

318 lines (317 loc) 12.3 kB
import { calculateCost, estimateTokens } from "../util/costs.js"; import { listProviders } from "../providers/index.js"; import { loadConfig } from "../state/config.js"; import { log } from "../util/logging.js"; const defaultConfig = { preferLocal: false, maxCostPerTask: 0.50, qualityThreshold: 0.7, enableHybrid: false, complexityThresholds: { simple: 500, moderate: 2000, complex: 8000 } }; // Analyze task complexity using heuristics export function analyzeTaskComplexity(taskDescription, contextSize = 0) { const task = taskDescription.toLowerCase(); const estimatedTokens = estimateTokens(taskDescription) + contextSize; // Keywords that suggest complexity levels const simpleKeywords = [ 'fix typo', 'update comment', 'rename', 'format', 'style', 'add import', 'remove', 'delete', 'clean up' ]; const moderateKeywords = [ 'implement', 'add function', 'create class', 'refactor', 'optimize', 'improve', 'enhance', 'modify logic' ]; const complexKeywords = [ 'architecture', 'system design', 'integrate', 'migrate', 'performance optimization', 'security', 'algorithm', 'complex logic', 'multi-step', 'full feature' ]; const expertKeywords = [ 'machine learning', 'ai model', 'distributed system', 'microservices', 'database design', 'scalability', 'concurrent', 'async', 'real-time', 'blockchain' ]; // Score based on keywords let complexityScore = 0; let matchedKeywords = []; for (const keyword of expertKeywords) { if (task.includes(keyword)) { complexityScore += 4; matchedKeywords.push(keyword); } } for (const keyword of complexKeywords) { if (task.includes(keyword)) { complexityScore += 3; matchedKeywords.push(keyword); } } for (const keyword of moderateKeywords) { if (task.includes(keyword)) { complexityScore += 2; matchedKeywords.push(keyword); } } for (const keyword of simpleKeywords) { if (task.includes(keyword)) { complexityScore += 1; matchedKeywords.push(keyword); } } // Factor in task length and context if (taskDescription.length > 200) complexityScore += 1; if (contextSize > 5000) complexityScore += 2; if (contextSize > 20000) complexityScore += 3; // Determine complexity level let level; let reasoning = ""; let suggestedProvider = "openai"; let suggestedModel = "gpt-4o-mini"; let confidenceScore = 0.8; if (complexityScore >= 7) { level = "expert"; reasoning = `Expert-level task (score: ${complexityScore}). Requires advanced reasoning`; suggestedProvider = "anthropic"; suggestedModel = "claude-3-5-sonnet"; confidenceScore = 0.9; } else if (complexityScore >= 4) { level = "complex"; reasoning = `Complex task (score: ${complexityScore}). Needs strong reasoning capabilities`; suggestedProvider = "openai"; suggestedModel = "gpt-4o"; confidenceScore = 0.85; } else if (complexityScore >= 2) { level = "moderate"; reasoning = `Moderate complexity (score: ${complexityScore}). Standard capabilities sufficient`; suggestedProvider = "openai"; suggestedModel = "gpt-4o-mini"; confidenceScore = 0.8; } else { level = "simple"; reasoning = `Simple task (score: ${complexityScore}). Can use efficient models`; // Prefer local for simple tasks if available suggestedProvider = "ollama"; suggestedModel = "llama3.1:8b"; confidenceScore = 0.75; } if (matchedKeywords.length > 0) { reasoning += `. Keywords: ${matchedKeywords.slice(0, 3).join(", ")}`; } return { level, reasoning, estimatedTokens, suggestedProvider, suggestedModel, confidenceScore }; } // Route task to optimal model based on complexity and cost export async function routeTask(taskDescription, contextSize = 0, userPreferences) { const config = await loadConfig(); const routerConfig = { ...defaultConfig, ...userPreferences }; // Analyze task complexity const complexity = analyzeTaskComplexity(taskDescription, contextSize); // Get available providers and models const providers = listProviders(); const candidates = []; // Evaluate each provider/model combination for (const provider of providers) { try { const models = await provider.listModels(); for (const modelInfo of models) { if (modelInfo.type !== 'chat') continue; const estimatedCost = await calculateCost(provider.id, modelInfo.id, complexity.estimatedTokens, complexity.estimatedTokens * 0.8 // estimated output ); // Skip if over budget if (estimatedCost > routerConfig.maxCostPerTask) continue; // Calculate quality score based on model capabilities let qualityScore = 0.5; // baseline if (provider.id === "anthropic" && modelInfo.id.includes("claude-3-5")) { qualityScore = 0.95; // highest quality } else if (provider.id === "openai" && modelInfo.id.includes("gpt-4o")) { qualityScore = 0.9; } else if (provider.id === "openai" && modelInfo.id.includes("gpt-4")) { qualityScore = 0.85; } else if (provider.id === "xai" && modelInfo.id.includes("grok")) { qualityScore = 0.8; } else if (provider.id === "google" && modelInfo.id.includes("gemini")) { qualityScore = 0.75; } else if (provider.id === "ollama") { // Local models - quality varies but they're free if (modelInfo.id.includes("70b")) qualityScore = 0.7; else if (modelInfo.id.includes("8b")) qualityScore = 0.6; else qualityScore = 0.5; } else { qualityScore = 0.6; // other providers } // Boost score for local models if preferred const isLocal = provider.id === "ollama"; if (isLocal && routerConfig.preferLocal) { qualityScore += 0.1; } // Boost score for fine-tuned models (they're trained on user's patterns) if (modelInfo.isFineTuned) { qualityScore += 0.2; } candidates.push({ provider: provider.id, model: modelInfo.id, estimatedCost, qualityScore, localModel: isLocal }); } } catch (error) { log.warn(`Failed to evaluate provider ${provider.id}:`, error); } } if (candidates.length === 0) { throw new Error("No suitable models available within budget"); } // Score candidates based on cost, quality, and complexity requirements const scoredCandidates = candidates.map(candidate => { let score = 0; // Quality component (weighted by quality threshold) score += candidate.qualityScore * routerConfig.qualityThreshold; // Cost component (inverse - lower cost is better) const maxCost = Math.max(...candidates.map(c => c.estimatedCost)); const costScore = maxCost > 0 ? 1 - (candidate.estimatedCost / maxCost) : 1; score += costScore * (1 - routerConfig.qualityThreshold); // Complexity matching bonus if (complexity.level === "expert" && candidate.qualityScore >= 0.9) score += 0.2; if (complexity.level === "complex" && candidate.qualityScore >= 0.8) score += 0.15; if (complexity.level === "simple" && candidate.localModel) score += 0.15; // Local preference bonus if (candidate.localModel && routerConfig.preferLocal) score += 0.1; return { ...candidate, score }; }); // Sort by score (highest first) scoredCandidates.sort((a, b) => b.score - a.score); const best = scoredCandidates[0]; const alternatives = scoredCandidates.slice(1, 4).map(candidate => ({ provider: candidate.provider, model: candidate.model, estimatedCost: candidate.estimatedCost, tradeoff: candidate.localModel ? "Local model - free but may have lower quality" : candidate.estimatedCost < best.estimatedCost ? "Cheaper option with potentially lower quality" : "Higher quality but more expensive" })); const reasoning = `Selected ${best.provider}/${best.model} for ${complexity.level} task. ` + `Cost: $${best.estimatedCost.toFixed(4)}, Quality: ${(best.qualityScore * 100).toFixed(0)}%, ` + `Score: ${best.score.toFixed(2)}`; return { provider: best.provider, model: best.model, reasoning, estimatedCost: best.estimatedCost, complexity, alternatives }; } // Get routing recommendations without executing export async function getRoutingRecommendations(taskDescription, contextSize = 0) { const complexity = analyzeTaskComplexity(taskDescription, contextSize); const routing = await routeTask(taskDescription, contextSize); const recommendations = [ { provider: routing.provider, model: routing.model, estimatedCost: routing.estimatedCost, reasoning: "Optimal choice based on complexity and cost analysis", pros: [ "Best balance of quality and cost", "Matches task complexity requirements", complexity.level === "simple" && routing.provider === "ollama" ? "Free local execution" : "Proven reliability" ], cons: [ routing.provider === "ollama" ? "Requires local setup" : "Uses API credits", routing.estimatedCost > 0.1 ? "Higher cost for complex task" : "May be overkill for simple tasks" ] }, ...routing.alternatives.map(alt => ({ provider: alt.provider, model: alt.model, estimatedCost: alt.estimatedCost, reasoning: alt.tradeoff, pros: [ alt.estimatedCost === 0 ? "Free local execution" : "Lower cost option", "Available as fallback" ], cons: [ alt.provider === "ollama" ? "May need model download" : "Potentially lower quality", "Not the optimal choice" ] })) ].slice(0, 3); // Limit to top 3 return { complexity, recommendations }; } // Load router configuration from main config export async function loadMetaRouterConfig() { try { const config = await loadConfig(); const routing = config?.metaRouter || {}; return { ...defaultConfig, ...routing }; } catch (error) { log.warn("Failed to load meta-router config, using defaults"); return defaultConfig; } } // Save router configuration export async function saveMetaRouterConfig(routerConfig) { try { const config = await loadConfig(); if (!config) throw new Error("No base config found"); const updatedConfig = { ...config, metaRouter: { ...defaultConfig, ...config.metaRouter, ...routerConfig } }; const { saveConfig } = await import("../state/config.js"); await saveConfig(updatedConfig); } catch (error) { log.error("Failed to save meta-router config:", error); throw error; } }