mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
committed by
decolua
co-authored by
Cursor
parent
8a664d619d
commit
9e3866658a
@@ -16,6 +16,7 @@ export default {
|
|||||||
transport: {
|
transport: {
|
||||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
|
||||||
headers: {},
|
headers: {},
|
||||||
|
quirks: { preserveCacheControl: true },
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
|
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
transport: {
|
transport: {
|
||||||
baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
|
baseUrl: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
|
||||||
headers: {},
|
headers: {},
|
||||||
|
quirks: { preserveCacheControl: true },
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
|
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus" },
|
||||||
|
|||||||
@@ -6,42 +6,46 @@ export { VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES };
|
|||||||
|
|
||||||
// Filter messages to OpenAI standard format
|
// Filter messages to OpenAI standard format
|
||||||
// Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks
|
// 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;
|
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 => {
|
body.messages = body.messages.map(msg => {
|
||||||
// Normalize developer role to system (many providers don't support developer)
|
// Normalize developer role to system (many providers don't support developer)
|
||||||
if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM };
|
if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM };
|
||||||
|
|
||||||
// Keep tool messages as-is (OpenAI format)
|
// Keep tool messages as-is (OpenAI format)
|
||||||
if (msg.role === ROLE.TOOL) return msg;
|
if (msg.role === ROLE.TOOL) return msg;
|
||||||
|
|
||||||
// Keep assistant messages with tool_calls as-is
|
// Keep assistant messages with tool_calls as-is
|
||||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg;
|
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg;
|
||||||
|
|
||||||
// Handle string content
|
// Handle string content
|
||||||
if (typeof msg.content === "string") return msg;
|
if (typeof msg.content === "string") return msg;
|
||||||
|
|
||||||
// Handle array content
|
// Handle array content
|
||||||
if (Array.isArray(msg.content)) {
|
if (Array.isArray(msg.content)) {
|
||||||
const filteredContent = [];
|
const filteredContent = [];
|
||||||
|
|
||||||
for (const block of msg.content) {
|
for (const block of msg.content) {
|
||||||
// Skip thinking blocks
|
// Skip thinking blocks
|
||||||
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue;
|
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue;
|
||||||
|
|
||||||
// Only keep valid OpenAI content types
|
// Only keep valid OpenAI content types
|
||||||
if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) {
|
if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) {
|
||||||
// Remove signature field if exists
|
filteredContent.push(stripBlock(block));
|
||||||
const { signature, cache_control, ...cleanBlock } = block;
|
|
||||||
filteredContent.push(cleanBlock);
|
|
||||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||||
// Convert tool_use to tool_calls format (handled separately)
|
// Convert tool_use to tool_calls format (handled separately)
|
||||||
continue;
|
continue;
|
||||||
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||||
// Keep tool_result but clean it
|
// Keep tool_result but clean it
|
||||||
const { signature, cache_control, ...cleanBlock } = block;
|
filteredContent.push(stripBlock(block));
|
||||||
filteredContent.push(cleanBlock);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
|
|||||||
// Always normalize to clean OpenAI format when target is OpenAI
|
// Always normalize to clean OpenAI format when target is OpenAI
|
||||||
// This handles hybrid requests (e.g., OpenAI messages + Claude tools)
|
// This handles hybrid requests (e.g., OpenAI messages + Claude tools)
|
||||||
if (targetFormat === FORMATS.OPENAI) {
|
if (targetFormat === FORMATS.OPENAI) {
|
||||||
result = filterToOpenAIFormat(result);
|
result = filterToOpenAIFormat(result, {
|
||||||
|
preserveCacheControl: !!PROVIDERS[provider]?.quirks?.preserveCacheControl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final step: prepare request for Claude format endpoints
|
// Final step: prepare request for Claude format endpoints
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user