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
JavaScript
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;
}
}