feat(llm): still work on decomplicating provider creation

This commit is contained in:
perf3ct 2025-06-06 20:30:24 +00:00
parent 8f33f37de3
commit 20ec294774
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
4 changed files with 262 additions and 444 deletions

View File

@ -267,12 +267,23 @@ export class AIServiceManager implements IAIServiceManager {
// If not a provider prefix, treat the entire string as a model name and continue with normal provider selection // If not a provider prefix, treat the entire string as a model name and continue with normal provider selection
} }
// Try each provider in order until one succeeds // If user has a specific provider selected, try only that one and fail fast
if (this.providerOrder.length === 1 && sortedProviders.length === 1) {
const selectedProvider = sortedProviders[0];
const service = await this.getOrCreateChatProvider(selectedProvider);
if (!service) {
throw new Error(`Failed to create selected chat provider: ${selectedProvider}. Please check your configuration.`);
}
log.info(`[AIServiceManager] Using selected provider ${selectedProvider} with options.stream: ${options.stream}`);
return await service.generateChatCompletion(messages, options);
}
// If no specific provider selected, try each provider in order until one succeeds
let lastError: Error | null = null; let lastError: Error | null = null;
for (const provider of sortedProviders) { for (const provider of sortedProviders) {
try { try {
const service = this.services[provider]; const service = await this.getOrCreateChatProvider(provider);
if (service) { if (service) {
log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`); log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`);
return await service.generateChatCompletion(messages, options); return await service.generateChatCompletion(messages, options);
@ -383,7 +394,7 @@ export class AIServiceManager implements IAIServiceManager {
} }
/** /**
* Get or create a chat provider on-demand * Get or create a chat provider on-demand with inline validation
*/ */
private async getOrCreateChatProvider(providerName: ServiceProviders): Promise<AIService | null> { private async getOrCreateChatProvider(providerName: ServiceProviders): Promise<AIService | null> {
// Return existing provider if already created // Return existing provider if already created
@ -391,38 +402,54 @@ export class AIServiceManager implements IAIServiceManager {
return this.services[providerName]; return this.services[providerName];
} }
// Create provider on-demand based on configuration // Create and validate provider on-demand
try { try {
let service: AIService | null = null;
switch (providerName) { switch (providerName) {
case 'openai': case 'openai': {
const openaiApiKey = await options.getOption('openaiApiKey'); const apiKey = await options.getOption('openaiApiKey');
if (openaiApiKey) { const baseUrl = await options.getOption('openaiBaseUrl');
this.services.openai = new OpenAIService(); if (!apiKey && !baseUrl) return null;
log.info('Created OpenAI chat provider on-demand');
return this.services.openai;
}
break;
case 'anthropic': service = new OpenAIService();
const anthropicApiKey = await options.getOption('anthropicApiKey'); // Validate by checking if it's available
if (anthropicApiKey) { if (!service.isAvailable()) {
this.services.anthropic = new AnthropicService(); throw new Error('OpenAI service not available');
log.info('Created Anthropic chat provider on-demand');
return this.services.anthropic;
} }
break; break;
}
case 'ollama': case 'anthropic': {
const ollamaBaseUrl = await options.getOption('ollamaBaseUrl'); const apiKey = await options.getOption('anthropicApiKey');
if (ollamaBaseUrl) { if (!apiKey) return null;
this.services.ollama = new OllamaService();
log.info('Created Ollama chat provider on-demand'); service = new AnthropicService();
return this.services.ollama; if (!service.isAvailable()) {
throw new Error('Anthropic service not available');
} }
break; break;
}
case 'ollama': {
const baseUrl = await options.getOption('ollamaBaseUrl');
if (!baseUrl) return null;
service = new OllamaService();
if (!service.isAvailable()) {
throw new Error('Ollama service not available');
}
break;
}
}
if (service) {
this.services[providerName] = service;
log.info(`Created and validated ${providerName} chat provider`);
return service;
} }
} catch (error: any) { } catch (error: any) {
log.error(`Error creating ${providerName} provider on-demand: ${error.message || 'Unknown error'}`); log.error(`Failed to create ${providerName} chat provider: ${error.message || 'Unknown error'}`);
} }
return null; return null;

View File

@ -48,53 +48,16 @@ export class IndexService {
async initialize() { async initialize() {
if (this.initialized) return; if (this.initialized) return;
try { // Setup event listeners for note changes
// Check if database is initialized before proceeding this.setupEventListeners();
if (!sqlInit.isDbInitialized()) {
log.info("Index service: Database not initialized yet, skipping initialization");
return;
}
const aiEnabled = options.getOptionOrNull('aiEnabled') === "true"; // Setup automatic indexing if enabled
if (!aiEnabled) { if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
log.info("Index service: AI features disabled, skipping initialization"); this.setupAutomaticIndexing();
return;
}
// Check if embedding system is ready
if (!(await hasWorkingEmbeddingProviders())) {
log.info("Index service: No working embedding providers available, skipping initialization");
return;
}
const providers = await providerManager.getEnabledEmbeddingProviders();
if (!providers || providers.length === 0) {
log.info("Index service: No enabled embedding providers, skipping initialization");
return;
}
// Check if this instance should process embeddings
const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client';
const isSyncServer = await this.isSyncServerForEmbeddings();
const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer;
// Setup automatic indexing if enabled and this instance should process embeddings
if (await options.getOptionBool('embeddingAutoUpdateEnabled') && shouldProcessEmbeddings) {
this.setupAutomaticIndexing();
log.info(`Index service: Automatic indexing enabled, processing embeddings ${isSyncServer ? 'as sync server' : 'as client'}`);
} else if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
log.info("Index service: Automatic indexing enabled, but this instance is not configured to process embeddings");
}
// Listen for note changes to update index
this.setupEventListeners();
this.initialized = true;
log.info("Index service initialized successfully");
} catch (error: any) {
log.error(`Error initializing index service: ${error.message || "Unknown error"}`);
throw error;
} }
this.initialized = true;
log.info("Index service initialized");
} }
/** /**
@ -147,23 +110,7 @@ export class IndexService {
this.automaticIndexingInterval = setInterval(async () => { this.automaticIndexingInterval = setInterval(async () => {
try { try {
if (!this.indexingInProgress) { if (!this.indexingInProgress) {
// Check if this instance should process embeddings await this.runBatchIndexing(50); // Processing logic handles sync server checks
const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client';
const isSyncServer = await this.isSyncServerForEmbeddings();
const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer;
if (!shouldProcessEmbeddings) {
// This instance is not configured to process embeddings
return;
}
const stats = await vectorStore.getEmbeddingStats();
// Only run automatic indexing if we're below 95% completion
if (stats.percentComplete < 95) {
log.info(`Starting automatic indexing (current completion: ${stats.percentComplete}%)`);
await this.runBatchIndexing(50); // Process 50 notes at a time
}
} }
} catch (error: any) { } catch (error: any) {
log.error(`Error in automatic indexing: ${error.message || "Unknown error"}`); log.error(`Error in automatic indexing: ${error.message || "Unknown error"}`);
@ -498,35 +445,14 @@ export class IndexService {
} }
try { try {
// Get all enabled embedding providers // Get the selected embedding provider on-demand
const providers = await providerManager.getEnabledEmbeddingProviders();
if (!providers || providers.length === 0) {
throw new Error("No embedding providers available");
}
// Get the selected embedding provider
const options = (await import('../options.js')).default;
const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider'); const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider');
let provider; const provider = selectedEmbeddingProvider
? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider)
if (selectedEmbeddingProvider) { : (await providerManager.getEnabledEmbeddingProviders())[0];
// Try to use the selected provider
const enabledProviders = await providerManager.getEnabledEmbeddingProviders();
provider = enabledProviders.find(p => p.name === selectedEmbeddingProvider);
if (!provider) {
log.info(`Selected embedding provider ${selectedEmbeddingProvider} is not available, using first enabled provider`);
// Fall back to first enabled provider
provider = providers[0];
}
} else {
// No provider selected, use first available provider
log.info('No embedding provider selected, using first available provider');
provider = providers[0];
}
if (!provider) { if (!provider) {
throw new Error("No suitable embedding provider found"); throw new Error("No embedding provider available");
} }
log.info(`Searching with embedding provider: ${provider.name}, model: ${provider.getConfig().model}`); log.info(`Searching with embedding provider: ${provider.name}, model: ${provider.getConfig().model}`);
@ -684,6 +610,12 @@ export class IndexService {
} }
try { try {
// Get embedding providers on-demand
const providers = await providerManager.getEnabledEmbeddingProviders();
if (providers.length === 0) {
return "I don't have access to your note embeddings. Please configure an embedding provider in your AI settings.";
}
// Find similar notes to the query // Find similar notes to the query
const similarNotes = await this.findSimilarNotes( const similarNotes = await this.findSimilarNotes(
query, query,
@ -819,9 +751,13 @@ export class IndexService {
// Get complete note context for indexing // Get complete note context for indexing
const context = await vectorStore.getNoteEmbeddingContext(noteId); const context = await vectorStore.getNoteEmbeddingContext(noteId);
// Queue note for embedding with all available providers // Generate embedding with the selected provider
const providers = await providerManager.getEnabledEmbeddingProviders(); const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider');
for (const provider of providers) { const provider = selectedEmbeddingProvider
? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider)
: (await providerManager.getEnabledEmbeddingProviders())[0];
if (provider) {
try { try {
const embedding = await provider.generateNoteEmbeddings(context); const embedding = await provider.generateNoteEmbeddings(context);
if (embedding) { if (embedding) {
@ -873,16 +809,13 @@ export class IndexService {
return; return;
} }
// Verify providers are available // Get embedding providers (will be created on-demand when needed)
if (!(await hasWorkingEmbeddingProviders())) {
throw new Error("No working embedding providers available");
}
const providers = await providerManager.getEnabledEmbeddingProviders(); const providers = await providerManager.getEnabledEmbeddingProviders();
if (providers.length === 0) { if (providers.length === 0) {
throw new Error("No embedding providers available"); log.info("No embedding providers configured, but continuing initialization");
} else {
log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`);
} }
log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`);
// Setup automatic indexing if enabled // Setup automatic indexing if enabled
if (await options.getOptionBool('embeddingAutoUpdateEnabled')) { if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {

View File

@ -18,7 +18,7 @@ export interface ProviderValidationResult {
} }
/** /**
* Validate all available providers without throwing errors * Simplified provider validation - just checks configuration without creating providers
*/ */
export async function validateProviders(): Promise<ProviderValidationResult> { export async function validateProviders(): Promise<ProviderValidationResult> {
const result: ProviderValidationResult = { const result: ProviderValidationResult = {
@ -37,14 +37,12 @@ export async function validateProviders(): Promise<ProviderValidationResult> {
return result; return result;
} }
// Validate embedding providers // Check configuration only - don't create providers
await validateEmbeddingProviders(result); await checkEmbeddingProviderConfigs(result);
await checkChatProviderConfigs(result);
// Validate chat providers // Determine if we have any valid providers based on configuration
await validateChatProviders(result); result.hasValidProviders = result.validChatProviders.length > 0;
// Determine if we have any valid providers
result.hasValidProviders = result.validEmbeddingProviders.length > 0 || result.validChatProviders.length > 0;
if (!result.hasValidProviders) { if (!result.hasValidProviders) {
result.errors.push("No valid AI providers are configured"); result.errors.push("No valid AI providers are configured");
@ -58,241 +56,80 @@ export async function validateProviders(): Promise<ProviderValidationResult> {
} }
/** /**
* Validate embedding providers * Check embedding provider configurations without creating providers
*/ */
async function validateEmbeddingProviders(result: ProviderValidationResult): Promise<void> { async function checkEmbeddingProviderConfigs(result: ProviderValidationResult): Promise<void> {
try { try {
// Import provider classes and check configurations // Check OpenAI embedding configuration
const { OpenAIEmbeddingProvider } = await import("./embeddings/providers/openai.js"); const openaiApiKey = await options.getOption('openaiApiKey');
const { OllamaEmbeddingProvider } = await import("./embeddings/providers/ollama.js"); const openaiBaseUrl = await options.getOption('openaiBaseUrl');
const { VoyageEmbeddingProvider } = await import("./embeddings/providers/voyage.js"); if (openaiApiKey || openaiBaseUrl) {
if (!openaiApiKey) {
result.warnings.push("OpenAI embedding: No API key (may work with compatible endpoints)");
}
log.info("OpenAI embedding provider configuration available");
}
// Check OpenAI embedding provider // Check Ollama embedding configuration
await validateOpenAIEmbeddingProvider(result, OpenAIEmbeddingProvider); const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
if (ollamaEmbeddingBaseUrl) {
log.info("Ollama embedding provider configuration available");
}
// Check Ollama embedding provider // Check Voyage embedding configuration
await validateOllamaEmbeddingProvider(result, OllamaEmbeddingProvider); const voyageApiKey = await options.getOption('voyageApiKey' as any);
if (voyageApiKey) {
log.info("Voyage embedding provider configuration available");
}
// Check Voyage embedding provider // Local provider is always available
await validateVoyageEmbeddingProvider(result, VoyageEmbeddingProvider); log.info("Local embedding provider available as fallback");
// Local provider is always available as fallback
await validateLocalEmbeddingProvider(result);
} catch (error: any) { } catch (error: any) {
result.errors.push(`Error validating embedding providers: ${error.message || 'Unknown error'}`); result.errors.push(`Error checking embedding provider configs: ${error.message || 'Unknown error'}`);
} }
} }
/** /**
* Validate chat providers * Check chat provider configurations without creating providers
*/ */
async function validateChatProviders(result: ProviderValidationResult): Promise<void> { async function checkChatProviderConfigs(result: ProviderValidationResult): Promise<void> {
try { try {
// Check OpenAI chat provider // Check OpenAI chat provider
const openaiApiKey = await options.getOption('openaiApiKey'); const openaiApiKey = await options.getOption('openaiApiKey');
const openaiBaseUrl = await options.getOption('openaiBaseUrl'); const openaiBaseUrl = await options.getOption('openaiBaseUrl');
if (openaiApiKey || openaiBaseUrl) { if (openaiApiKey || openaiBaseUrl) {
if (!openaiApiKey && !openaiBaseUrl) { if (!openaiApiKey) {
result.warnings.push("OpenAI chat provider: No API key or base URL configured"); result.warnings.push("OpenAI chat: No API key (may work with compatible endpoints)");
} else if (!openaiApiKey) {
result.warnings.push("OpenAI chat provider: No API key configured (may work with compatible endpoints)");
result.validChatProviders.push('openai');
} else {
result.validChatProviders.push('openai');
} }
result.validChatProviders.push('openai');
} }
// Check Anthropic chat provider // Check Anthropic chat provider
const anthropicApiKey = await options.getOption('anthropicApiKey'); const anthropicApiKey = await options.getOption('anthropicApiKey');
if (anthropicApiKey) { if (anthropicApiKey) {
result.validChatProviders.push('anthropic'); result.validChatProviders.push('anthropic');
} else {
result.warnings.push("Anthropic chat provider: No API key configured");
} }
// Check Ollama chat provider // Check Ollama chat provider
const ollamaBaseUrl = await options.getOption('ollamaBaseUrl'); const ollamaBaseUrl = await options.getOption('ollamaBaseUrl');
if (ollamaBaseUrl) { if (ollamaBaseUrl) {
result.validChatProviders.push('ollama'); result.validChatProviders.push('ollama');
} else { }
result.warnings.push("Ollama chat provider: No base URL configured");
if (result.validChatProviders.length === 0) {
result.warnings.push("No chat providers configured. Please configure at least one provider.");
} }
} catch (error: any) { } catch (error: any) {
result.errors.push(`Error validating chat providers: ${error.message || 'Unknown error'}`); result.errors.push(`Error checking chat provider configs: ${error.message || 'Unknown error'}`);
} }
} }
/**
* Validate OpenAI embedding provider
*/
async function validateOpenAIEmbeddingProvider(
result: ProviderValidationResult,
OpenAIEmbeddingProvider: any
): Promise<void> {
try {
const openaiApiKey = await options.getOption('openaiApiKey');
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
if (openaiApiKey || openaiBaseUrl) {
const openaiModel = await options.getOption('openaiEmbeddingModel');
const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1';
if (!openaiApiKey) {
result.warnings.push("OpenAI embedding provider: No API key configured (may work with compatible endpoints)");
}
const provider = new OpenAIEmbeddingProvider({
model: openaiModel,
dimension: 1536,
type: 'float32',
apiKey: openaiApiKey || '',
baseUrl: finalBaseUrl
});
result.validEmbeddingProviders.push(provider);
log.info(`Validated OpenAI embedding provider: ${openaiModel} at ${finalBaseUrl}`);
} else {
result.warnings.push("OpenAI embedding provider: No API key or base URL configured");
}
} catch (error: any) {
result.errors.push(`OpenAI embedding provider validation failed: ${error.message || 'Unknown error'}`);
}
}
/** /**
* Validate Ollama embedding provider * Check if any chat providers are configured
*/
async function validateOllamaEmbeddingProvider(
result: ProviderValidationResult,
OllamaEmbeddingProvider: any
): Promise<void> {
try {
const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
if (ollamaEmbeddingBaseUrl) {
const embeddingModel = await options.getOption('ollamaEmbeddingModel');
try {
const provider = new OllamaEmbeddingProvider({
model: embeddingModel,
dimension: 768,
type: 'float32',
baseUrl: ollamaEmbeddingBaseUrl
});
// Try to initialize to validate connection
await provider.initialize();
result.validEmbeddingProviders.push(provider);
log.info(`Validated Ollama embedding provider: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`);
} catch (error: any) {
result.warnings.push(`Ollama embedding provider initialization failed: ${error.message || 'Unknown error'}`);
}
} else {
result.warnings.push("Ollama embedding provider: No base URL configured");
}
} catch (error: any) {
result.errors.push(`Ollama embedding provider validation failed: ${error.message || 'Unknown error'}`);
}
}
/**
* Validate Voyage embedding provider
*/
async function validateVoyageEmbeddingProvider(
result: ProviderValidationResult,
VoyageEmbeddingProvider: any
): Promise<void> {
try {
const voyageApiKey = await options.getOption('voyageApiKey' as any);
if (voyageApiKey) {
const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
const provider = new VoyageEmbeddingProvider({
model: voyageModel,
dimension: 1024,
type: 'float32',
apiKey: voyageApiKey,
baseUrl: 'https://api.voyageai.com/v1'
});
result.validEmbeddingProviders.push(provider);
log.info(`Validated Voyage embedding provider: ${voyageModel}`);
} else {
result.warnings.push("Voyage embedding provider: No API key configured");
}
} catch (error: any) {
result.errors.push(`Voyage embedding provider validation failed: ${error.message || 'Unknown error'}`);
}
}
/**
* Validate local embedding provider (always available as fallback)
*/
async function validateLocalEmbeddingProvider(result: ProviderValidationResult): Promise<void> {
try {
// Simple local embedding provider implementation
class SimpleLocalEmbeddingProvider {
name = "local";
config = {
model: 'local',
dimension: 384,
type: 'float32' as const
};
getConfig() {
return this.config;
}
getNormalizationStatus() {
return 0; // NormalizationStatus.NEVER
}
async generateEmbeddings(text: string): Promise<Float32Array> {
const result = new Float32Array(this.config.dimension);
for (let i = 0; i < result.length; i++) {
const charSum = Array.from(text).reduce((sum, char, idx) =>
sum + char.charCodeAt(0) * Math.sin(idx * 0.1), 0);
result[i] = Math.sin(i * 0.1 + charSum * 0.01);
}
return result;
}
async generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]> {
return Promise.all(texts.map(text => this.generateEmbeddings(text)));
}
async generateNoteEmbeddings(context: any): Promise<Float32Array> {
const text = (context.title || "") + " " + (context.content || "");
return this.generateEmbeddings(text);
}
async generateBatchNoteEmbeddings(contexts: any[]): Promise<Float32Array[]> {
return Promise.all(contexts.map(context => this.generateNoteEmbeddings(context)));
}
}
const localProvider = new SimpleLocalEmbeddingProvider();
result.validEmbeddingProviders.push(localProvider as any);
log.info("Validated local embedding provider as fallback");
} catch (error: any) {
result.errors.push(`Local embedding provider validation failed: ${error.message || 'Unknown error'}`);
}
}
/**
* Check if any working providers are available for embeddings
*/
export async function hasWorkingEmbeddingProviders(): Promise<boolean> {
const validation = await validateProviders();
return validation.validEmbeddingProviders.length > 0;
}
/**
* Check if any working providers are available for chat
*/ */
export async function hasWorkingChatProviders(): Promise<boolean> { export async function hasWorkingChatProviders(): Promise<boolean> {
const validation = await validateProviders(); const validation = await validateProviders();
@ -300,11 +137,21 @@ export async function hasWorkingChatProviders(): Promise<boolean> {
} }
/** /**
* Get only the working embedding providers * Check if any embedding providers are configured (simplified)
*/ */
export async function getWorkingEmbeddingProviders(): Promise<EmbeddingProvider[]> { export async function hasWorkingEmbeddingProviders(): Promise<boolean> {
const validation = await validateProviders(); if (!(await options.getOptionBool('aiEnabled'))) {
return validation.validEmbeddingProviders; return false;
}
// Check if any embedding provider is configured
const openaiKey = await options.getOption('openaiApiKey');
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
const ollamaUrl = await options.getOption('ollamaEmbeddingBaseUrl');
const voyageKey = await options.getOption('voyageApiKey' as any);
// Local provider is always available as fallback
return !!(openaiKey || openaiBaseUrl || ollamaUrl || voyageKey) || true;
} }
/** /**

View File

@ -124,118 +124,129 @@ export function getEmbeddingProvider(name: string): EmbeddingProvider | undefine
} }
/** /**
* Create providers on-demand based on current options * Get or create a specific embedding provider with inline validation
*/ */
export async function createProvidersFromCurrentOptions(): Promise<EmbeddingProvider[]> { export async function getOrCreateEmbeddingProvider(providerName: string): Promise<EmbeddingProvider | null> {
const result: EmbeddingProvider[] = []; // Return existing provider if already created and valid
const existing = providers.get(providerName);
try { if (existing) {
// Create Ollama provider if embedding base URL is configured return existing;
const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
if (ollamaEmbeddingBaseUrl) {
const embeddingModel = await options.getOption('ollamaEmbeddingModel');
try {
const ollamaProvider = new OllamaEmbeddingProvider({
model: embeddingModel,
dimension: 768, // Initial value, will be updated during initialization
type: 'float32',
baseUrl: ollamaEmbeddingBaseUrl
});
await ollamaProvider.initialize();
registerEmbeddingProvider(ollamaProvider);
result.push(ollamaProvider);
log.info(`Created Ollama provider on-demand: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`);
} catch (error: any) {
log.error(`Error creating Ollama embedding provider on-demand: ${error.message || 'Unknown error'}`);
}
}
// Create OpenAI provider even without API key (for OpenAI-compatible endpoints)
const openaiApiKey = await options.getOption('openaiApiKey');
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
// Only create OpenAI provider if base URL is set or API key is provided
if (openaiApiKey || openaiBaseUrl) {
const openaiModel = await options.getOption('openaiEmbeddingModel')
const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1';
if (!openaiApiKey) {
log.info('Creating OpenAI embedding provider without API key. This may cause issues with official OpenAI endpoints.');
}
const openaiProvider = new OpenAIEmbeddingProvider({
model: openaiModel,
dimension: 1536,
type: 'float32',
apiKey: openaiApiKey || '', // Default to empty string
baseUrl: finalBaseUrl
});
registerEmbeddingProvider(openaiProvider);
result.push(openaiProvider);
log.info(`Created OpenAI provider on-demand: ${openaiModel} at ${finalBaseUrl}`);
}
// Create Voyage provider if API key is configured
const voyageApiKey = await options.getOption('voyageApiKey' as any);
if (voyageApiKey) {
const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
const voyageBaseUrl = 'https://api.voyageai.com/v1';
const voyageProvider = new VoyageEmbeddingProvider({
model: voyageModel,
dimension: 1024,
type: 'float32',
apiKey: voyageApiKey,
baseUrl: voyageBaseUrl
});
registerEmbeddingProvider(voyageProvider);
result.push(voyageProvider);
log.info(`Created Voyage provider on-demand: ${voyageModel}`);
}
// Always include local provider as fallback
if (!providers.has('local')) {
const localProvider = new SimpleLocalEmbeddingProvider({
model: 'local',
dimension: 384,
type: 'float32'
});
registerEmbeddingProvider(localProvider);
result.push(localProvider);
log.info(`Created local provider on-demand as fallback`);
} else {
result.push(providers.get('local')!);
}
} catch (error: any) {
log.error(`Error creating providers from current options: ${error.message || 'Unknown error'}`);
} }
return result; // Create and validate provider on-demand
try {
let provider: EmbeddingProvider | null = null;
switch (providerName) {
case 'ollama': {
const baseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
if (!baseUrl) return null;
const model = await options.getOption('ollamaEmbeddingModel');
provider = new OllamaEmbeddingProvider({
model,
dimension: 768,
type: 'float32',
baseUrl
});
// Validate by initializing (if provider supports it)
if ('initialize' in provider && typeof provider.initialize === 'function') {
await provider.initialize();
}
break;
}
case 'openai': {
const apiKey = await options.getOption('openaiApiKey');
const baseUrl = await options.getOption('openaiBaseUrl');
if (!apiKey && !baseUrl) return null;
const model = await options.getOption('openaiEmbeddingModel');
provider = new OpenAIEmbeddingProvider({
model,
dimension: 1536,
type: 'float32',
apiKey: apiKey || '',
baseUrl: baseUrl || 'https://api.openai.com/v1'
});
if (!apiKey) {
log.info('OpenAI embedding provider created without API key for compatible endpoints');
}
break;
}
case 'voyage': {
const apiKey = await options.getOption('voyageApiKey' as any);
if (!apiKey) return null;
const model = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
provider = new VoyageEmbeddingProvider({
model,
dimension: 1024,
type: 'float32',
apiKey,
baseUrl: 'https://api.voyageai.com/v1'
});
break;
}
case 'local': {
provider = new SimpleLocalEmbeddingProvider({
model: 'local',
dimension: 384,
type: 'float32'
});
break;
}
default:
return null;
}
if (provider) {
registerEmbeddingProvider(provider);
log.info(`Created and validated ${providerName} embedding provider`);
return provider;
}
} catch (error: any) {
log.error(`Failed to create ${providerName} embedding provider: ${error.message || 'Unknown error'}`);
}
return null;
} }
/** /**
* Get all enabled embedding providers * Get all enabled embedding providers for the specified feature
*/ */
export async function getEnabledEmbeddingProviders(): Promise<EmbeddingProvider[]> { export async function getEnabledEmbeddingProviders(feature: 'embeddings' | 'chat' = 'embeddings'): Promise<EmbeddingProvider[]> {
if (!(await options.getOptionBool('aiEnabled'))) { if (!(await options.getOptionBool('aiEnabled'))) {
return []; return [];
} }
// First try to get existing registered providers const result: EmbeddingProvider[] = [];
const existingProviders = Array.from(providers.values());
// If no providers are registered, create them on-demand from current options // Get the selected provider for the feature
if (existingProviders.length === 0) { const selectedProvider = feature === 'embeddings'
log.info('No providers registered, creating from current options'); ? await options.getOption('embeddingSelectedProvider')
return await createProvidersFromCurrentOptions(); : await options.getOption('aiSelectedProvider');
// Try to get or create the specific selected provider
const provider = await getOrCreateEmbeddingProvider(selectedProvider);
if (!provider) {
throw new Error(`Failed to create selected embedding provider: ${selectedProvider}. Please check your configuration.`);
}
result.push(provider);
// Always ensure local provider as fallback
const localProvider = await getOrCreateEmbeddingProvider('local');
if (localProvider && !result.some(p => p.name === 'local')) {
result.push(localProvider);
} }
return existingProviders; return result;
} }
/** /**
@ -342,7 +353,7 @@ export default {
getEmbeddingProviders, getEmbeddingProviders,
getEmbeddingProvider, getEmbeddingProvider,
getEnabledEmbeddingProviders, getEnabledEmbeddingProviders,
createProvidersFromCurrentOptions, getOrCreateEmbeddingProvider,
createEmbeddingProviderConfig, createEmbeddingProviderConfig,
updateEmbeddingProviderConfig, updateEmbeddingProviderConfig,
deleteEmbeddingProviderConfig, deleteEmbeddingProviderConfig,