diff --git a/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts new file mode 100644 index 0000000..b902a90 --- /dev/null +++ b/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts @@ -0,0 +1,59 @@ +// src/api/ai/core/AIModelRegistry.ts + +import { AICapability } from './types' + +export type AIModel = { + id: string + provider: string + capabilities: AICapability[] +} + +class AIModelRegistry { + private models: AIModel[] = [] + + // -------------------- + // Model management + // -------------------- + + register(model: AIModel) { + if (this.models.some(m => m.id === model.id && m.provider === model.provider)) { + throw new Error( + `Model "${model.id}" from provider "${model.provider}" is already registered.` + ) + } + this.models.push(model) + } + + listProviders(): string[] { + return [...new Set(this.models.map(m => m.provider))] + } + + listModelsByProvider(provider: string): AIModel[] { + return this.models.filter(m => m.provider === provider) + } + + getModel(provider: string, modelId: string): AIModel | undefined { + return this.models.find( + m => m.provider === provider && m.id === modelId + ) + } + + listAll(): AIModel[] { + return [...this.models] + } +} + +export const aiModelRegistry = new AIModelRegistry() + +// Register default models +aiModelRegistry.register({ + id: 'gemini-pro', + provider: 'gemini', + capabilities: ['cloud'], +}) + +aiModelRegistry.register({ + id: 'gemini-pro-vision', + provider: 'gemini', + capabilities: ['cloud', 'multimodal'], +}) \ No newline at end of file diff --git a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts index d60b66b..4c23d37 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -3,34 +3,114 @@ import { AIProvider } from './AIProvider' import { AICapability } from './types' +type ProviderHealth = { + failures: number + lastFailureAt?: number +} + +const MAX_FAILURES = 3 +const COOLDOWN_PERIOD_MS = 300000 // 5 minutes + class AIProviderRegistry { private providers = new Map() + private health = new Map() + + // -------------------- + // Provider management + // -------------------- register(provider: AIProvider) { - if (this.providers.has(provider.name)) { - throw new Error( - `A provider with the name "${provider.name}" is already registered.` + if (this.providers.has(provider.name)) { + throw new Error( + `A provider with the name "${provider.name}" is already registered.` + ) + } + + this.providers.set(provider.name, provider) + this.health.set(provider.name, { failures: 0 }) + } + + get(name: string): AIProvider | undefined { + return this.providers.get(name) + } + + list(): AIProvider[] { + return Array.from(this.providers.values()) + } + + findByCapabilities(capabilities: AICapability[]): AIProvider[] { + return Array.from(this.providers.values()).filter(provider => + capabilities.every(cap => provider.supports(cap)) ) } - this.providers.set(provider.name, provider) -} + // -------------------- + // Health tracking with cooldown recovery + // -------------------- + private isHealthy(providerName: string): boolean { + const info = this.health.get(providerName) + if (!info) return true - get(name: string): AIProvider | undefined { - return this.providers.get(name) + if (info.failures < MAX_FAILURES) { + return true + } + + // Allow retry after cooldown period + if (info.lastFailureAt && Date.now() - info.lastFailureAt > COOLDOWN_PERIOD_MS) { + this.markSuccess(providerName) // Reset health to allow retry + return true + } + + return false } - findByCapabilities(capabilities: AICapability[]): AIProvider[] { - return Array.from(this.providers.values()).filter(provider => - capabilities.every(cap => provider.supports(cap)) - ) -} + private markFailure(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + info.failures += 1 + info.lastFailureAt = Date.now() + } - list(): AIProvider[] { - return Array.from(this.providers.values()) + private markSuccess(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + + info.failures = 0 + } + + // -------------------- + // Provider + Model routing + // -------------------- + + async generateTextWithModel( + providerName: string, + modelId: string, + input: { prompt: string } + ): Promise { + const provider = this.get(providerName) + + if (!provider) { + throw new Error(`Provider "${providerName}" not found`) + } + + if (!this.isHealthy(provider.name)) { + throw new Error(`Provider "${providerName}" is currently degraded`) + } + + try { + const result = await provider.generateText({ + ...input, + context: modelId, + }) + this.markSuccess(provider.name) + return result + } catch (err) { + this.markFailure(provider.name) + throw err + } } } -export const aiProviderRegistry = new AIProviderRegistry() +export const aiProviderRegistry = new AIProviderRegistry() \ No newline at end of file