From 140a3d81ed84076f7c20699a643aee117e5ed70f Mon Sep 17 00:00:00 2001 From: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Date: Thu, 21 May 2026 11:47:06 +0200 Subject: [PATCH 1/2] fix: extract system role messages in semantic passthrough path + bump CLI wire image to v2.1.146 --- .env.example | 2 +- open-sse/config/anthropicHeaders.ts | 7 ++- open-sse/executors/claudeIdentity.ts | 7 ++- open-sse/handlers/chatCore.ts | 72 ++++++++++++++--------- open-sse/services/ccBridgeTransforms.ts | 2 +- open-sse/services/claudeCodeCompatible.ts | 6 +- 6 files changed, 59 insertions(+), 37 deletions(-) diff --git a/.env.example b/.env.example index 1bda4388c7..18b4b2c5fb 100644 --- a/.env.example +++ b/.env.example @@ -600,7 +600,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98 # Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup. # Update these when providers release new CLI versions to avoid blocks. -CLAUDE_USER_AGENT="claude-cli/2.1.145 (external, cli)" +CLAUDE_USER_AGENT="claude-cli/2.1.146 (external, cli)" CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)" GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1" ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0" diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts index 7ddc7a86a3..5f708a5a80 100644 --- a/open-sse/config/anthropicHeaders.ts +++ b/open-sse/config/anthropicHeaders.ts @@ -12,6 +12,9 @@ const ANTHROPIC_BETA_BASE = Object.freeze([ "fast-mode-2026-02-01", "redact-thinking-2026-02-12", "token-efficient-tools-2026-03-28", + "advisor-tool-2026-03-01", + "extended-cache-ttl-2025-04-11", + "cache-diagnosis-2026-04-07", ]); const CLAUDE_OAUTH_EXTRA_BETAS = Object.freeze(["fine-grained-tool-streaming-2025-05-14"]); @@ -26,7 +29,7 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [ ...ANTHROPIC_BETA_BASE.slice(3), ].join(","); -export const CLAUDE_CLI_VERSION = "2.1.137"; +export const CLAUDE_CLI_VERSION = "2.1.146"; export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`; -export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.81.0"; +export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.94.0"; export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0"; diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index 30e081acd9..a2fe491ddc 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -12,9 +12,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; // ---------- Versions ------------------------------------------------------ -export const CLAUDE_CODE_VERSION = "2.1.131"; +export const CLAUDE_CODE_VERSION = "2.1.146"; /** Bundled @anthropic-ai/sdk version for the pinned CLI release. */ -export const CLAUDE_CODE_STAINLESS_VERSION = "0.81.0"; +export const CLAUDE_CODE_STAINLESS_VERSION = "0.94.0"; // ---------- Stainless OS / Arch / Runtime -------------------------------- @@ -318,7 +318,8 @@ export function selectBetaFlags(body: Record | null | undefined flags.push( "advanced-tool-use-2025-11-20", "effort-2025-11-24", - "extended-cache-ttl-2025-04-11" + "extended-cache-ttl-2025-04-11", + "cache-diagnosis-2026-04-07" ); } return flags.join(","); diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 74a059e2e3..a2c24bea2b 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2577,6 +2577,45 @@ export async function handleChatCore({ content?: unknown; }; + /** + * Lightweight extraction: only lifts role:"system" messages to the top-level + * `system` parameter. Unlike normalizeClaudeUpstreamMessages, this does NOT + * convert file/document blocks, drop unknown types, or change tool history. + * Used in the semantic passthrough path where Claude Code's native payload + * structure must be preserved — only memory injection (which prepends a + * system message) needs this correction. + */ + const extractSystemRoleMessages = (payload: Record) => { + if (!Array.isArray(payload.messages)) return; + const messages = payload.messages as ClaudeMessage[]; + const systemMessages = messages.filter((m) => m.role === "system"); + if (systemMessages.length === 0) return; + + const extraBlocks: ClaudeContentBlock[] = []; + for (const sm of systemMessages) { + if (typeof sm.content === "string" && sm.content.length > 0) { + extraBlocks.push({ type: "text", text: sm.content }); + } else if (Array.isArray(sm.content)) { + for (const block of sm.content as ClaudeContentBlock[]) { + if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { + extraBlocks.push(block); + } + } + } + } + if (extraBlocks.length > 0) { + const existingSystem = payload.system; + if (typeof existingSystem === "string" && existingSystem.length > 0) { + payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; + } else if (Array.isArray(existingSystem)) { + payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; + } else { + payload.system = extraBlocks; + } + } + payload.messages = messages.filter((m) => m.role !== "system"); + }; + const normalizeClaudeUpstreamMessages = ( payload: Record, options?: { preserveToolResultBlocks?: boolean } @@ -2586,33 +2625,8 @@ export async function handleChatCore({ let messages = payload.messages as ClaudeMessage[]; // Extract system role messages (Issue #1797) - const systemMessages = messages.filter((m) => m.role === "system"); - if (systemMessages.length > 0) { - const extraBlocks: ClaudeContentBlock[] = []; - for (const sm of systemMessages) { - if (typeof sm.content === "string" && sm.content.length > 0) { - extraBlocks.push({ type: "text", text: sm.content }); - } else if (Array.isArray(sm.content)) { - for (const block of sm.content as ClaudeContentBlock[]) { - if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { - extraBlocks.push(block); - } - } - } - } - if (extraBlocks.length > 0) { - const existingSystem = payload.system; - if (typeof existingSystem === "string" && existingSystem.length > 0) { - payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; - } else if (Array.isArray(existingSystem)) { - payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; - } else { - payload.system = extraBlocks; - } - } - messages = messages.filter((m) => m.role !== "system"); - payload.messages = messages; - } + extractSystemRoleMessages(payload); + messages = payload.messages as ClaudeMessage[]; // Anthropic rejects empty text blocks in native Messages payloads. for (const msg of messages) { @@ -2745,6 +2759,10 @@ export async function handleChatCore({ if (!isClaudeCodeSemanticPassthrough) { normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true }); } else { + // Lightweight system role extraction only — preserves all other + // Claude Code payload structure (documents, tool history, etc.) + // while correcting memory injection's role:"system" message. + extractSystemRoleMessages(translatedBody); log?.debug?.("FORMAT", "claude-code semantic passthrough enabled"); } diff --git a/open-sse/services/ccBridgeTransforms.ts b/open-sse/services/ccBridgeTransforms.ts index 065b613a33..83c5138703 100644 --- a/open-sse/services/ccBridgeTransforms.ts +++ b/open-sse/services/ccBridgeTransforms.ts @@ -114,7 +114,7 @@ export const CCH_SALT = "59cf53e54c78"; /** Character positions sampled from the first user message text. */ export const CCH_POSITIONS = [4, 7, 20] as const; /** Default `cc_version=` value embedded in the billing header. */ -export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.137"; +export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.146"; /** Identity sentinel prepended for Claude Agent SDK callers. */ export const CLAUDE_AGENT_SDK_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 6b7bd98b04..1276626921 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -39,9 +39,9 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = [ "interleaved-thinking-2025-05-14", "effort-2025-11-24", ].join(","); -export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.137"; -export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.137 (external, sdk-cli)"; -export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.81.0"; +export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.146"; +export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.146 (external, sdk-cli)"; +export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0"; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0"; export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07"; const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [ From 7e05abd398514aabbab9d9247c4538b31108e7fd Mon Sep 17 00:00:00 2001 From: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Date: Thu, 21 May 2026 12:26:07 +0200 Subject: [PATCH 2/2] fix: extract system role messages in semantic passthrough path + add test --- open-sse/handlers/chatCore.ts | 74 +++++++------- tests/unit/system-role-extraction.test.ts | 119 ++++++++++++++++++++++ 2 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 tests/unit/system-role-extraction.test.ts diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a2c24bea2b..aa918d2f8d 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1242,6 +1242,41 @@ function isCopilotClient( return false; } +export function extractSystemRoleMessages(payload: Record): void { + if (!Array.isArray(payload.messages)) return; + const messages = payload.messages as Array<{ role?: unknown; content?: unknown }>; + const systemMessages = messages.filter( + (m) => typeof m.role === "string" && m.role.toLowerCase() === "system" + ); + if (systemMessages.length === 0) return; + + const extraBlocks: Array> = []; + for (const sm of systemMessages) { + if (typeof sm.content === "string" && sm.content.length > 0) { + extraBlocks.push({ type: "text", text: sm.content }); + } else if (Array.isArray(sm.content)) { + for (const block of sm.content as Array>) { + if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { + extraBlocks.push({ ...block }); + } + } + } + } + if (extraBlocks.length > 0) { + const existingSystem = payload.system; + if (typeof existingSystem === "string" && existingSystem.length > 0) { + payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; + } else if (Array.isArray(existingSystem)) { + payload.system = [...(existingSystem as Array>), ...extraBlocks]; + } else { + payload.system = extraBlocks; + } + } + payload.messages = messages.filter( + (m) => typeof m.role !== "string" || m.role.toLowerCase() !== "system" + ); +} + export async function handleChatCore({ body, modelInfo, @@ -2577,45 +2612,6 @@ export async function handleChatCore({ content?: unknown; }; - /** - * Lightweight extraction: only lifts role:"system" messages to the top-level - * `system` parameter. Unlike normalizeClaudeUpstreamMessages, this does NOT - * convert file/document blocks, drop unknown types, or change tool history. - * Used in the semantic passthrough path where Claude Code's native payload - * structure must be preserved — only memory injection (which prepends a - * system message) needs this correction. - */ - const extractSystemRoleMessages = (payload: Record) => { - if (!Array.isArray(payload.messages)) return; - const messages = payload.messages as ClaudeMessage[]; - const systemMessages = messages.filter((m) => m.role === "system"); - if (systemMessages.length === 0) return; - - const extraBlocks: ClaudeContentBlock[] = []; - for (const sm of systemMessages) { - if (typeof sm.content === "string" && sm.content.length > 0) { - extraBlocks.push({ type: "text", text: sm.content }); - } else if (Array.isArray(sm.content)) { - for (const block of sm.content as ClaudeContentBlock[]) { - if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { - extraBlocks.push(block); - } - } - } - } - if (extraBlocks.length > 0) { - const existingSystem = payload.system; - if (typeof existingSystem === "string" && existingSystem.length > 0) { - payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; - } else if (Array.isArray(existingSystem)) { - payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; - } else { - payload.system = extraBlocks; - } - } - payload.messages = messages.filter((m) => m.role !== "system"); - }; - const normalizeClaudeUpstreamMessages = ( payload: Record, options?: { preserveToolResultBlocks?: boolean } diff --git a/tests/unit/system-role-extraction.test.ts b/tests/unit/system-role-extraction.test.ts new file mode 100644 index 0000000000..306fb7feef --- /dev/null +++ b/tests/unit/system-role-extraction.test.ts @@ -0,0 +1,119 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { extractSystemRoleMessages } from "../../open-sse/handlers/chatCore.ts"; + +test("extractSystemRoleMessages moves role=system to top-level system", () => { + const payload = { + messages: [ + { role: "system", content: "Memory context: foo" }, + { role: "user", content: "hello" }, + { role: "assistant", content: "hi" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 2); + assert.equal(payload.messages[0].role, "user"); + assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: foo" }]); +}); + +test("extractSystemRoleMessages merges with existing top-level system string", () => { + const payload = { + system: "You are Claude.", + messages: [ + { role: "system", content: "Memory context: bar" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "You are Claude." }, + { type: "text", text: "Memory context: bar" }, + ]); +}); + +test("extractSystemRoleMessages merges with existing top-level system array", () => { + const payload = { + system: [{ type: "text", text: "Existing system" }], + messages: [ + { role: "system", content: "Memory context: baz" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "Existing system" }, + { type: "text", text: "Memory context: baz" }, + ]); +}); + +test("extractSystemRoleMessages does nothing when no system role messages", () => { + const payload = { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: "hi" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 2); + assert.equal(payload.system, undefined); +}); + +test("extractSystemRoleMessages handles non-array messages gracefully", () => { + const payload = { messages: "not-an-array" }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages, "not-an-array"); +}); + +test("extractSystemRoleMessages handles empty messages array", () => { + const payload = { messages: [] }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 0); +}); + +test("extractSystemRoleMessages handles case-insensitive role System", () => { + const payload = { + messages: [ + { role: "System", content: "Memory context: caps" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: caps" }]); +}); + +test("extractSystemRoleMessages drops empty text content from system messages", () => { + const payload = { + messages: [ + { role: "system", content: "" }, + { role: "system", content: "valid" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [{ type: "text", text: "valid" }]); +}); + +test("extractSystemRoleMessages handles system messages with array content", () => { + const payload = { + messages: [ + { + role: "system", + content: [ + { type: "text", text: "Block 1" }, + { type: "text", text: "Block 2" }, + ], + }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "Block 1" }, + { type: "text", text: "Block 2" }, + ]); +});