UNPKG

@tehreet/conduit

Version:

LLM API gateway with intelligent routing, robust process management, and health monitoring

226 lines 9.17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClaudeWrapper = void 0; const events_1 = require("events"); const child_process_1 = require("child_process"); const context_extractor_1 = require("../utils/context-extractor"); const router_1 = require("../utils/router"); const log_1 = require("../utils/log"); const token_counter_1 = require("../utils/token-counter"); class ClaudeWrapper extends events_1.EventEmitter { constructor(options) { super(); this.options = options; } async wrapCommand(args, env = {}) { try { // Parse Claude command arguments const parsedArgs = this.parseClaudeArgs(args); // Extract context for routing decision const context = await this.extractContext(parsedArgs, env); // Make routing decision const routingDecision = await this.makeRoutingDecision(context, parsedArgs); // Log the routing decision if (this.options.loggingEnabled) { this.logRoutingDecision(context, routingDecision); } // Emit routing decision event this.emit('routing-decision', routingDecision); // Modify arguments with selected model const modifiedArgs = this.applyRoutingDecision(args, routingDecision); // Create metadata to inject const metadata = { routingDecision, timestamp: new Date().toISOString(), conduitVersion: '2.0.0', }; // Spawn the actual Claude process const claudeProcess = (0, child_process_1.spawn)(this.options.claudeBinaryPath, modifiedArgs, { env: { ...process.env, ...env, // Pass through routing metadata CONDUIT_METADATA: JSON.stringify(metadata), }, stdio: ['pipe', 'pipe', 'pipe'], }); // Inject metadata into stream if needed this.setupMetadataInjection(claudeProcess, metadata); // Track usage if enabled if (this.options.metricsEnabled) { this.trackUsage(routingDecision, context); } return claudeProcess; } catch (error) { (0, log_1.log)('Error in Claude wrapper:', error); // Fall back to direct Claude execution return (0, child_process_1.spawn)(this.options.claudeBinaryPath, args, { env }); } } parseClaudeArgs(args) { const parsed = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '-m': case '--model': parsed.model = args[++i]; break; case '-p': case '--prompt': parsed.systemPrompt = args[++i]; break; case '--message': parsed.messages = [{ role: 'user', content: args[++i] }]; break; case '--output-format': parsed.outputFormat = args[++i]; break; case '--thinking': parsed.thinking = true; break; default: if (!arg.startsWith('-')) { // Assume it's a message if not a flag if (!parsed.messages) { parsed.messages = [{ role: 'user', content: arg }]; } } } } return parsed; } async extractContext(parsedArgs, env) { const synapseContext = context_extractor_1.ContextExtractor.extractSynapseContext(env); // Calculate token count if we have message content let tokenCount = 0; if (parsedArgs.messages && parsedArgs.messages.length > 0) { const tokenResult = await token_counter_1.tokenCounter.countMessagesTokens(parsedArgs.messages, parsedArgs.model || 'claude-3-5-sonnet-20241022'); tokenCount = tokenResult.count; } return { synapseContext, tokenCount, requestedModel: parsedArgs.model, thinking: parsedArgs.thinking, env, }; } async makeRoutingDecision(context, parsedArgs) { // Try Synapse-based routing first if (context.synapseContext?.agentModelConfig || context.synapseContext?.projectModelConfig) { const synapseModel = await this.routeWithSynapseContext(context); if (synapseModel) { return { model: synapseModel, source: 'synapse-context', reason: 'Selected based on Synapse project/agent configuration', tokenCount: context.tokenCount, metadata: { synapseContext: context.synapseContext }, }; } } // Use standard Conduit routing const model = await (0, router_1.getUseModel)({ body: { messages: parsedArgs.messages, model: parsedArgs.model } }, context.tokenCount, this.options.config, context.env); return { model, source: 'default', reason: 'Selected by standard Conduit routing', tokenCount: context.tokenCount, }; } async routeWithSynapseContext(context) { const { synapseContext } = context; // Check agent config first (highest priority) if (synapseContext.agentModelConfig) { try { const agentConfig = JSON.parse(synapseContext.agentModelConfig); if (!agentConfig.useProjectDefaults && agentConfig.overrideModel) { // Map Synapse model format to Claude format return this.mapSynapseModelToClaude(agentConfig.overrideModel); } } catch (error) { (0, log_1.log)('Error parsing agent model config:', error); } } // Check project config if (synapseContext.projectModelConfig) { try { const projectConfig = JSON.parse(synapseContext.projectModelConfig); if (projectConfig.enabled && projectConfig.defaultModel) { return this.mapSynapseModelToClaude(projectConfig.defaultModel); } } catch (error) { (0, log_1.log)('Error parsing project model config:', error); } } return null; } mapSynapseModelToClaude(synapseModel) { // Remove 'anthropic/' prefix if present if (synapseModel.startsWith('anthropic/')) { return synapseModel.substring('anthropic/'.length); } return synapseModel; } applyRoutingDecision(args, decision) { const modifiedArgs = [...args]; // Find and replace model argument let modelIndex = -1; for (let i = 0; i < modifiedArgs.length; i++) { if (modifiedArgs[i] === '-m' || modifiedArgs[i] === '--model') { modelIndex = i + 1; break; } } if (modelIndex > 0 && modelIndex < modifiedArgs.length) { // Replace existing model modifiedArgs[modelIndex] = decision.model; } else { // Add model argument if not present modifiedArgs.unshift('--model', decision.model); } return modifiedArgs; } setupMetadataInjection(claudeProcess, metadata) { // For NDJSON output format, inject metadata as first line const isStreamJson = process.env.CLAUDE_OUTPUT_FORMAT === 'stream-json'; if (isStreamJson && claudeProcess.stdout) { const metadataLine = JSON.stringify({ type: 'conduit_metadata', data: metadata, }) + '\n'; // Inject metadata as first output claudeProcess.stdout.once('readable', () => { claudeProcess.stdout?.emit('data', Buffer.from(metadataLine)); }); } } logRoutingDecision(context, decision) { (0, log_1.log)('Routing decision:', { model: decision.model, source: decision.source, reason: decision.reason, tokenCount: decision.tokenCount, projectId: context.synapseContext?.projectId, agentId: context.synapseContext?.agentId, }); } async trackUsage(decision, context) { // Emit usage event for tracking this.emit('usage', { model: decision.model, tokenCount: decision.tokenCount, projectId: context.synapseContext?.projectId, agentId: context.synapseContext?.agentId, timestamp: new Date().toISOString(), }); } } exports.ClaudeWrapper = ClaudeWrapper; //# sourceMappingURL=claude-wrapper.js.map