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.
Files changed (2) hide show
  1. package/package.json +4 -1
  2. 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.2",
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
- const prompt = buildPromptFromMessages(messages);
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 claude = spawn('claude', [
265
+ const args = [
153
266
  '--print',
154
- '--output-format', 'json',
155
- prompt
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: claudeResponse.content || [{ type: 'text', text: output.trim() }],
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 claude = spawn('claude', [
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: content.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(content.input)
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
- // 发送用户消息给 Claude
385
- const lastUserMessage = messages.filter(m => m.role === 'user').pop();
386
- if (lastUserMessage) {
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: lastUserMessage
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
- .filter(c => c.type === 'text')
436
- .map(c => c.text)
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