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 <cursoragent@cursor.com>
This commit is contained in:
Rex
2026-06-29 15:28:02 +07:00
committed by decolua
parent 8a664d619d
commit 9e3866658a
5 changed files with 88 additions and 14 deletions

View File

@@ -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" },

View File

@@ -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" },

View File

@@ -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));
}
}

View File

@@ -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