fix: strip proxy_ prefix in non-streaming Claude responses & fix LongCat validation (#605, #592) (#607)

- 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)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-25 08:16:46 -03:00
committed by GitHub
parent e9ae50be0c
commit 33fee5dcc5
4 changed files with 49 additions and 6 deletions

View File

@@ -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<string, string> | null
)
: responseBody;
// T26: Strip markdown code blocks if provider format is Claude

View File

@@ -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<prefixedName, originalName> for Claude OAuth tool name stripping
*/
export function translateNonStreamingResponse(
responseBody: unknown,
targetFormat: string,
sourceFormat: string
sourceFormat: string,
toolNameMap?: Map<string, string> | 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 || {}),
},
});

View File

@@ -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<string, unknown> =
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,
};
});

View File

@@ -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]) => [