@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
226 lines • 9.17 kB
JavaScript
;
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