Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 107 additions & 34 deletions proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,36 @@ function getEnvironment() {
function buildCcRequest(openaiReq) {
const { model, messages, max_tokens, temperature, tools, stream, reasoning_effort, tool_choice, parallel_tool_calls } = openaiReq;

// 从 messages 中提取 system prompt
// 从 messages 中提取 system prompt — 保留 cache_control 以启用 prompt caching
const systemMsgs = messages.filter(m => m.role === 'system');
const systemPrompt = systemMsgs.map(m => m.content).join('\n');
// 检测 system 是否带有 cache_control(Array content with cache_control)
let systemHasCache = false;
for (const m of systemMsgs) {
if (Array.isArray(m.content) && m.content.some(c => c && c.cache_control)) { systemHasCache = true; break; }
}
let systemPrompt = null;
if (systemHasCache) {
const blocks = [];
for (const m of systemMsgs) {
if (Array.isArray(m.content)) {
for (const c of m.content) {
const block = { type: 'text', text: c.text || c.content || '' };
if (c.cache_control) block.cache_control = c.cache_control;
blocks.push(block);
}
} else if (typeof m.content === 'string') {
blocks.push({ type: 'text', text: m.content });
}
}
systemPrompt = blocks;
} else {
systemPrompt = systemMsgs.map(m => {
if (typeof m.content === 'string') return m.content;
if (Array.isArray(m.content)) return m.content.map(c => c.text || c.content || '').join('\n');
return String(m.content || '');
}).join('\n');
if (!systemPrompt) systemPrompt = null;
}
const chatMessages = messages.filter(m => m.role !== 'system');

// Build tool_call_id → tool_name reverse lookup
Expand All @@ -384,20 +411,30 @@ function buildCcRequest(openaiReq) {
}
}

// 转换 messages 为 CC 格式
// 转换 messages 为 CC 格式 — 保留 cache_control 以启用 prompt caching
const ccMessages = chatMessages.map(msg => {
if (msg.role === 'user') {
if (typeof msg.content === 'string') {
return { role: 'user', content: [{ type: 'text', text: msg.content }] };
const block = { type: 'text', text: msg.content };
if (msg.cache_control) block.cache_control = msg.cache_control;
return { role: 'user', content: [block] };
}
// 多模态:数组 content 原样透传(text + image_url → CC image 格式)
// 多模态:数组 content 原样透传(text + image_url → CC image 格式),保留 cache_control
if (Array.isArray(msg.content)) {
const parts = msg.content.map(part => {
if (part.type === 'image_url') {
const url = part.image_url?.url || '';
// CC CLI 真实格式: { type: "image", image: "data:image/jpeg;base64,..." }
return { type: 'image', image: url };
const img = { type: 'image', image: url };
if (part.cache_control) img.cache_control = part.cache_control;
return img;
}
if (part.type === 'text') {
const t = { type: 'text', text: part.text || part.content || '' };
if (part.cache_control) t.cache_control = part.cache_control;
return t;
}
// 透传其他类型并保留 cache_control
if (part.cache_control) return { ...part };
return part;
}).filter(Boolean);
return { role: 'user', content: parts };
Expand All @@ -407,10 +444,16 @@ function buildCcRequest(openaiReq) {
if (msg.role === 'assistant') {
const parts = [];
if (msg.content && typeof msg.content === 'string') {
parts.push({ type: 'text', text: msg.content });
const block = { type: 'text', text: msg.content };
if (msg.cache_control) block.cache_control = msg.cache_control;
parts.push(block);
} else if (msg.content && Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'text') parts.push(part);
if (part.type === 'text') {
const t = { type: 'text', text: part.text || '' };
if (part.cache_control) t.cache_control = part.cache_control;
parts.push(t);
}
}
}
if (msg.tool_calls) {
Expand Down Expand Up @@ -476,12 +519,16 @@ function buildCcRequest(openaiReq) {
body.params.reasoning_effort = reasoning_effort;
}
if (tools && tools.length > 0) {
body.params.tools = tools.map(t => ({
type: t.type || 'function',
name: t.function?.name || t.name || '',
description: t.function?.description || t.description || '',
input_schema: t.function?.parameters || t.input_schema || { type: 'object', properties: {} },
}));
body.params.tools = tools.map(t => {
const tool = {
type: t.type || 'function',
name: t.function?.name || t.name || '',
description: t.function?.description || t.description || '',
input_schema: t.function?.parameters || t.input_schema || { type: 'object', properties: {} },
};
if (t.cache_control) tool.cache_control = t.cache_control;
return tool;
});
}
if (tool_choice !== undefined) {
// OpenAI 格式 → CC (Anthropic 风格) 格式
Expand Down Expand Up @@ -1166,25 +1213,34 @@ function buildAnthropicResponse(model, fullText, toolCalls, finishReason, usage)
}

function convertAnthropicToOpenAI(anthropicReq) {
// 1. Extract system prompt (top-level, not in messages array)
let systemPrompt = '';
// 1. Extract system prompt (top-level, not in messages array) — keep cache_control blocks
let systemContent = null;
if (anthropicReq.system) {
if (typeof anthropicReq.system === 'string') {
systemPrompt = anthropicReq.system;
systemContent = anthropicReq.system;
} else if (Array.isArray(anthropicReq.system)) {
systemPrompt = anthropicReq.system
.filter(b => b.type === 'text')
.map(b => b.text)
.join('\n');
const hasCache = anthropicReq.system.some(b => b && b.cache_control);
if (hasCache) {
systemContent = anthropicReq.system.map(b => {
const block = { type: 'text', text: b.text || '' };
if (b.cache_control) block.cache_control = b.cache_control;
return block;
});
} else {
systemContent = anthropicReq.system
.filter(b => b.type === 'text')
.map(b => b.text)
.join('\n');
}
}
}

// 2. Build tool name map + convert messages
const toolNameFromId = {};
const openaiMessages = [];

if (systemPrompt) {
openaiMessages.push({ role: 'system', content: systemPrompt });
if (systemContent !== null && systemContent !== '') {
openaiMessages.push({ role: 'system', content: systemContent });
}

const messages = anthropicReq.messages || [];
Expand Down Expand Up @@ -1220,13 +1276,26 @@ function convertAnthropicToOpenAI(anthropicReq) {
for (const block of msg.content) {
if (block.type === 'text') {
textContent += block.text || '';
if (block.cache_control) textHasCache = block;
} else if (block.type === 'tool_result') {
toolResults.push(block);
}
}
}
if (textContent) {
openaiMessages.push({ role: 'user', content: textContent });
// CC 需按 content 数组透传 cache_control,仅当真正带有标记时
if (Array.isArray(msg.content) && msg.content.some(b => b && b.cache_control)) {
const blocks = msg.content.filter(b => b.type === 'text').map(b => {
const tb = { type: 'text', text: b.text || '' };
if (b.cache_control) tb.cache_control = b.cache_control;
return tb;
});
openaiMessages.push({ role: 'user', content: blocks });
} else {
const userMsg = { role: 'user', content: textContent };
if (msg.cache_control) userMsg.cache_control = msg.cache_control;
openaiMessages.push(userMsg);
}
}
for (const tr of toolResults) {
const toolContent = typeof tr.content === 'string' ? tr.content
Expand All @@ -1250,16 +1319,20 @@ function convertAnthropicToOpenAI(anthropicReq) {
stream: anthropicReq.stream === true,
};

// 4. Map tools
// 4. Map tools — keep cache_control for prompt caching
if (anthropicReq.tools && anthropicReq.tools.length > 0) {
openaiReq.tools = anthropicReq.tools.map(t => ({
type: 'function',
function: {
name: t.name,
description: t.description || '',
parameters: t.input_schema || { type: 'object', properties: {} },
},
}));
openaiReq.tools = anthropicReq.tools.map(t => {
const tool = {
type: 'function',
function: {
name: t.name,
description: t.description || '',
parameters: t.input_schema || { type: 'object', properties: {} },
},
};
if (t.cache_control) tool.cache_control = t.cache_control;
return tool;
});
}

// 5. Map tool_choice
Expand Down