import type { EmbeddingErrorCode, EmbeddingSafeError } from '../../shared/embedding-contracts' const MAX_SAFE_MESSAGE_LENGTH = 500 const descriptors: Record< EmbeddingErrorCode, Omit > = { model_not_found: { message: '未找到指定的向量模型。', retryable: false, remedy: '请确认模型名称正确,并确认该模型已在服务端启用。' }, authentication: { message: '向量服务身份验证失败。', retryable: false, remedy: '请检查访问密钥是否有效以及是否具备调用向量模型的权限。' }, rate_limited: { message: '向量服务当前请求过多。', retryable: true, remedy: '请稍后重试,或检查服务配额与速率限制。' }, timeout: { message: '向量服务响应超时。', retryable: true, remedy: '请检查网络和服务状态,然后重试。' }, network: { message: '无法连接到向量服务。', retryable: true, remedy: '请检查服务地址、网络连接和代理设置。' }, provider_unavailable: { message: '向量服务暂时不可用。', retryable: true, remedy: '请稍后重试并检查服务运行状态。' }, invalid_configuration: { message: '向量模型配置无效。', retryable: false, remedy: '请检查服务地址、模型名称和配置参数。' }, invalid_response: { message: '向量服务返回了无效结果。', retryable: false, remedy: '请确认服务兼容 OpenAI 向量接口并返回有效向量。' }, cancelled: { message: '向量操作已取消。', retryable: true }, unknown: { message: '向量操作失败。', retryable: false, remedy: '请检查向量服务配置后重试。' } } function errorText(error: unknown): string { if (error instanceof Error) { return `${error.name} ${error.message}`.toLowerCase() } return typeof error === 'string' ? error.toLowerCase() : '' } function numericStatus(error: unknown): number | undefined { if (typeof error !== 'object' || error === null) { return undefined } for (const key of ['status', 'statusCode', 'code'] as const) { const value = Reflect.get(error, key) if (typeof value === 'number' && Number.isInteger(value)) { return value } if ( typeof value === 'string' && /^\d{3}$/u.test(value) && Number.isInteger(Number(value)) ) { return Number(value) } } return undefined } function statusFromText(text: string): number | undefined { const match = /\b(?:http|status(?: code)?)\s*[:=]?\s*(\d{3})\b/iu.exec( text ) return match?.[1] ? Number(match[1]) : undefined } function hasAny(text: string, patterns: readonly string[]): boolean { return patterns.some((pattern) => text.includes(pattern)) } function classifyCode( error: unknown, options: { cancelled?: boolean; timedOut?: boolean } ): EmbeddingErrorCode { const text = errorText(error) const status = numericStatus(error) ?? statusFromText(text) if (error instanceof Error && error.name === 'TimeoutError') { return 'timeout' } if (error instanceof Error && error.name === 'AbortError') { return 'cancelled' } if (options.timedOut) { return 'timeout' } if ( options.cancelled || hasAny(text, ['aborterror', 'aborted', 'cancelled', 'canceled']) ) { return 'cancelled' } if (hasAny(text, ['timeout', 'timed out', 'etimedout'])) { return 'timeout' } if ( status === 401 || status === 403 || hasAny(text, [ 'unauthorized', 'forbidden', 'authentication', 'invalid api key', 'incorrect api key' ]) ) { return 'authentication' } if ( status === 404 || hasAny(text, [ 'model not found', 'model_not_found', 'unknown model', 'does not exist' ]) ) { return 'model_not_found' } if ( status === 429 || hasAny(text, ['rate limit', 'rate_limit', 'too many requests', 'quota']) ) { return 'rate_limited' } if ( status === 408 || status === 504 ) { return 'timeout' } if ( status !== undefined && status >= 500 && status <= 599 ) { return 'provider_unavailable' } if ( hasAny(text, [ 'econnrefused', 'econnreset', 'enotfound', 'fetch failed', 'network', 'failed to fetch', 'socket' ]) ) { return 'network' } if ( error instanceof RangeError || hasAny(text, [ 'endpoint must', 'model must', 'invalid endpoint', 'invalid configuration' ]) ) { return 'invalid_configuration' } if ( error instanceof TypeError || hasAny(text, [ 'invalid dimensions', 'invalid result', 'invalid indexes', 'inconsistent dimensions', 'finite numbers', 'valid json', 'invalid response' ]) ) { return 'invalid_response' } return 'unknown' } /** * Converts provider and transport failures into a bounded, user-safe error. * Raw provider response bodies, endpoints, keys and nested causes are never * copied into the returned value. */ export function classifyEmbeddingError( error: unknown, options: { cancelled?: boolean; timedOut?: boolean } = {} ): EmbeddingSafeError { const code = classifyCode(error, options) const descriptor = descriptors[code] return { code, message: descriptor.message.slice(0, MAX_SAFE_MESSAGE_LENGTH), retryable: descriptor.retryable, ...(descriptor.remedy ? { remedy: descriptor.remedy.slice(0, MAX_SAFE_MESSAGE_LENGTH) } : {}) } } export class EmbeddingOperationError extends Error { readonly code: EmbeddingErrorCode readonly retryable: boolean readonly remedy?: string constructor(error: EmbeddingSafeError) { super(error.message) this.name = 'EmbeddingOperationError' this.code = error.code this.retryable = error.retryable this.remedy = error.remedy } toSafeError(): EmbeddingSafeError { return { code: this.code, message: this.message, retryable: this.retryable, ...(this.remedy ? { remedy: this.remedy } : {}) } } } export function toEmbeddingOperationError( error: unknown, options?: { cancelled?: boolean; timedOut?: boolean } ): EmbeddingOperationError { return error instanceof EmbeddingOperationError ? error : new EmbeddingOperationError(classifyEmbeddingError(error, options)) }