mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
41
tests/unit/claude-native-passthrough-tools.test.mjs
Normal file
41
tests/unit/claude-native-passthrough-tools.test.mjs
Normal 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,
|
||||
Reference in New Issue
Block a user