Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 96 additions & 14 deletions LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,117 @@

import { AIProvider } from './AIProvider'
import { AICapability } from './types'
import { GeminiProvider } from '../providers/GeminiProvider'

type ProviderHealth = {
failures: number
lastFailureAt?: number
}

const MAX_FAILURES = 3

class AIProviderRegistry {
private providers = new Map<string, AIProvider>()
private health = new Map<string, ProviderHealth>()

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)
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))
)
}
markSuccess(providerName: string) {
const info = this.health.get(providerName)
if (!info) return

info.failures = 0
}

list(): AIProvider[] {
return Array.from(this.providers.values())
// --------------------
// Safe execution with proper fallback
// --------------------

async generateTextWithFallback(
capabilities: AICapability[],
input: { prompt: string; context?: string }
): Promise<string> {
const candidates = this.findByCapabilities(capabilities)
const healthyProviders = candidates.filter(p => this.isHealthy(p.name))
const degradedProviders = candidates.filter(p => !this.isHealthy(p.name))
let lastError: unknown

// 1️⃣ Try all healthy providers first
for (const provider of healthyProviders) {
try {
const result = await provider.generateText(input)
this.markSuccess(provider.name)
return result
} catch (err) {
lastError = err
this.markFailure(provider.name)
}
}

// 2️⃣ Fall back to degraded providers if all healthy ones failed
for (const provider of degradedProviders) {
try {
const result = await provider.generateText(input)
this.markSuccess(provider.name)
return result
} catch (err) {
lastError = err
this.markFailure(provider.name)
}
}

// 3️⃣ Throw the last error encountered (preserves stack trace and error details)
if (lastError) {
throw lastError
}

// 4️⃣ If no providers matched capabilities at all
throw new Error('No AI provider available for the specified capabilities')
}
}

export const aiProviderRegistry = new AIProviderRegistry()

// Register providers
aiProviderRegistry.register(new GeminiProvider())
25 changes: 25 additions & 0 deletions LocalMind-Backend/src/api/ai/providers/GeminiProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// src/api/ai/providers/GeminiProvider.ts

import { AIProvider } from '../core/AIProvider'
import { AICapability } from '../core/types'

export class GeminiProvider implements AIProvider {
readonly name = 'gemini'

readonly capabilities = new Set<AICapability>([
'cloud',
'multimodal',
])

supports(capability: AICapability): boolean {
return this.capabilities.has(capability)
}

async generateText(input: {
prompt: string
context?: string
}): Promise<string> {
// TEMP mock response (safe, non-breaking)
return `[Gemini] ${input.prompt}`
}
}