Merge pull request #663 from coobabm/codex/claude-native-tool-fix-push

Fix Claude native tool names for Claude Code
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-27 06:14:12 -03:00
committed by GitHub
3 changed files with 125 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ import {
getModelUpstreamExtraHeaders,
} from "@/lib/localDb";
import { getExecutor } from "../executors/index.ts";
import { CLAUDE_OAUTH_TOOL_PREFIX } from "../translator/request/openai-to-claude.ts";
import {
parseCodexQuotaHeaders,
getCodexResetTime,
@@ -84,6 +85,51 @@ export function shouldUseNativeCodexPassthrough({
return segments.includes("responses");
}
function buildClaudePassthroughToolNameMap(body: Record<string, unknown> | null | undefined) {
if (!body || !Array.isArray(body.tools)) return null;
const toolNameMap = new Map<string, string>();
for (const tool of body.tools) {
const toolRecord = tool as Record<string, unknown>;
const toolData =
toolRecord?.type === "function" &&
toolRecord.function &&
typeof toolRecord.function === "object"
? (toolRecord.function as Record<string, unknown>)
: toolRecord;
const originalName = typeof toolData?.name === "string" ? toolData.name.trim() : "";
if (!originalName) continue;
toolNameMap.set(`${CLAUDE_OAUTH_TOOL_PREFIX}${originalName}`, originalName);
}
return toolNameMap.size > 0 ? toolNameMap : null;
}
function restoreClaudePassthroughToolNames(
responseBody: Record<string, unknown>,
toolNameMap: Map<string, string> | null
) {
if (!toolNameMap || !Array.isArray(responseBody?.content)) return responseBody;
let changed = false;
const content = responseBody.content.map((block: Record<string, unknown>) => {
if (block?.type !== "tool_use" || typeof block?.name !== "string") return block;
const restoredName = toolNameMap.get(block.name) ?? block.name;
if (restoredName === block.name) return block;
changed = true;
return {
...block,
name: restoredName,
};
});
if (!changed) return responseBody;
return {
...responseBody,
content,
};
}
/**
* Core chat handler - shared between SSE and Worker
* Returns { success, response, status, error } for caller to handle fallback
@@ -415,6 +461,7 @@ export async function handleChatCore({
FORMATS.OPENAI,
FORMATS.CLAUDE,
model,
{ ...translatedBody, _disableToolPrefix: true },
translatedBody,
stream,
credentials,
@@ -566,7 +613,14 @@ export async function handleChatCore({
}
// Extract toolNameMap for response translation (Claude OAuth)
const toolNameMap = translatedBody._toolNameMap;
const translatedToolNameMap = translatedBody._toolNameMap;
const nativeClaudeToolNameMap = isClaudePassthrough
? buildClaudePassthroughToolNameMap(body)
: null;
const toolNameMap =
translatedToolNameMap instanceof Map && translatedToolNameMap.size > 0
? translatedToolNameMap
: nativeClaudeToolNameMap;
delete translatedBody._toolNameMap;
delete translatedBody._disableToolPrefix;
@@ -1014,6 +1068,10 @@ export async function handleChatCore({
}
}
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) {
responseBody = restoreClaudePassthroughToolNames(responseBody, toolNameMap);
}
// Notify success - caller can clear error status if needed
if (onRequestSuccess) {
await onRequestSuccess();
@@ -1235,6 +1293,7 @@ export async function handleChatCore({
transformStream = createPassthroughStreamWithLogger(
provider,
reqLogger,
toolNameMap,
model,
connectionId,
body,

View File

@@ -72,6 +72,22 @@ function getOpenAIIntermediateChunks(value: unknown): unknown[] {
return Array.isArray(candidate) ? candidate : [];
}
function restoreClaudePassthroughToolUseName(parsed: JsonRecord, toolNameMap: unknown): boolean {
if (!(toolNameMap instanceof Map)) return false;
if (!parsed || typeof parsed !== "object") return false;
const block =
parsed.content_block && typeof parsed.content_block === "object"
? (parsed.content_block as JsonRecord)
: null;
if (!block || block.type !== "tool_use" || typeof block.name !== "string") return false;
const restoredName = toolNameMap.get(block.name) ?? block.name;
if (restoredName === block.name) return false;
block.name = restoredName;
return true;
}
// Note: TextDecoder/TextEncoder are created per-stream inside createSSEStream()
// to avoid shared state issues with concurrent streams (TextDecoder with {stream:true}
// maintains internal buffering state between decode() calls).
@@ -248,6 +264,7 @@ export function createSSEStream(options: StreamOptions = {}) {
if (eu.cache_creation_input_tokens)
u.cache_creation_input_tokens = eu.cache_creation_input_tokens;
}
const restoredToolName = restoreClaudePassthroughToolUseName(parsed, toolNameMap);
// Track content length and accumulate from Claude format
if (parsed.delta?.text) {
totalContentLength += parsed.delta.text.length;
@@ -257,6 +274,11 @@ export function createSSEStream(options: StreamOptions = {}) {
totalContentLength += parsed.delta.thinking.length;
passthroughAccumulatedContent += parsed.delta.thinking;
}
if (restoredToolName) {
output = `data: ${JSON.stringify(parsed)}
`;
injectedUsage = true;
}
} else {
// Chat Completions: full sanitization pipeline
parsed = sanitizeStreamingChunk(parsed);
@@ -771,6 +793,7 @@ export function createSSETransformStreamWithLogger(
export function createPassthroughStreamWithLogger(
provider: string | null = null,
reqLogger: StreamLogger | null = null,
toolNameMap: unknown = null,
model: string | null = null,
connectionId: string | null = null,
body: unknown = null,
@@ -781,6 +804,7 @@ export function createPassthroughStreamWithLogger(
mode: STREAM_MODE.PASSTHROUGH,
provider,
reqLogger,
toolNameMap,
model,
connectionId,
apiKeyInfo,

View File

@@ -0,0 +1,41 @@
import test from "node:test";
import assert from "node:assert/strict";
const { translateRequest } = await import("../../open-sse/translator/index.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
test("Claude native passthrough normalization keeps original tool names", () => {
const body = {
model: "claude-sonnet-4-6",
max_tokens: 64,
messages: [
{
role: "user",
content: [{ type: "text", text: "Write a todo item" }],
},
],
tools: [
{
name: "TodoWrite",
description: "Create or update a todo list",
input_schema: {
type: "object",
properties: {
todos: {
type: "array",
items: { type: "string" },
},
},
required: ["todos"],
},
},
],
};
const openaiBody = translateRequest(
FORMATS.CLAUDE,
FORMATS.OPENAI,
body.model,
structuredClone(body),
false,
null,