diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 9ce310a7..5b4d3a67 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -154,11 +154,7 @@ export class AIService { let data: Record; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal) as Record; - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal) as Record; - } + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal) as Record; } else { const url = buildFinalApiUrl(this.config.baseUrl, apiType); const response = await fetch(url, { @@ -216,11 +212,7 @@ export class AIService { let data: unknown; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal); - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); - } + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal); } else { const url = buildApiUrl(this.config.baseUrl, 'v1/messages'); const response = await fetch(url, { @@ -276,11 +268,7 @@ ${options.user}` : options.user; let data: unknown; if (backend.isAvailable) { - if (this.config.id) { - data = await backend.proxyAIRequest(this.config.id, requestBody, options.signal); - } else { - data = await backend.proxyAIRequestWithConfig(this.config, requestBody, options.signal); - } + data = await backend.proxyAIRequestWithFallback(this.config.id, this.config, requestBody, options.signal); } else { const path = `v1beta/models/${encodeURIComponent(model)}:generateContent`; const urlObj = new URL(buildApiUrl(this.config.baseUrl, path)); diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts index 5a9829a1..61df6b84 100644 --- a/src/services/backendAdapter.ts +++ b/src/services/backendAdapter.ts @@ -136,7 +136,10 @@ class BackendAdapter { } } catch { /* body not JSON */ } const translated = translateBackendError(code, `${fallbackPrefix}: ${res.status}`); - throw new Error(detail ? `${translated} - ${detail}` : translated); + const error = new Error(detail ? `${translated} - ${detail}` : translated) as Error & { statusCode?: number; code?: string }; + error.statusCode = res.status; + if (code) error.code = code; + throw error; } // === GitHub Proxy === @@ -252,6 +255,32 @@ class BackendAdapter { return res.json(); } + async proxyAIRequestWithFallback(configId: string, aiConfig: { apiType?: string; baseUrl: string; apiKey: string; model: string; reasoningEffort?: string }, body: object, signal?: AbortSignal): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + // Try configId lookup first to avoid sending API key inline + if (configId) { + try { + const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/ai`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ configId, body }), + signal, + }, 120000); + if (res.ok) return res.json(); + // Fall through to inline config on 404 (config not synced yet) + if (res.status !== 404) await this.throwTranslatedError(res, 'AI proxy error'); + } catch (err) { + // Rethrow non-404 errors; fall through to inline config on config-not-found + const e = err as Error & { statusCode?: number; code?: string }; + if (e.statusCode !== 404 && e.code !== 'AI_CONFIG_NOT_FOUND') throw err; + } + } + + // Fallback: send full config inline + return this.proxyAIRequestWithConfig(aiConfig, body, signal); + } + // === WebDAV Proxy === async proxyWebDAV(configId: string, method: string, path: string, body?: string, headers?: Record): Promise {