fix: preserve Claude Code messages on direct routes (#2351)

Integrated into release/v3.8.0 — Claude Code semantic passthrough for /v1/messages payloads
This commit is contained in:
terence71-glitch
2026-05-18 02:09:02 +02:00
committed by GitHub
parent 9b8dc4d6db
commit db8a2dc735
3 changed files with 306 additions and 8 deletions

View File

@@ -422,6 +422,36 @@ export function shouldUseNativeCodexPassthrough({
return segments.includes("responses");
}
export function isClaudeCodeSemanticPassthroughRequest({
provider,
sourceFormat,
targetFormat,
headers,
userAgent,
}: {
provider?: string | null;
sourceFormat?: string | null;
targetFormat?: string | null;
headers?: Record<string, unknown> | Headers | null;
userAgent?: string | null;
}): boolean {
const isDirectClaudeCodeProvider =
provider === "claude" || isClaudeCodeCompatibleProvider(provider);
if (!isDirectClaudeCodeProvider) return false;
if (sourceFormat !== FORMATS.CLAUDE) return false;
if (targetFormat !== FORMATS.CLAUDE) return false;
const headerUserAgent = getHeaderValueCaseInsensitive(headers, "user-agent");
const ua = `${userAgent || ""} ${headerUserAgent || ""}`.toLowerCase();
if (ua.includes("claude-code") || ua.includes("claude-cli")) return true;
const appHeader = getHeaderValueCaseInsensitive(headers, "x-app");
if (typeof appHeader === "string" && appHeader.trim().toLowerCase() === "cli") return true;
const sessionId = getHeaderValueCaseInsensitive(headers, "x-claude-code-session-id");
return typeof sessionId === "string" && sessionId.trim().length > 0;
}
function buildClaudePassthroughToolNameMap(body: Record<string, unknown> | null | undefined) {
if (!body || !Array.isArray(body.tools)) return null;
@@ -2408,6 +2438,13 @@ export async function handleChatCore({
let translatedBody = body;
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
const isClaudeCodeCompatible = isClaudeCodeCompatibleProvider(provider);
const isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
provider,
sourceFormat,
targetFormat,
headers: clientRawRequest?.headers,
userAgent,
});
const upstreamStream = stream || isClaudeCodeCompatible;
let ccSessionId: string | null = null;
const stripTypes = getStripTypesForProviderModel(provider || "", model || "");
@@ -2568,7 +2605,9 @@ export async function handleChatCore({
// Claude Code-compatible providers expect Anthropic Messages-shaped payloads,
// but we extract only role/text/max_tokens/effort from an OpenAI-like view first.
if (sourceFormat !== FORMATS.OPENAI) {
if (sourceFormat === FORMATS.CLAUDE && isClaudeCodeSemanticPassthrough) {
log?.debug?.("FORMAT", "claude-code semantic passthrough enabled for compatible bridge");
} else if (sourceFormat !== FORMATS.OPENAI) {
const normalizeToolCallId = getModelNormalizeToolCallId(
provider || "",
model || "",
@@ -2603,6 +2642,7 @@ export async function handleChatCore({
cwd: process.cwd(),
now: new Date(),
preserveCacheControl,
preserveClaudeMessages: sourceFormat === FORMATS.CLAUDE && isClaudeCodeSemanticPassthrough,
});
log?.debug?.("FORMAT", "claude-code-compatible bridge enabled");
} else if (isClaudePassthrough) {
@@ -2613,7 +2653,11 @@ export async function handleChatCore({
// regardless of combo strategy or cache_control settings.
translatedBody = { ...body };
translatedBody._disableToolPrefix = true;
normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true });
if (!isClaudeCodeSemanticPassthrough) {
normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true });
} else {
log?.debug?.("FORMAT", "claude-code semantic passthrough enabled");
}
log?.debug?.("FORMAT", `claude passthrough (preserveCache=${preserveCacheControl})`);

View File

@@ -79,6 +79,7 @@ type BuildRequestOptions = {
now?: Date;
sessionId?: string | null;
preserveCacheControl?: boolean;
preserveClaudeMessages?: boolean;
};
function supportsClaudeXHighEffort(model: string | null | undefined): boolean {
@@ -228,10 +229,13 @@ export function buildClaudeCodeCompatibleRequest({
cwd = process.cwd(),
sessionId,
preserveCacheControl = false,
preserveClaudeMessages = false,
}: BuildRequestOptions) {
const normalized = normalizedBody || {};
const preparedClaudeBody = claudeBody
? prepareClaudeCodeCompatibleBody(claudeBody, preserveCacheControl)
? preserveClaudeMessages
? prepareClaudeCodeCompatibleSemanticBody(claudeBody)
: prepareClaudeCodeCompatibleBody(claudeBody, preserveCacheControl)
: null;
const normalizedMessages = Array.isArray(normalized.messages)
? (normalized.messages as MessageLike[])
@@ -242,13 +246,18 @@ export function buildClaudeCodeCompatibleRequest({
: null;
const effectiveClaudeBody = preparedClaudeBody || extractedClaudeBody;
const messages = effectiveClaudeBody
? buildClaudeCodeCompatibleMessagesFromClaude(
effectiveClaudeBody.messages as MessageLike[],
preserveCacheControl
)
? preserveClaudeMessages && preparedClaudeBody
? cloneClaudeCodeCompatibleMessagesFromClaude(
effectiveClaudeBody.messages as MessageLike[],
preserveCacheControl
)
: buildClaudeCodeCompatibleMessagesFromClaude(
effectiveClaudeBody.messages as MessageLike[],
preserveCacheControl
)
: buildClaudeCodeCompatibleMessages(normalizedMessages);
const system = buildClaudeCodeCompatibleSystemBlocks({
messages: normalizedMessages,
messages: preserveClaudeMessages ? [] : normalizedMessages,
systemBlocks: effectiveClaudeBody?.system as Record<string, unknown>[] | undefined,
preserveCacheControl,
});
@@ -616,6 +625,25 @@ function buildClaudeCodeCompatibleMessagesFromClaude(
return merged;
}
function cloneClaudeCodeCompatibleMessagesFromClaude(
messages: MessageLike[] | undefined,
preserveCacheControl: boolean
) {
const cloned = Array.isArray(messages)
? messages.map((message) => cloneValue(message) as MessageLike)
: [];
if (!preserveCacheControl) {
for (const message of cloned) {
if (Array.isArray(message.content)) {
stripCacheControlFromContentBlocks(message.content as Array<Record<string, unknown>>);
}
}
}
return cloned;
}
function buildClaudeCodeCompatibleSystemBlocks({
messages,
systemBlocks,
@@ -794,6 +822,28 @@ function prepareClaudeCodeCompatibleBody(
return readRecord(prepared);
}
function prepareClaudeCodeCompatibleSemanticBody(claudeBody: Record<string, unknown>) {
const prepared: Record<string, unknown> = {
system: normalizeClaudeSystemInput(claudeBody.system),
messages: Array.isArray(claudeBody.messages)
? (cloneValue(claudeBody.messages) as Array<Record<string, unknown>>)
: [],
tools: normalizeClaudeToolInput(claudeBody.tools),
thinking: (readRecord(cloneValue(claudeBody.thinking)) || null) as Record<
string,
unknown
> | null,
};
const metadata = readRecord(cloneValue(claudeBody.metadata));
if (metadata) prepared.metadata = metadata;
const outputConfig = readRecord(cloneValue(claudeBody.output_config));
if (outputConfig) prepared.output_config = outputConfig;
return prepared;
}
function extractClaudeBodyFromSource(
sourceBody: Record<string, unknown>,
preserveCacheControl: boolean

View File

@@ -37,6 +37,7 @@ const { getCallLogs, getCallLogById } = await import("../../src/lib/usage/callLo
const {
handleChatCore,
shouldUseNativeCodexPassthrough,
isClaudeCodeSemanticPassthroughRequest,
isTokenExpiringSoon,
clearUpstreamProxyConfigCache,
buildStreamingResponseHeaders,
@@ -488,6 +489,46 @@ test("chatCore helper exports detect responses passthrough paths and token expir
assert.equal(isTokenExpiringSoon(null), false);
});
test("chatCore helper detects Claude Code semantic passthrough only for direct Claude-Code routes", () => {
assert.equal(
isClaudeCodeSemanticPassthroughRequest({
provider: "claude",
sourceFormat: FORMATS.CLAUDE,
targetFormat: FORMATS.CLAUDE,
userAgent: "claude-cli/2.1.137",
}),
true
);
assert.equal(
isClaudeCodeSemanticPassthroughRequest({
provider: "anthropic-compatible-cc-test",
sourceFormat: FORMATS.CLAUDE,
targetFormat: FORMATS.CLAUDE,
headers: new Headers({ "x-app": "cli" }),
userAgent: "unit-test",
}),
true
);
assert.equal(
isClaudeCodeSemanticPassthroughRequest({
provider: "anthropic-compatible-test",
sourceFormat: FORMATS.CLAUDE,
targetFormat: FORMATS.CLAUDE,
userAgent: "claude-cli/2.1.137",
}),
false
);
assert.equal(
isClaudeCodeSemanticPassthroughRequest({
provider: "claude",
sourceFormat: FORMATS.CLAUDE,
targetFormat: FORMATS.CLAUDE,
userAgent: "generic-client",
}),
false
);
});
test("chatCore applies payload rules after translating Responses input into Chat payloads", async () => {
setPayloadRulesConfig({
default: [
@@ -565,6 +606,169 @@ test("chatCore builds Claude Code-compatible upstream requests for CC providers"
assert.equal(call.body.messages[0].content[0].text, "Ping");
});
test("chatCore preserves native Claude Code messages for native Claude OAuth passthrough", async () => {
const clientMessages = [
{
role: "system",
content: [{ type: "text", text: "system-message-that-should-stay-in-messages" }],
},
{
role: "user",
content: [
{ type: "text", text: "" },
{ type: "text", text: "Run pwd", cache_control: { type: "ephemeral" } },
{ type: "document", name: "README.md", content: "Do not flatten me" },
{ type: "future_block", payload: { keep: true } },
],
},
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_pwd", name: "Bash", input: { command: "pwd" } }],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_pwd", content: "ok" }],
},
];
const { call, result } = await invokeChatCore({
provider: "claude",
model: "claude-sonnet-4-6",
endpoint: "/v1/messages",
credentials: { apiKey: "claude-key", providerSpecificData: {} },
body: {
model: "omniroute/alias-that-should-resolve",
max_tokens: 64,
system: [{ type: "text", text: "top-level-system" }],
messages: clientMessages,
tools: [{ name: "Bash", input_schema: { type: "object", properties: {} } }],
},
userAgent: "claude-cli/2.1.137",
requestHeaders: { "x-app": "cli", "x-claude-code-session-id": "session-123" },
responseFormat: "claude",
});
assert.equal(result.success, true);
assert.equal(call.body.model, "claude-sonnet-4-6");
assert.deepEqual(call.body.messages, clientMessages);
assert.equal(
call.body.system.some(
(block: { text?: string }) => block.text === "system-message-that-should-stay-in-messages"
),
false
);
assert.equal(call.body.messages[1].content[0].text, "");
assert.equal(call.body.messages[1].content[2].type, "document");
assert.equal(call.body.messages[1].content[3].type, "future_block");
assert.equal(call.body.messages[3].content[0].type, "tool_result");
});
test("chatCore keeps Claude normalization for non-Claude-Code Claude passthrough", async () => {
const { call, result } = await invokeChatCore({
provider: "claude",
model: "claude-sonnet-4-6",
endpoint: "/v1/messages",
credentials: { apiKey: "claude-key", providerSpecificData: {} },
body: {
model: "claude-sonnet-4-6",
max_tokens: 64,
messages: [
{ role: "system", content: "system role should move" },
{
role: "user",
content: [
{ type: "text", text: "" },
{ type: "text", text: "hello" },
{ type: "document", name: "README.md", content: "Read me" },
{ type: "future_block", payload: { drop: true } },
],
},
],
},
userAgent: "generic-client/1.0",
responseFormat: "claude",
});
assert.equal(result.success, true);
assert.equal(
call.body.messages.some((message) => message.role === "system"),
false
);
assert.equal(call.body.system.at(-1).text, "system role should move");
assert.deepEqual(call.body.messages[0].content, [
{ type: "text", text: "hello" },
{ type: "text", text: "[README.md]\nRead me" },
]);
});
test("chatCore preserves native Claude Code messages before CC-compatible relay transforms", async () => {
const clientMessages = [
{
role: "system",
content: [{ type: "text", text: "system-message-remains-in-source-history" }],
},
{
role: "user",
content: [
{ type: "text", text: "" },
{ type: "text", text: "Inspect project", cache_control: { type: "ephemeral" } },
{ type: "document", name: "design.md", content: "Keep as document block" },
{ type: "future_block", payload: { keep: true } },
],
},
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_read", name: "Read", input: { file_path: "a.ts" } }],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_read", content: "file contents" }],
},
];
const { call, result } = await invokeChatCore({
provider: "anthropic-compatible-cc-test",
model: "claude-sonnet-4-6",
endpoint: "/v1/messages",
credentials: {
apiKey: "sk-test",
providerSpecificData: {
baseUrl: "https://proxy.example.com/v1/messages?beta=true",
chatPath: "/v1/messages?beta=true",
},
},
body: {
model: "claude-sonnet-4-6",
max_tokens: 64,
system: [{ type: "text", text: "top-level-system" }],
messages: clientMessages,
tools: [{ name: "Read", input_schema: { type: "object", properties: {} } }],
},
userAgent: "Claude-Code/2.1.137",
requestHeaders: { "x-app": "cli", "x-claude-code-session-id": "cc-session-123" },
responseFormat: "claude",
});
assert.equal(result.success, true);
assert.match(call.url, /\/v1\/messages\?beta=true$/);
assert.equal(call.body.stream, true);
assert.deepEqual(call.body.messages, clientMessages);
assert.equal(
call.body.system[0].text,
"You are a Claude agent, built on Anthropic's Claude Agent SDK."
);
assert.equal(
call.body.system.some(
(block: { text?: string }) => block.text === "system-message-remains-in-source-history"
),
false
);
assert.equal(call.body.messages[1].content[0].text, "");
assert.equal(call.body.messages[1].content[2].type, "document");
assert.equal(call.body.messages[1].content[3].type, "future_block");
assert.equal(call.body.messages[3].content[0].type, "tool_result");
});
test("chatCore preserves cache_control automatically for Claude Code single-model requests", async () => {
await settingsDb.updateSettings({ alwaysPreserveClientCache: "auto" });
invalidateCacheControlSettingsCache();