From fe81f37adb0287161a958b5ceacdd0505533edfb Mon Sep 17 00:00:00 2001 From: AutoAgent Date: Fri, 15 May 2026 03:52:00 +0800 Subject: [PATCH] fix: normalize non-standard errors from anthropic-format third-party APIs Third-party Anthropic-compatible APIs (e.g. step-router-v1) often return errors in non-standard shapes like: {"error":{"message":"...","type":"input_invalid"}} Previously, anthropic-format providers bypassed the proxy entirely. When these APIs returned errors, cc-haha injected a malformed AssistantMessage into conversation history, causing a cascade failure on every subsequent request (only /clear could recover). This change: 1. Routes anthropic-format providers through the proxy (passthrough mode) 2. Normalizes non-standard error responses to proper Anthropic format: {type:"error", error:{type:"...", message:"..."}} 3. Passes through successful responses unchanged Fixes the cascade failure issue reported with step-router-v1 where any error (image reading, deferred tools, unrecognized tool calls) would permanently corrupt the conversation until /clear. Co-Authored-By: Claude Opus 4.7 --- src/server/proxy/handler.ts | 128 ++++++++++++++++++++++--- src/server/services/providerService.ts | 4 +- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/src/server/proxy/handler.ts b/src/server/proxy/handler.ts index 88e00443fc..518a5d4c31 100644 --- a/src/server/proxy/handler.ts +++ b/src/server/proxy/handler.ts @@ -53,20 +53,12 @@ export async function handleProxyRequest(req: Request, url: URL): Promise { + const url = `${baseUrl}/v1/messages` + + const upstream = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify(body), + signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000), + }) + + if (!upstream.ok) { + const errText = await upstream.text().catch(() => '') + return Response.json( + normalizeAnthropicError(upstream.status, errText), + { status: upstream.status }, + ) + } + + // Pass through successful response as-is + if (isStream) { + if (!upstream.body) { + return Response.json( + { type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } }, + { status: 502 }, + ) + } + return new Response(upstream.body, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }) + } + + return new Response(upstream.body, { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) +} + +/** + * Normalize a non-standard API error response body into proper Anthropic format. + * + * Recognizes upstream error shapes: + * {"error":{"message":"...","type":"input_invalid"}} → standard format + * {"error":{"message":"..."}} → standard format + * Any other JSON or plain text → wrapped as api_error + */ +function normalizeAnthropicError(status: number, rawBody: string): { + type: string + error: { type: string; message: string } +} { + try { + const parsed = JSON.parse(rawBody) + if (parsed?.error?.message) { + const errType = parsed.error.type || 'invalid_request_error' + return { + type: 'error', + error: { + type: errType, + message: String(parsed.error.message), + }, + } + } + } catch { + // Not JSON — use raw body as message + } + + return { + type: 'error', + error: { + type: status >= 500 ? 'api_error' : 'invalid_request_error', + message: rawBody.slice(0, 500) || `HTTP ${status}`, + }, + } +} diff --git a/src/server/services/providerService.ts b/src/server/services/providerService.ts index 2d4202f08a..a6ad0b1d57 100644 --- a/src/server/services/providerService.ts +++ b/src/server/services/providerService.ts @@ -347,7 +347,7 @@ export class ProviderService { provider: SavedProvider, options?: { proxyPath?: string }, ): Record { - const needsProxy = provider.apiFormat != null && provider.apiFormat !== 'anthropic' + const needsProxy = provider.apiFormat != null const proxyPath = options?.proxyPath ?? '/proxy' const baseUrl = needsProxy ? `http://127.0.0.1:${ProviderService.serverPort}${proxyPath}` @@ -460,7 +460,7 @@ export class ProviderService { const provider = index.providers.find(p => p.id === index.activeId) if (provider) { const presetDefaultEnv = getPresetDefaultEnv(provider.presetId) - const needsProxy = provider.apiFormat != null && provider.apiFormat !== 'anthropic' + const needsProxy = provider.apiFormat != null const authEnv = buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy) if (Object.values(authEnv).some(value => value.length > 0)) { return { hasAuth: true, source: 'cc-haha-provider', activeProvider: provider.name }