import { logger } from '@services/libs/log'; import { AiAPIConfig } from '@services/agentInstance/promptConcat/promptConcatSchema'; import { AuthenticationError, MissingAPIKeyError, MissingBaseURLError } from './errors'; import type { AIEmbeddingResponse, AIProviderConfig } from './interface'; interface EmbeddingAPIResponse { data?: Array<{ embedding: number[] }>; object?: string; usage?: { prompt_tokens: number; total_tokens: number; }; } interface EmbeddingOptions { /** Dimensions for the embedding (supported by some providers) */ dimensions?: number; /** Encoding format for the embedding */ encoding_format?: 'float' | 'base64'; } /** * Generate embeddings from an AI provider */ export async function generateEmbeddingsFromProvider( inputs: string[], config: AiAPIConfig, signal: AbortSignal, providerConfig?: AIProviderConfig, options: EmbeddingOptions = {}, ): Promise { const provider = config.api.provider; const model = config.api.model; logger.info(`Using AI embedding provider: ${provider}, model: ${model}`); try { // Check if API key is required const isOllama = providerConfig?.providerClass === 'ollama'; const isLocalOpenAICompatible = providerConfig?.providerClass === 'openAICompatible' && providerConfig?.baseURL && (providerConfig.baseURL.includes('localhost') || providerConfig.baseURL.includes('127.0.0.1')); if (!providerConfig?.apiKey && !isOllama && !isLocalOpenAICompatible) { throw new MissingAPIKeyError(provider); } // Get base URL and prepare headers let baseURL = providerConfig?.baseURL || ''; const headers: Record = { 'Content-Type': 'application/json', }; // Set up provider-specific configuration switch (providerConfig?.providerClass || provider) { case 'openai': baseURL = 'https://api.openai.com/v1'; headers['Authorization'] = `Bearer ${providerConfig?.apiKey}`; break; case 'openAICompatible': if (!providerConfig?.baseURL) { throw new MissingBaseURLError(provider); } baseURL = providerConfig.baseURL; if (providerConfig.apiKey) { headers['Authorization'] = `Bearer ${providerConfig.apiKey}`; } break; case 'deepseek': baseURL = 'https://api.deepseek.com/v1'; headers['Authorization'] = `Bearer ${providerConfig?.apiKey}`; break; case 'anthropic': throw new Error(`Anthropic provider does not support embeddings`); case 'ollama': if (!providerConfig?.baseURL) { throw new MissingBaseURLError(provider); } baseURL = providerConfig.baseURL; break; default: // For silicon flow and other openai-compatible providers if (!providerConfig?.baseURL) { throw new MissingBaseURLError(provider); } baseURL = providerConfig.baseURL; if (providerConfig.apiKey) { headers['Authorization'] = `Bearer ${providerConfig.apiKey}`; } break; } // Prepare request body const requestBody: Record = { model, input: inputs, }; // Add optional parameters based on provider support if (options.dimensions && (providerConfig?.providerClass === 'openAICompatible' || provider === 'siliconflow')) { requestBody.dimensions = options.dimensions; } if (options.encoding_format) { requestBody.encoding_format = options.encoding_format; } // Make the API call const response = await fetch(`${baseURL}/embeddings`, { method: 'POST', headers, body: JSON.stringify(requestBody), signal, }); if (!response.ok) { const errorText = await response.text(); logger.error('Embedding API error', { function: 'generateEmbeddingsFromProvider', status: response.status, errorText, }); if (response.status === 401) { throw new AuthenticationError(provider); } else if (response.status === 404) { throw new Error(`${provider} error: Model "${model}" not found`); } else if (response.status === 429) { throw new Error(`${provider} too many requests: Reduce request frequency or check API limits`); } else { throw new Error(`${provider} embedding error: ${errorText}`); } } const data = await response.json() as EmbeddingAPIResponse; // Transform the response to our standard format const embeddings = data.data?.map(item => item.embedding) || []; return { requestId: crypto.randomUUID(), embeddings, model, object: data.object || 'list', usage: data.usage, status: 'done' as const, }; } catch (error) { logger.error(`${provider} embedding error:`, error); if (error instanceof Error && error.name === 'AbortError') { throw error; } // Return error response for consistency return { requestId: crypto.randomUUID(), embeddings: [], model, object: 'error', status: 'error' as const, errorDetail: { name: error instanceof Error ? error.name : 'UnknownError', code: 'EMBEDDING_FAILED', provider, message: error instanceof Error ? error.message : String(error), }, }; } }