From 9e3866658af5e1ee571ab5a0a134a3d9d5507664 Mon Sep 17 00:00:00 2001 From: Rex <177378174+anki1kr@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:28:02 +0700 Subject: [PATCH] fix(alicode): preserve cache_control for DashScope providers (#2069) Opt-in quirk preserveCacheControl keeps cache_control on content blocks for alicode/alicode-intl, enabling DashScope prompt caching. signature is always stripped; all other providers unchanged. Co-authored-by: Cursor --- open-sse/providers/registry/alicode-intl.js | 1 + open-sse/providers/registry/alicode.js | 1 + open-sse/translator/formats/openai.js | 30 +++++---- open-sse/translator/index.js | 4 +- tests/unit/alicode-cache-control-2069.test.js | 66 +++++++++++++++++++ 5 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 tests/unit/alicode-cache-control-2069.test.js diff --git a/open-sse/providers/registry/alicode-intl.js b/open-sse/providers/registry/alicode-intl.js index ac98cb2d..b2eca7d8 100644 --- a/open-sse/providers/registry/alicode-intl.js +++ b/open-sse/providers/registry/alicode-intl.js @@ -16,6 +16,7 @@ export default { transport: { baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", headers: {}, + quirks: { preserveCacheControl: true }, }, models: [ { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, diff --git a/open-sse/providers/registry/alicode.js b/open-sse/providers/registry/alicode.js index 5b6a088f..e572c7e7 100644 --- a/open-sse/providers/registry/alicode.js +++ b/open-sse/providers/registry/alicode.js @@ -16,6 +16,7 @@ export default { transport: { baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions", headers: {}, + quirks: { preserveCacheControl: true }, }, models: [ { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, diff --git a/open-sse/translator/formats/openai.js b/open-sse/translator/formats/openai.js index d6c850c4..3252568f 100644 --- a/open-sse/translator/formats/openai.js +++ b/open-sse/translator/formats/openai.js @@ -6,42 +6,46 @@ export { VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES }; // Filter messages to OpenAI standard format // Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks -export function filterToOpenAIFormat(body) { +// opts.preserveCacheControl: keep cache_control on content blocks (e.g. for DashScope/alicode) +export function filterToOpenAIFormat(body, opts = {}) { if (!body.messages || !Array.isArray(body.messages)) return body; - + const keepCache = !!opts.preserveCacheControl; + + function stripBlock(block) { + const { signature, cache_control, ...rest } = block; + return keepCache && cache_control ? { ...rest, cache_control } : rest; + } + body.messages = body.messages.map(msg => { // Normalize developer role to system (many providers don't support developer) if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM }; - + // Keep tool messages as-is (OpenAI format) if (msg.role === ROLE.TOOL) return msg; - + // Keep assistant messages with tool_calls as-is if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg; - + // Handle string content if (typeof msg.content === "string") return msg; - + // Handle array content if (Array.isArray(msg.content)) { const filteredContent = []; - + for (const block of msg.content) { // Skip thinking blocks if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue; - + // Only keep valid OpenAI content types if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) { - // Remove signature field if exists - const { signature, cache_control, ...cleanBlock } = block; - filteredContent.push(cleanBlock); + filteredContent.push(stripBlock(block)); } else if (block.type === CLAUDE_BLOCK.TOOL_USE) { // Convert tool_use to tool_calls format (handled separately) continue; } else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) { // Keep tool_result but clean it - const { signature, cache_control, ...cleanBlock } = block; - filteredContent.push(cleanBlock); + filteredContent.push(stripBlock(block)); } } diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index 8c25d486..e9c84971 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -109,7 +109,9 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream // Always normalize to clean OpenAI format when target is OpenAI // This handles hybrid requests (e.g., OpenAI messages + Claude tools) if (targetFormat === FORMATS.OPENAI) { - result = filterToOpenAIFormat(result); + result = filterToOpenAIFormat(result, { + preserveCacheControl: !!PROVIDERS[provider]?.quirks?.preserveCacheControl, + }); } // Final step: prepare request for Claude format endpoints diff --git a/tests/unit/alicode-cache-control-2069.test.js b/tests/unit/alicode-cache-control-2069.test.js new file mode 100644 index 00000000..53d15e7f --- /dev/null +++ b/tests/unit/alicode-cache-control-2069.test.js @@ -0,0 +1,66 @@ +// #2069 — cache_control markers stripped for alicode/alicode-intl (DashScope) providers. +// DashScope supports explicit cache_control: { type: "ephemeral" } in content blocks, +// but the default filterToOpenAIFormat strips them. preserveCacheControl quirk opts-in. +import { describe, it, expect } from "vitest"; +import { filterToOpenAIFormat } from "../../open-sse/translator/formats/openai.js"; + +const msgWithCache = [ + { + role: "user", + content: [ + { type: "text", text: "large context", cache_control: { type: "ephemeral" } }, + ], + }, + { + role: "assistant", + content: [ + { type: "text", text: "reply", cache_control: { type: "ephemeral" } }, + ], + }, +]; + +describe("filterToOpenAIFormat cache_control handling (#2069)", () => { + it("strips cache_control by default (all standard OpenAI providers)", () => { + const body = { messages: JSON.parse(JSON.stringify(msgWithCache)) }; + filterToOpenAIFormat(body); + for (const msg of body.messages) { + for (const block of msg.content) { + expect(block.cache_control).toBeUndefined(); + } + } + }); + + it("preserves cache_control when preserveCacheControl option is true (alicode/DashScope)", () => { + const body = { messages: JSON.parse(JSON.stringify(msgWithCache)) }; + filterToOpenAIFormat(body, { preserveCacheControl: true }); + for (const msg of body.messages) { + for (const block of msg.content) { + expect(block.cache_control).toEqual({ type: "ephemeral" }); + } + } + }); + + it("always strips signature regardless of preserveCacheControl", () => { + const body = { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "hi", signature: "sig123", cache_control: { type: "ephemeral" } }, + ], + }, + ], + }; + filterToOpenAIFormat(body, { preserveCacheControl: true }); + expect(body.messages[0].content[0].signature).toBeUndefined(); + expect(body.messages[0].content[0].cache_control).toEqual({ type: "ephemeral" }); + }); + + it("does not add cache_control when block had none (preserveCacheControl: true)", () => { + const body = { + messages: [{ role: "user", content: [{ type: "text", text: "no cache" }] }], + }; + filterToOpenAIFormat(body, { preserveCacheControl: true }); + expect(body.messages[0].content[0].cache_control).toBeUndefined(); + }); +});