/** * ProviderManager - Unified facade for LLM providers * * Implements contracts §15; DD §12.1. * INV-4: runtime calls llm only via this facade. * * @module packages/llm/src/ProviderManager */ import type { ProviderAdapter } from '@aircoding/contracts' // Local type definitions (contract types not yet finalized) type CompleteOptions = { max_tokens?: number; temperature?: number; top_p?: number; system?: string } type StreamEvent = { type: 'text' | 'thinking' | 'done'; content?: string; reason?: string } type ModelRequirement = { model: string; provider?: string; min_output_tokens?: number; prefers_thinking?: boolean; requires_tools?: boolean } type ModelAssignment = { provider: string; model: string; adapter: any; capabilities?: any } import { ModelConfigLoader, createModelConfigLoader } from './ModelConfigLoader.js' import { CapabilityMatrixRegistry, createCapabilityMatrixRegistry } from './CapabilityMatrix.js' import { AnthropicAdapter, createAnthropicAdapter } from './adapters/AnthropicAdapter.js' import { OpenAICompatibleAdapter, createOpenAICompatibleAdapter } from './adapters/OpenAICompatibleAdapter.js' export interface ProviderManagerConfig { config_loader?: ModelConfigLoader capability_matrix?: CapabilityMatrixRegistry } export class ProviderManager { private config_loader: ModelConfigLoader private capability_matrix: CapabilityMatrixRegistry private adapters: Map = new Map() private current_adapter: ProviderAdapter | null = null private current_model: string = '' constructor(config: ProviderManagerConfig = {}) { this.config_loader = config.config_loader || createModelConfigLoader() this.capability_matrix = config.capability_matrix || createCapabilityMatrixRegistry() // Initialize adapters this.initialize_adapters() } /** * Load configuration from files. */ load_config(): { ok: boolean; error?: string } { const result = this.config_loader.load() if (!result.ok) { return { ok: false, error: result.error } } return { ok: true } } /** * Select a model based on requirements. * Returns a ModelAssignment. */ select_model(requirement: ModelRequirement): ModelAssignment { // Try to find matching model in capability matrix const best_model = this.capability_matrix.find_best(requirement.provider || 'anthropic', { min_output_tokens: requirement.min_output_tokens, thinking: requirement.prefers_thinking, tool_use: requirement.requires_tools }) const model = requirement.model || best_model || `${requirement.provider}-default` // Get adapter for this provider const adapter = this.get_or_create_adapter(requirement.provider || 'anthropic', model) this.current_adapter = adapter this.current_model = model return { provider: requirement.provider || 'anthropic', model, adapter, capabilities: this.capability_matrix.lookup(requirement.provider || 'anthropic', model) } } /** * Complete a request with the current model. */ async complete( messages: unknown[], assignment: ModelAssignment, options: CompleteOptions = {} ): Promise<{ content: string; usage?: { input_tokens: number; output_tokens: number } }> { const adapter = assignment.adapter || this.current_adapter if (!adapter) { throw new Error('No adapter selected. Call select_model first.') } return adapter.complete(messages as any, { model: assignment.model }, options) } /** * Stream a completion request. */ async *stream_complete( messages: unknown[], assignment: ModelAssignment, options: CompleteOptions = {} ): AsyncGenerator { const adapter = assignment.adapter || this.current_adapter if (!adapter) { throw new Error('No adapter selected. Call select_model first.') } yield* adapter.stream_complete(messages as any, { model: assignment.model }, options) } /** * Get an adapter for a specific provider. */ adapter_for(provider: string, model: string): ProviderAdapter { return this.get_or_create_adapter(provider, model) } /** * Get current adapter. */ get_current_adapter(): ProviderAdapter | null { return this.current_adapter } /** * Get current model. */ get_current_model(): string { return this.current_model } // ============================================================================ // Private helpers // ============================================================================ private initialize_adapters(): void { // Create default adapters const anthropic = createAnthropicAdapter() this.adapters.set('anthropic', anthropic) // Check for OpenAI-compatible providers in config const config = this.config_loader.load() if (config.ok && config.configs?.global) { for (const [name, model_config] of Object.entries(config.configs.global)) { if (model_config.provider === 'openai-compatible' && model_config.base_url) { const adapter = createOpenAICompatibleAdapter({ base_url: model_config.base_url, model: model_config.model, api_key: model_config.api_key }) this.adapters.set(name, adapter) } } } } private get_or_create_adapter(provider: string, model: string): ProviderAdapter { // Check if we already have an adapter for this provider const existing = this.adapters.get(provider) if (existing) { return existing } // Create new adapter based on provider let adapter: ProviderAdapter if (provider === 'anthropic') { adapter = createAnthropicAdapter() } else { // Check config for OpenAI-compatible const model_config = this.config_loader.get_model(`${provider}-${model}`) if (model_config?.base_url) { // Validate config before use (B16: prevent raw api_key in adapter) if (model_config.api_key && !model_config.auth_ref) { this.config_loader.validate(model_config) console.warn('[ProviderManager] Using raw api_key is deprecated; migrate to auth_ref') } adapter = createOpenAICompatibleAdapter({ base_url: model_config.base_url, model: model_config.model, api_key: model_config.api_key }) } else { // Default to OpenAI-compatible with default settings adapter = createOpenAICompatibleAdapter({ base_url: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', model: model }) } } this.adapters.set(provider, adapter) return adapter } } export function createProviderManager(config?: ProviderManagerConfig): ProviderManager { return new ProviderManager(config) } // Export facade as default instance let default_instance: ProviderManager | undefined export function get_provider_manager(): ProviderManager { if (!default_instance) { default_instance = createProviderManager() } return default_instance }