import { EnhancedDuckProvider } from './duck-provider-enhanced.js'; import { ProviderManager } from './manager.js'; import { ConfigManager } from '../config/config.js'; import { FunctionBridge } from '../services/function-bridge.js'; import { UsageService } from '../services/usage.js'; import { GuardrailsService } from '../guardrails/service.js'; import { DuckResponse, MessageContent } from '../config/types.js'; import { ChatOptions, MCPResult } from './types.js'; import { logger } from '../utils/logger.js'; export class EnhancedProviderManager extends ProviderManager { private enhancedProviders: Map = new Map(); private functionBridge?: FunctionBridge; private mcpEnabled: boolean = false; constructor( configManager: ConfigManager, functionBridge?: FunctionBridge, usageService?: UsageService, guardrailsService?: GuardrailsService ) { super(configManager, usageService, guardrailsService); this.functionBridge = functionBridge; this.mcpEnabled = !!functionBridge && (configManager.getConfig().mcp_bridge?.enabled || false); if (this.mcpEnabled) { this.initializeEnhancedProviders(); } } private initializeEnhancedProviders() { if (!this.functionBridge) { logger.warn('Function bridge not available, skipping enhanced providers'); return; } const config = this.configManager.getConfig(); const allProviders = config.providers; const maxToolRounds = config.mcp_bridge?.max_tool_rounds ?? 10; for (const [name, providerConfig] of Object.entries(allProviders)) { try { // Skip CLI providers - they don't support MCP bridge // CLI tools like claude/gemini have their own native tool systems // that conflict with MCP tool injection if (providerConfig.type === 'cli') { logger.debug( `Skipping CLI provider ${name} for MCP bridge (CLI tools have native tool systems)` ); continue; } // Create enhanced HTTP provider with MCP support const enhancedProvider = new EnhancedDuckProvider( name, providerConfig.nickname, { apiKey: providerConfig.api_key, baseURL: providerConfig.base_url, model: providerConfig.default_model, availableModels: providerConfig.models, temperature: providerConfig.temperature, timeout: providerConfig.timeout, maxRetries: providerConfig.max_retries, systemPrompt: providerConfig.system_prompt, }, this.functionBridge, this.mcpEnabled, this.guardrailsService, maxToolRounds ); this.enhancedProviders.set(name, enhancedProvider); logger.info( `Initialized enhanced provider: ${name} (${providerConfig.nickname}) with MCP support` ); } catch (error) { logger.error(`Failed to initialize enhanced provider ${name}:`, error); } } } getEnhancedProvider(name?: string): EnhancedDuckProvider { if (!this.mcpEnabled) { throw new Error('MCP bridge is not enabled'); } const providerName = name || this.configManager.getConfig().default_provider; if (!providerName) { throw new Error('No provider specified and no default provider configured'); } const provider = this.enhancedProviders.get(providerName); if (!provider) { throw new Error(`Enhanced duck "${providerName}" not found in the pond`); } return provider; } async askDuckWithMCP( providerName: string | undefined, prompt: MessageContent, options?: Partial ): Promise< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > { if (!this.mcpEnabled) { // Fall back to regular provider return this.askDuck(providerName, prompt, options); } // Resolve provider name const resolvedName = providerName || this.configManager.getConfig().default_provider; // Check if this is a CLI provider (not in enhanced providers map) // CLI providers don't support MCP bridge, fall back to regular askDuck if (resolvedName && !this.enhancedProviders.has(resolvedName)) { logger.debug( `Provider ${resolvedName} is not MCP-enhanced (likely CLI), using regular askDuck` ); return this.askDuck(providerName, prompt, options); } const provider = this.getEnhancedProvider(providerName); const startTime = Date.now(); const modelToUse = options?.model || provider.getInfo().model; try { const response = await provider.chat({ messages: [{ role: 'user', content: prompt, timestamp: new Date() }], ...options, }); // Record usage if (this.usageService && response.usage) { this.usageService.recordUsage( provider.name, response.model, response.usage.promptTokens, response.usage.completionTokens, false ); } return { provider: provider.name, nickname: provider.nickname, model: response.model, content: response.content, usage: response.usage ? { prompt_tokens: response.usage.promptTokens, completion_tokens: response.usage.completionTokens, total_tokens: response.usage.totalTokens, promptTokens: response.usage.promptTokens, completionTokens: response.usage.completionTokens, totalTokens: response.usage.totalTokens, } : undefined, latency: Date.now() - startTime, pendingApprovals: response.pendingApprovals, mcpResults: response.mcpResults, toolRoundsUsed: response.toolRoundsUsed, }; } catch (error: unknown) { // Record error if (this.usageService) { this.usageService.recordUsage(provider.name, modelToUse, 0, 0, true); } // Try failover if enabled if (this.configManager.getConfig().enable_failover && providerName === undefined) { const errorMessage = error instanceof Error ? error.message : String(error); logger.warn(`Primary enhanced provider failed, attempting failover: ${errorMessage}`); return this.askDuckWithMCPFailover(prompt, options, provider.name); } throw error; } } private async askDuckWithMCPFailover( prompt: MessageContent, options: Partial | undefined, failedProvider: string ): Promise< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > { const availableProviders = Array.from(this.enhancedProviders.keys()).filter( (name) => name !== failedProvider ); for (const providerName of availableProviders) { try { logger.info(`Trying enhanced failover to ${providerName}...`); return await this.askDuckWithMCP(providerName, prompt, options); } catch (error) { logger.warn(`Enhanced failover to ${providerName} failed:`, error); continue; } } throw new Error('All enhanced ducks have flown away! No providers available.'); } async compareDucksWithMCP( prompt: MessageContent, providerNames?: string[], options?: Partial ): Promise< Array< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > > { if (!this.mcpEnabled) { // Fall back to regular comparison return this.compareDucks(prompt, providerNames, options); } // Use ALL providers (including CLI), not just enhanced ones const allProviderNames = providerNames || this.getProviderNames(); if (allProviderNames.length === 0) { throw new Error('No valid providers specified'); } // askDuckWithMCP already handles CLI providers by falling back to regular askDuck const promises = allProviderNames.map((name) => { return this.askDuckWithMCP(name, prompt, options).catch((error) => { const provider = this.enhancedProviders.get(name); const baseProvider = this.getProvider(name); return { provider: name, nickname: provider?.nickname || baseProvider?.nickname || 'Unknown', model: '', content: `Error: ${error instanceof Error ? error.message : String(error)}`, latency: 0, }; }); }); return Promise.all(promises); } async compareDucksWithProgressMCP( prompt: MessageContent, providerNames: string[] | undefined, options: Partial | undefined, onProviderComplete: (providerName: string, completed: number, total: number) => void ): Promise< Array< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > > { if (!this.mcpEnabled) { return this.compareDucksWithProgress(prompt, providerNames, options, onProviderComplete); } // Use ALL providers (including CLI), not just enhanced ones const allProviderNames = providerNames || this.getProviderNames(); if (allProviderNames.length === 0) { throw new Error('No valid providers specified'); } const total = allProviderNames.length; let completed = 0; // askDuckWithMCP already handles CLI providers by falling back to regular askDuck const promises = allProviderNames.map((name) => { return this.askDuckWithMCP(name, prompt, options) .catch((error) => { const provider = this.enhancedProviders.get(name); const baseProvider = this.getProvider(name); return { provider: name, nickname: provider?.nickname || baseProvider?.nickname || 'Unknown', model: '', content: `Error: ${error instanceof Error ? error.message : String(error)}`, latency: 0, }; }) .then((result) => { completed++; onProviderComplete(name, completed, total); return result; }); }); return Promise.all(promises); } async duckCouncilWithMCP( prompt: MessageContent, options?: Partial ): Promise< Array< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > > { return this.compareDucksWithMCP(prompt, undefined, options); } // Method to retry with approval async retryWithApproval( approvalId: string, providerName: string | undefined, prompt: MessageContent, options?: Partial ): Promise< DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[]; toolRoundsUsed?: number; } > { if (!this.mcpEnabled) { throw new Error('MCP bridge is not enabled'); } const provider = this.getEnhancedProvider(providerName); const startTime = Date.now(); const modelToUse = options?.model || provider.getInfo().model; try { const response = await provider.retryWithApproval( approvalId, [{ role: 'user', content: prompt, timestamp: new Date() }], { messages: [{ role: 'user', content: prompt, timestamp: new Date() }], ...options, } ); // Record usage if (this.usageService && response.usage) { this.usageService.recordUsage( provider.name, response.model, response.usage.promptTokens, response.usage.completionTokens, false ); } return { provider: provider.name, nickname: provider.nickname, model: response.model, content: response.content, usage: response.usage ? { prompt_tokens: response.usage.promptTokens, completion_tokens: response.usage.completionTokens, total_tokens: response.usage.totalTokens, promptTokens: response.usage.promptTokens, completionTokens: response.usage.completionTokens, totalTokens: response.usage.totalTokens, } : undefined, latency: Date.now() - startTime, pendingApprovals: response.pendingApprovals, mcpResults: response.mcpResults, toolRoundsUsed: response.toolRoundsUsed, }; } catch (error: unknown) { // Record error if (this.usageService) { this.usageService.recordUsage(provider.name, modelToUse, 0, 0, true); } const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to retry with approval: ${errorMessage}`); } } // Get enhanced provider statistics getAllEnhancedProviders(): Array<{ name: string; info: ReturnType; mcpEnabled: boolean; mcpStats?: ReturnType; functionCount?: number; }> { return Array.from(this.enhancedProviders.entries()).map(([name, provider]) => ({ name, info: provider.getInfo(), mcpEnabled: provider.isMCPEnabled(), mcpStats: provider.getMCPStats(), functionCount: 0, // Will be populated when functions are loaded })); } // Check if MCP is enabled isMCPEnabled(): boolean { return this.mcpEnabled; } // Enable/disable MCP for all providers setMCPEnabled(enabled: boolean): void { this.mcpEnabled = enabled; for (const provider of this.enhancedProviders.values()) { provider.setMCPEnabled(enabled); } logger.info(`MCP ${enabled ? 'enabled' : 'disabled'} for all providers`); } // Get MCP function count for a provider async getMCPFunctionCount(providerName?: string): Promise { if (!this.mcpEnabled) { return 0; } try { const provider = this.getEnhancedProvider(providerName); return await provider.getMCPFunctionCount(); } catch (_error) { return 0; } } }