termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
321 lines (304 loc) • 12.5 kB
JavaScript
import { getProvider } from "../providers/index.js";
import { loadConfig } from "../state/config.js";
import { estimateTokens } from "../util/costs.js";
import { log } from "../util/logging.js";
const defaultHybridConfig = {
enabled: false,
localProvider: "ollama",
localModel: "llama3.1:8b",
cloudProvider: "openai",
cloudModel: "gpt-4o-mini",
maxLocalTokens: 8000,
compressionRatio: 0.3, // compress to 30% of original
summarizationThreshold: 10 // summarize if more than 10 chunks
};
// Check if local model is available
export async function checkLocalAvailability(provider) {
try {
const providerInstance = getProvider(provider);
const healthCheck = await providerInstance.healthCheck();
return healthCheck.status === "healthy";
}
catch (error) {
return false;
}
}
// Summarize context using local model
async function summarizeWithLocal(chunks, task, config) {
const localProvider = getProvider(config.localProvider);
// Combine chunks into groups for summarization
const chunkGroups = [];
const maxChunksPerGroup = 5;
for (let i = 0; i < chunks.length; i += maxChunksPerGroup) {
const group = chunks.slice(i, i + maxChunksPerGroup);
const combinedText = group.map(chunk => `File: ${chunk.file} (lines ${chunk.start}-${chunk.end})\n${chunk.text}`).join("\n\n");
chunkGroups.push(combinedText);
}
const summaries = [];
for (const groupText of chunkGroups) {
const summarizePrompt = `Summarize this code context for the task "${task}". Focus on relevant details, key functions, patterns, and dependencies. Be concise but preserve important technical details.
Context:
${groupText}
Summary:`;
try {
const summary = await localProvider.chat([
{ role: "user", content: summarizePrompt }
], {
model: config.localModel,
temperature: 0.1
});
summaries.push(summary);
log.step("Local summarization", `Compressed ${estimateTokens(groupText)} → ${estimateTokens(summary)} tokens`);
}
catch (error) {
log.warn("Local summarization failed for chunk group:", error);
// Fallback: use original text (truncated)
summaries.push(groupText.substring(0, 1000) + "...");
}
}
return summaries.join("\n\n");
}
// Enhanced context distillation using local model
async function distillContext(chunks, task, config) {
const localProvider = getProvider(config.localProvider);
// Create a context distillation prompt
const contextText = chunks.slice(0, 20).map(chunk => // Limit to prevent overwhelm
`${chunk.file}:${chunk.start}-${chunk.end}: ${chunk.text.substring(0, 500)}`).join("\n\n");
const distillPrompt = `You are a code context analyzer. Your job is to extract and synthesize the most relevant information from this codebase context for the given task.
Task: ${task}
Context from codebase:
${contextText}
Please provide a distilled analysis focusing on:
1. Relevant files and their purposes
2. Key functions/classes/patterns that relate to the task
3. Dependencies and imports that matter
4. Existing code patterns to follow
5. Potential conflicts or considerations
Keep your response concise but technically precise:`;
try {
const distilled = await localProvider.chat([
{ role: "user", content: distillPrompt }
], {
model: config.localModel,
temperature: 0.2
});
const originalTokens = estimateTokens(contextText);
const distilledTokens = estimateTokens(distilled);
const compressionRatio = distilledTokens / originalTokens;
log.step("Context distillation", `${originalTokens} → ${distilledTokens} tokens (${(compressionRatio * 100).toFixed(1)}% compression)`);
return distilled;
}
catch (error) {
log.warn("Context distillation failed:", error);
// Fallback to basic summarization
return await summarizeWithLocal(chunks, task, config);
}
}
// Process task using hybrid local + cloud approach
export async function processHybrid(task, repoPath, chunks = [], userConfig) {
const startTime = Date.now();
const config = { ...defaultHybridConfig, ...userConfig };
if (!config.enabled) {
// Fall back to cloud-only processing
return processCloudOnly(task, repoPath, chunks, config);
}
// Check if local model is available
const localAvailable = await checkLocalAvailability(config.localProvider);
if (!localAvailable) {
log.warn("Local model not available, falling back to cloud-only");
return processCloudOnly(task, repoPath, chunks, config);
}
// Estimate total context size
const contextText = chunks.map(c => c.text).join("\n");
const totalTokens = estimateTokens(contextText + task);
// If context is small enough for local processing, do it locally
if (totalTokens <= config.maxLocalTokens && chunks.length < config.summarizationThreshold) {
log.info("Context small enough for local-only processing");
return processLocalOnly(task, repoPath, chunks, config);
}
log.info(`Processing hybrid: ${chunks.length} chunks, ${totalTokens} tokens`);
// Use local model to distill context
const distilledContext = await distillContext(chunks, task, config);
const compressedTokens = estimateTokens(distilledContext);
const tokensCompressed = totalTokens - compressedTokens;
// Send distilled context + task to cloud model
const cloudProvider = getProvider(config.cloudProvider);
const hybridPrompt = `You are an expert software developer. You have been provided with a distilled analysis of a codebase and need to complete a specific task.
CODEBASE ANALYSIS:
${distilledContext}
TASK:
${task}
Please provide a complete solution that:
1. Follows the existing code patterns shown in the analysis
2. Integrates properly with the existing codebase
3. Includes proper error handling and documentation
4. Uses the identified dependencies and imports appropriately
Your response should be production-ready code with clear explanations of any changes made.`;
try {
const finalResponse = await cloudProvider.chat([
{ role: "user", content: hybridPrompt }
], {
model: config.cloudModel,
temperature: 0.2
});
// Calculate cost savings (approximate)
const { calculateCost } = await import("../util/costs.js");
const cloudCostFull = await calculateCost(config.cloudProvider, config.cloudModel, totalTokens, estimateTokens(finalResponse));
const cloudCostHybrid = await calculateCost(config.cloudProvider, config.cloudModel, compressedTokens, estimateTokens(finalResponse));
const costSaved = cloudCostFull - cloudCostHybrid;
const processingTime = Date.now() - startTime;
log.success(`Hybrid processing complete: ${(costSaved * 100).toFixed(2)}¢ saved, ${tokensCompressed} tokens compressed`);
return {
method: "hybrid",
localSummary: distilledContext.substring(0, 200) + "...",
finalResponse,
costSaved,
tokensCompressed,
processingTime
};
}
catch (error) {
log.error("Hybrid processing failed:", error);
// Fallback to cloud-only with truncated context
return processCloudOnly(task, repoPath, chunks.slice(0, 10), config);
}
}
// Process entirely with local model
async function processLocalOnly(task, repoPath, chunks, config) {
const startTime = Date.now();
const localProvider = getProvider(config.localProvider);
const contextText = chunks.map(chunk => `${chunk.file}:${chunk.start}: ${chunk.text}`).join("\n\n");
const prompt = `You are an expert software developer working on a coding task.
Context from codebase:
${contextText}
Task: ${task}
Please provide a complete solution:`;
try {
const response = await localProvider.chat([
{ role: "user", content: prompt }
], {
model: config.localModel,
temperature: 0.2
});
const processingTime = Date.now() - startTime;
return {
method: "local-only",
finalResponse: response,
costSaved: 0, // Local is free
tokensCompressed: 0,
processingTime
};
}
catch (error) {
log.error("Local processing failed:", error);
throw error;
}
}
// Process entirely with cloud model (fallback)
async function processCloudOnly(task, repoPath, chunks, config) {
const startTime = Date.now();
const cloudProvider = getProvider(config.cloudProvider);
// Limit context to prevent exceeding limits
const limitedChunks = chunks.slice(0, 15);
const contextText = limitedChunks.map(chunk => `${chunk.file}:${chunk.start}: ${chunk.text.substring(0, 800)}`).join("\n\n");
const prompt = `You are an expert software developer working on a coding task.
Context from codebase:
${contextText}
Task: ${task}
Please provide a complete solution:`;
try {
const response = await cloudProvider.chat([
{ role: "user", content: prompt }
], {
model: config.cloudModel,
temperature: 0.2
});
const processingTime = Date.now() - startTime;
return {
method: "cloud-only",
finalResponse: response,
costSaved: 0,
tokensCompressed: 0,
processingTime
};
}
catch (error) {
log.error("Cloud processing failed:", error);
throw error;
}
}
// Get hybrid processing recommendations
export async function getHybridRecommendations(task, contextSize) {
const config = defaultHybridConfig;
const localAvailable = await checkLocalAvailability(config.localProvider);
if (!localAvailable) {
return {
recommended: false,
reasoning: "Local model (Ollama) is not available. Install and run 'ollama serve' to enable hybrid processing.",
estimatedSavings: 0,
requirements: [
"Install Ollama (https://ollama.ai)",
"Download a model: ollama pull llama3.1:8b",
"Start server: ollama serve"
]
};
}
if (contextSize < config.maxLocalTokens) {
return {
recommended: false,
reasoning: "Context is small enough for efficient cloud processing. Hybrid mode provides minimal benefit for simple tasks.",
estimatedSavings: 0,
requirements: []
};
}
// Estimate potential savings
const { calculateCost } = await import("../util/costs.js");
const fullCloudCost = await calculateCost("openai", "gpt-4o-mini", contextSize, contextSize * 0.5);
const hybridCost = await calculateCost("openai", "gpt-4o-mini", contextSize * 0.3, contextSize * 0.5);
const estimatedSavings = fullCloudCost - hybridCost;
return {
recommended: true,
reasoning: `Large context (${contextSize} tokens) can benefit from local summarization. Estimated ${((1 - hybridCost / fullCloudCost) * 100).toFixed(0)}% cost reduction.`,
estimatedSavings,
requirements: [
"Ollama running locally",
"llama3.1:8b model downloaded"
]
};
}
// Load hybrid configuration
export async function loadHybridConfig() {
try {
const config = await loadConfig();
const hybrid = config?.hybrid || {};
return {
...defaultHybridConfig,
...hybrid
};
}
catch (error) {
return defaultHybridConfig;
}
}
// Save hybrid configuration
export async function saveHybridConfig(hybridConfig) {
try {
const config = await loadConfig();
if (!config)
throw new Error("No base config found");
const updatedConfig = {
...config,
hybrid: {
...defaultHybridConfig,
...config.hybrid,
...hybridConfig
}
};
const { saveConfig } = await import("../state/config.js");
await saveConfig(updatedConfig);
}
catch (error) {
log.error("Failed to save hybrid config:", error);
throw error;
}
}