From a534de5f7185cf50fb5af0ff29002e04af432a79 Mon Sep 17 00:00:00 2001 From: Shashank Saga Date: Tue, 6 Jan 2026 15:40:15 +0530 Subject: [PATCH 1/2] feat(ai): add provider-model routing with model registry --- .../src/api/ai/core/AIModelRegistry.ts | 40 ++++++++ .../src/api/ai/core/AIProviderRegistry.ts | 96 ++++++++++++++++--- 2 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts 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..4a4823c --- /dev/null +++ b/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts @@ -0,0 +1,40 @@ +// src/api/ai/core/AIModelRegistry.ts + +import { AICapability } from './types' + +export type AIModel = { + id: string + provider: string + capabilities: AICapability[] +} + +class AIModelRegistry { + private models: AIModel[] = [ + { + id: 'gemini-pro', + provider: 'gemini', + capabilities: ['cloud'], + }, + { + id: 'gemini-pro-vision', + provider: 'gemini', + capabilities: ['cloud', 'multimodal'], + }, + ] + + 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 + ) + } +} + +export const aiModelRegistry = new AIModelRegistry() diff --git a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts index d60b66b..6923718 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -3,33 +3,101 @@ import { AIProvider } from './AIProvider' import { AICapability } from './types' +type ProviderHealth = { + failures: number + lastFailureAt?: number +} + +const MAX_FAILURES = 3 + 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 + // -------------------- + private isHealthy(providerName: string): boolean { + const info = this.health.get(providerName) + if (!info) return true + return info.failures < MAX_FAILURES + } - get(name: string): AIProvider | undefined { - return this.providers.get(name) + private markFailure(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + + info.failures += 1 + info.lastFailureAt = Date.now() } - findByCapabilities(capabilities: AICapability[]): AIProvider[] { - return Array.from(this.providers.values()).filter(provider => - capabilities.every(cap => provider.supports(cap)) - ) -} + private markSuccess(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + info.failures = 0 + } - list(): AIProvider[] { - return Array.from(this.providers.values()) + // -------------------- + // Provider + Model routing + // -------------------- + + async generateTextWithModel( + providerName: string, + modelId: string, + input: { prompt: string; context?: 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 + } } } From 4e8ff9e31ae339ead9f6269d3328eddff460e79e Mon Sep 17 00:00:00 2001 From: Shashank Saga Date: Tue, 6 Jan 2026 15:48:10 +0530 Subject: [PATCH 2/2] fix(ai-core): add provider cooldown recovery, clarify model routing API, and make model registry extensible --- .../src/api/ai/core/AIModelRegistry.ts | 43 +++++++++++++------ .../src/api/ai/core/AIProviderRegistry.ts | 20 +++++++-- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts index 4a4823c..b902a90 100644 --- a/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIModelRegistry.ts @@ -9,18 +9,20 @@ export type AIModel = { } class AIModelRegistry { - private models: AIModel[] = [ - { - id: 'gemini-pro', - provider: 'gemini', - capabilities: ['cloud'], - }, - { - id: 'gemini-pro-vision', - provider: 'gemini', - capabilities: ['cloud', 'multimodal'], - }, - ] + 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))] @@ -35,6 +37,23 @@ class AIModelRegistry { 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 6923718..4c23d37 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -9,6 +9,7 @@ type ProviderHealth = { } const MAX_FAILURES = 3 +const COOLDOWN_PERIOD_MS = 300000 // 5 minutes class AIProviderRegistry { private providers = new Map() @@ -44,13 +45,24 @@ class AIProviderRegistry { } // -------------------- - // Health tracking + // Health tracking with cooldown recovery // -------------------- private isHealthy(providerName: string): boolean { const info = this.health.get(providerName) if (!info) return true - return info.failures < MAX_FAILURES + + 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 } private markFailure(providerName: string) { @@ -75,7 +87,7 @@ class AIProviderRegistry { async generateTextWithModel( providerName: string, modelId: string, - input: { prompt: string; context?: string } + input: { prompt: string } ): Promise { const provider = this.get(providerName) @@ -101,4 +113,4 @@ class AIProviderRegistry { } } -export const aiProviderRegistry = new AIProviderRegistry() +export const aiProviderRegistry = new AIProviderRegistry() \ No newline at end of file