open-claude-code-proxy 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/server.js +180 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-claude-code-proxy",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Local proxy that forwards API requests through the official Claude Code CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
"homepage": "https://github.com/lkyxuan/open-claude-code-proxy#readme",
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.1"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/server.js
CHANGED
|
@@ -19,6 +19,96 @@ function log(message, data = null) {
|
|
|
19
19
|
fs.appendFileSync(LOG_FILE, logLine + '\n');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Claude Code → OpenCode 参数名映射(snake_case → camelCase)
|
|
23
|
+
const PARAM_MAPPING = {
|
|
24
|
+
'file_path': 'filePath',
|
|
25
|
+
'old_string': 'oldString',
|
|
26
|
+
'new_string': 'newString',
|
|
27
|
+
'replace_all': 'replaceAll'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Claude Code → OpenCode 工具名映射
|
|
31
|
+
const TOOL_NAME_MAPPING = {
|
|
32
|
+
// 大写 → 小写
|
|
33
|
+
'Read': 'read',
|
|
34
|
+
'Write': 'write',
|
|
35
|
+
'Edit': 'edit',
|
|
36
|
+
'Bash': 'bash',
|
|
37
|
+
'Glob': 'glob',
|
|
38
|
+
'Grep': 'grep',
|
|
39
|
+
'Task': 'task',
|
|
40
|
+
'TodoWrite': 'todowrite',
|
|
41
|
+
// 特殊映射
|
|
42
|
+
'WebSearch': 'websearch_exa_web_search_exa',
|
|
43
|
+
'WebFetch': 'webfetch'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 转换 tool_use 中的工具名和参数名(Claude Code 格式 → OpenCode 格式)
|
|
47
|
+
function convertToolUse(content) {
|
|
48
|
+
if (!Array.isArray(content)) return content;
|
|
49
|
+
|
|
50
|
+
return content.map(block => {
|
|
51
|
+
if (block.type !== 'tool_use') return block;
|
|
52
|
+
|
|
53
|
+
// 转换工具名
|
|
54
|
+
const convertedName = TOOL_NAME_MAPPING[block.name] || block.name;
|
|
55
|
+
|
|
56
|
+
// 转换参数名
|
|
57
|
+
const convertedInput = {};
|
|
58
|
+
if (block.input) {
|
|
59
|
+
for (const [key, value] of Object.entries(block.input)) {
|
|
60
|
+
const newKey = PARAM_MAPPING[key] || key;
|
|
61
|
+
convertedInput[newKey] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { ...block, name: convertedName, input: convertedInput };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 清理所有 cache_control - 因为 Claude Code CLI 会加自己的,我们不能再加
|
|
70
|
+
function sanitizeCacheControl(requestData) {
|
|
71
|
+
let removedCount = 0;
|
|
72
|
+
|
|
73
|
+
// 清理 system 中的 cache_control
|
|
74
|
+
if (Array.isArray(requestData.system)) {
|
|
75
|
+
for (const block of requestData.system) {
|
|
76
|
+
if (block.cache_control) {
|
|
77
|
+
delete block.cache_control;
|
|
78
|
+
removedCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 清理 messages 中的 cache_control
|
|
84
|
+
if (Array.isArray(requestData.messages)) {
|
|
85
|
+
for (const message of requestData.messages) {
|
|
86
|
+
if (Array.isArray(message.content)) {
|
|
87
|
+
for (const block of message.content) {
|
|
88
|
+
if (block.cache_control) {
|
|
89
|
+
delete block.cache_control;
|
|
90
|
+
removedCount++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 清理 tools 中的 cache_control
|
|
98
|
+
if (Array.isArray(requestData.tools)) {
|
|
99
|
+
for (const tool of requestData.tools) {
|
|
100
|
+
if (tool.cache_control) {
|
|
101
|
+
delete tool.cache_control;
|
|
102
|
+
removedCount++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (removedCount > 0) {
|
|
108
|
+
log(`清理了 ${removedCount} 个 cache_control(让 Claude Code 使用自己的缓存策略)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
22
112
|
// 启动时清空日志
|
|
23
113
|
fs.writeFileSync(LOG_FILE, `=== Claude Local Proxy 启动于 ${new Date().toISOString()} ===\n`);
|
|
24
114
|
|
|
@@ -60,6 +150,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
60
150
|
|
|
61
151
|
const requestData = JSON.parse(body);
|
|
62
152
|
|
|
153
|
+
// 清理 cache_control - Anthropic API 最多允许 4 个
|
|
154
|
+
sanitizeCacheControl(requestData);
|
|
155
|
+
|
|
63
156
|
// 限制日志大小,保留最近的请求(超过 100KB 截断)
|
|
64
157
|
try {
|
|
65
158
|
const stats = fs.statSync(LOG_FILE);
|
|
@@ -110,6 +203,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
110
203
|
: '[complex]'
|
|
111
204
|
}))
|
|
112
205
|
});
|
|
206
|
+
log('7️⃣ Tools', {
|
|
207
|
+
toolsCount: requestData.tools?.length || 0,
|
|
208
|
+
toolNames: requestData.tools?.map(t => t.name) || []
|
|
209
|
+
});
|
|
210
|
+
// 记录完整的工具定义(用于研究映射)
|
|
211
|
+
if (requestData.tools && requestData.tools.length > 0) {
|
|
212
|
+
log('完整 Tools 定义', JSON.stringify(requestData.tools, null, 2));
|
|
213
|
+
}
|
|
113
214
|
log('其他信息', {
|
|
114
215
|
apiKey: apiKey.substring(0, 20) + '...',
|
|
115
216
|
model: requestData.model,
|
|
@@ -145,15 +246,30 @@ async function handleNormalRequest(requestData, res) {
|
|
|
145
246
|
const messages = requestData.messages || [];
|
|
146
247
|
|
|
147
248
|
// 构建 prompt:把 messages 数组转成对话格式
|
|
148
|
-
|
|
249
|
+
let prompt = buildPromptFromMessages(messages);
|
|
250
|
+
|
|
251
|
+
// 如果有工具定义,在用户消息前面加上指令(而不是在 system prompt 中)
|
|
252
|
+
if (requestData.tools && requestData.tools.length > 0) {
|
|
253
|
+
const toolInstruction = `[PROXY MODE] You are being accessed through a proxy. For this request, use ONLY the following client-defined tools instead of any built-in tools:
|
|
254
|
+
|
|
255
|
+
${JSON.stringify(requestData.tools, null, 2)}
|
|
256
|
+
|
|
257
|
+
When you want to use one of these tools, respond with tool_use in your content array. Now here is the user's request:
|
|
258
|
+
|
|
259
|
+
`;
|
|
260
|
+
prompt = toolInstruction + prompt;
|
|
261
|
+
}
|
|
149
262
|
|
|
150
263
|
return new Promise((resolve, reject) => {
|
|
151
264
|
// 使用 JSON 输出格式,支持 tool_use
|
|
152
|
-
const
|
|
265
|
+
const args = [
|
|
153
266
|
'--print',
|
|
154
|
-
'--output-format', 'json'
|
|
155
|
-
|
|
156
|
-
|
|
267
|
+
'--output-format', 'json'
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
args.push(prompt);
|
|
271
|
+
|
|
272
|
+
const claude = spawn('claude', args, {
|
|
157
273
|
env: { ...process.env },
|
|
158
274
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
159
275
|
});
|
|
@@ -178,12 +294,15 @@ async function handleNormalRequest(requestData, res) {
|
|
|
178
294
|
// 解析 Claude 的 JSON 输出
|
|
179
295
|
const claudeResponse = JSON.parse(output.trim());
|
|
180
296
|
|
|
297
|
+
// 转换 tool_use 参数名(Claude Code → OpenCode 格式)
|
|
298
|
+
const convertedContent = convertToolUse(claudeResponse.content);
|
|
299
|
+
|
|
181
300
|
// 构建 Anthropic API 格式的响应
|
|
182
301
|
const response = {
|
|
183
302
|
id: claudeResponse.id || `msg_${Date.now()}`,
|
|
184
303
|
type: 'message',
|
|
185
304
|
role: 'assistant',
|
|
186
|
-
content:
|
|
305
|
+
content: convertedContent || [{ type: 'text', text: output.trim() }],
|
|
187
306
|
model: requestData.model || 'claude-sonnet-4-20250514',
|
|
188
307
|
stop_reason: claudeResponse.stop_reason || 'end_turn',
|
|
189
308
|
stop_sequence: claudeResponse.stop_sequence || null,
|
|
@@ -233,14 +352,27 @@ async function handleNormalRequest(requestData, res) {
|
|
|
233
352
|
async function handleStreamRequest(requestData, res) {
|
|
234
353
|
const messages = requestData.messages || [];
|
|
235
354
|
|
|
355
|
+
// 如果有工具定义,构建工具指令前缀
|
|
356
|
+
const toolInstruction = requestData.tools && requestData.tools.length > 0
|
|
357
|
+
? `[PROXY MODE] You are being accessed through a proxy. For this request, use ONLY the following client-defined tools instead of any built-in tools:
|
|
358
|
+
|
|
359
|
+
${JSON.stringify(requestData.tools, null, 2)}
|
|
360
|
+
|
|
361
|
+
When you want to use one of these tools, respond with tool_use in your content array. Now here is the user's request:
|
|
362
|
+
|
|
363
|
+
`
|
|
364
|
+
: '';
|
|
365
|
+
|
|
236
366
|
// 使用 stream-json 格式与 Claude Code 交互
|
|
237
367
|
return new Promise((resolve, reject) => {
|
|
238
|
-
const
|
|
368
|
+
const args = [
|
|
239
369
|
'--print',
|
|
240
370
|
'--input-format', 'stream-json',
|
|
241
371
|
'--output-format', 'stream-json',
|
|
242
372
|
'--verbose'
|
|
243
|
-
]
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const claude = spawn('claude', args, {
|
|
244
376
|
env: { ...process.env },
|
|
245
377
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
246
378
|
});
|
|
@@ -320,6 +452,18 @@ async function handleStreamRequest(requestData, res) {
|
|
|
320
452
|
contentBlockIndex++;
|
|
321
453
|
}
|
|
322
454
|
|
|
455
|
+
// 转换工具名(Claude Code → OpenCode 格式)
|
|
456
|
+
const convertedName = TOOL_NAME_MAPPING[content.name] || content.name;
|
|
457
|
+
|
|
458
|
+
// 转换参数名(Claude Code → OpenCode 格式)
|
|
459
|
+
const convertedInput = {};
|
|
460
|
+
if (content.input) {
|
|
461
|
+
for (const [key, value] of Object.entries(content.input)) {
|
|
462
|
+
const newKey = PARAM_MAPPING[key] || key;
|
|
463
|
+
convertedInput[newKey] = value;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
323
467
|
// 发送新的 tool_use block 开始
|
|
324
468
|
sendSSE(res, 'content_block_start', {
|
|
325
469
|
type: 'content_block_start',
|
|
@@ -327,18 +471,18 @@ async function handleStreamRequest(requestData, res) {
|
|
|
327
471
|
content_block: {
|
|
328
472
|
type: 'tool_use',
|
|
329
473
|
id: content.id,
|
|
330
|
-
name:
|
|
474
|
+
name: convertedName,
|
|
331
475
|
input: {}
|
|
332
476
|
}
|
|
333
477
|
});
|
|
334
478
|
|
|
335
|
-
//
|
|
479
|
+
// 发送工具输入(使用转换后的参数名)
|
|
336
480
|
sendSSE(res, 'content_block_delta', {
|
|
337
481
|
type: 'content_block_delta',
|
|
338
482
|
index: contentBlockIndex,
|
|
339
483
|
delta: {
|
|
340
484
|
type: 'input_json_delta',
|
|
341
|
-
partial_json: JSON.stringify(
|
|
485
|
+
partial_json: JSON.stringify(convertedInput)
|
|
342
486
|
}
|
|
343
487
|
});
|
|
344
488
|
|
|
@@ -381,12 +525,18 @@ async function handleStreamRequest(requestData, res) {
|
|
|
381
525
|
reject(error);
|
|
382
526
|
});
|
|
383
527
|
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
528
|
+
// 构建完整的对话历史作为 prompt(包括 tool_use 和 tool_result)
|
|
529
|
+
let fullPrompt = buildPromptFromMessages(messages);
|
|
530
|
+
|
|
531
|
+
// 在 prompt 前加上工具指令(如果有)
|
|
532
|
+
if (toolInstruction) {
|
|
533
|
+
fullPrompt = toolInstruction + fullPrompt;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (fullPrompt) {
|
|
387
537
|
const input = JSON.stringify({
|
|
388
538
|
type: 'user',
|
|
389
|
-
message:
|
|
539
|
+
message: { role: 'user', content: fullPrompt }
|
|
390
540
|
}) + '\n';
|
|
391
541
|
|
|
392
542
|
claude.stdin.write(input);
|
|
@@ -424,7 +574,7 @@ function buildPromptFromMessages(messages) {
|
|
|
424
574
|
return prompt;
|
|
425
575
|
}
|
|
426
576
|
|
|
427
|
-
// 获取消息内容(支持 string 和 array
|
|
577
|
+
// 获取消息内容(支持 string 和 array 格式,包括 tool_use 和 tool_result)
|
|
428
578
|
function getMessageContent(message) {
|
|
429
579
|
if (typeof message.content === 'string') {
|
|
430
580
|
return message.content;
|
|
@@ -432,8 +582,20 @@ function getMessageContent(message) {
|
|
|
432
582
|
|
|
433
583
|
if (Array.isArray(message.content)) {
|
|
434
584
|
return message.content
|
|
435
|
-
.
|
|
436
|
-
|
|
585
|
+
.map(c => {
|
|
586
|
+
if (c.type === 'text') {
|
|
587
|
+
return c.text;
|
|
588
|
+
}
|
|
589
|
+
if (c.type === 'tool_use') {
|
|
590
|
+
return `[调用工具 ${c.name},参数: ${JSON.stringify(c.input)}]`;
|
|
591
|
+
}
|
|
592
|
+
if (c.type === 'tool_result') {
|
|
593
|
+
const content = typeof c.content === 'string' ? c.content : JSON.stringify(c.content);
|
|
594
|
+
return `[工具 ${c.tool_use_id} 返回: ${content.substring(0, 500)}${content.length > 500 ? '...' : ''}]`;
|
|
595
|
+
}
|
|
596
|
+
return '';
|
|
597
|
+
})
|
|
598
|
+
.filter(Boolean)
|
|
437
599
|
.join('\n');
|
|
438
600
|
}
|
|
439
601
|
|