From ed146fcf074c5972a82ceda35fe47e5784caf275 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 25 Mar 2026 08:11:35 -0300 Subject: [PATCH] fix: strip proxy_ prefix in non-streaming Claude responses & fix LongCat validation (#605, #592) - fix(translator): pass toolNameMap to translateNonStreamingResponse so Claude OAuth proxy_ prefix is correctly stripped from tool_use block names in non-streaming responses (was only stripped in streaming path) - fix(validation): add LongCat specialty validator that probes /chat/completions directly, bypassing the /v1/models endpoint that LongCat does not expose (#592) --- open-sse/handlers/chatCore.ts | 11 ++++++++-- open-sse/handlers/responseTranslator.ts | 11 ++++++++-- .../translator/request/openai-to-claude.ts | 12 +++++++++-- src/lib/providers/validation.ts | 21 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 6551c636d6..1da929b673 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -223,7 +223,8 @@ export async function handleChatCore({ const endpointPath = String(clientRawRequest?.endpoint || ""); const sourceFormat = detectFormatFromEndpoint(body, endpointPath); - const isResponsesEndpoint = /\/responses(?=\/|$)/i.test(endpointPath) || /^responses(?=\/|$)/i.test(endpointPath); + const isResponsesEndpoint = + /\/responses(?=\/|$)/i.test(endpointPath) || /^responses(?=\/|$)/i.test(endpointPath); const nativeCodexPassthrough = shouldUseNativeCodexPassthrough({ provider, sourceFormat, @@ -1067,8 +1068,14 @@ export async function handleChatCore({ } // Translate response to client's expected format (usually OpenAI) + // Pass toolNameMap so Claude OAuth proxy_ prefix is stripped in tool_use blocks (#605) let translatedResponse = needsTranslation(targetFormat, sourceFormat) - ? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) + ? translateNonStreamingResponse( + responseBody, + targetFormat, + sourceFormat, + toolNameMap as Map | null + ) : responseBody; // T26: Strip markdown code blocks if provider format is Claude diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index b4fe447250..698be1e48b 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -68,11 +68,14 @@ function findBestMessageText(output: unknown[]): { /** * Translate non-streaming response to OpenAI format * Handles different provider response formats (Gemini, Claude, etc.) + * + * @param toolNameMap - Optional Map for Claude OAuth tool name stripping */ export function translateNonStreamingResponse( responseBody: unknown, targetFormat: string, - sourceFormat: string + sourceFormat: string, + toolNameMap?: Map | null ): unknown { // If already in source format (usually OpenAI), return as-is if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) { @@ -334,11 +337,15 @@ export function translateNonStreamingResponse( } else if (blockObj.type === "thinking") { thinkingContent += toString(blockObj.thinking); } else if (blockObj.type === "tool_use") { + // Strip Claude OAuth tool name prefix (proxy_) using the map from request translation. + // Fallback to raw name if block wasn't prefixed (disableToolPrefix path). + const rawName = toString(blockObj.name); + const strippedName = toolNameMap?.get(rawName) ?? rawName; toolCalls.push({ id: toString(blockObj.id, `call_${Date.now()}_${toolCalls.length}`), type: "function", function: { - name: toString(blockObj.name), + name: strippedName, arguments: JSON.stringify(blockObj.input || {}), }, }); diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index 14231ad544..8d8d1a3160 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -259,11 +259,19 @@ export function openaiToClaudeRequest(model, body, stream) { toolNameMap.set(toolName, originalName); } + // Normalize input_schema: Anthropic requires `properties` when type is "object" (#595). + // MCP tools (e.g. pencil, computer_use) may omit properties on object-type schemas. + const rawSchema: Record = + toolData.parameters || toolData.input_schema || { type: "object", properties: {}, required: [] }; + const normalizedSchema = + rawSchema.type === "object" && !rawSchema.properties + ? { ...rawSchema, properties: {} } + : rawSchema; + return { name: toolName, description: toolData.description || "", - input_schema: toolData.parameters || - toolData.input_schema || { type: "object", properties: {}, required: [] }, + input_schema: normalizedSchema, }; }); diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index f5ca56e168..a0a1b3218e 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -652,6 +652,27 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi elevenlabs: validateElevenLabsProvider, inworld: validateInworldProvider, "bailian-coding-plan": validateBailianCodingPlanProvider, + // LongCat AI — does not expose /v1/models; validate via chat completions directly (#592) + longcat: async ({ apiKey }: any) => { + try { + const res = await fetch("https://longcat.chat/api/v1/chat/completions", { + method: "POST", + headers: buildBearerHeaders(apiKey), + body: JSON.stringify({ + model: "longcat", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + // Any non-auth response (200, 400, 422) means auth passed + return { valid: true, error: null }; + } catch (error: any) { + return { valid: false, error: error.message || "Connection failed" }; + } + }, // Search providers — use factored validator ...Object.fromEntries( Object.entries(SEARCH_VALIDATOR_CONFIGS).map(([id, configFn]) => [