protocol-proxy 2.8.2 → 2.9.0

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.
@@ -0,0 +1,108 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const DATA_DIR = path.join(os.homedir(), '.protocol-proxy', 'conversations');
6
+
7
+ // 内存缓存:conversationId → { id, proxyId, messages, createdAt, lastActivity }
8
+ let conversations = {};
9
+
10
+ // debounce 写入:500ms 内同一 conv 只写一次
11
+ const pendingWrites = new Map(); // convId → timer
12
+
13
+ function scheduleSave(conv) {
14
+ if (pendingWrites.has(conv.id)) clearTimeout(pendingWrites.get(conv.id));
15
+ pendingWrites.set(conv.id, setTimeout(() => {
16
+ pendingWrites.delete(conv.id);
17
+ fs.writeFile(path.join(DATA_DIR, conv.id + '.json'), JSON.stringify(conv), 'utf8', () => {});
18
+ }, 500));
19
+ }
20
+
21
+ function saveImmediate(conv) {
22
+ if (pendingWrites.has(conv.id)) {
23
+ clearTimeout(pendingWrites.get(conv.id));
24
+ pendingWrites.delete(conv.id);
25
+ }
26
+ try {
27
+ fs.writeFileSync(path.join(DATA_DIR, conv.id + '.json'), JSON.stringify(conv), 'utf8');
28
+ } catch {}
29
+ }
30
+
31
+ function flushAll() {
32
+ for (const [id, timer] of pendingWrites) {
33
+ clearTimeout(timer);
34
+ const conv = conversations[id];
35
+ if (conv) {
36
+ try { fs.writeFileSync(path.join(DATA_DIR, id + '.json'), JSON.stringify(conv), 'utf8'); } catch {}
37
+ }
38
+ }
39
+ pendingWrites.clear();
40
+ }
41
+
42
+ function init() {
43
+ if (!fs.existsSync(DATA_DIR)) {
44
+ fs.mkdirSync(DATA_DIR, { recursive: true });
45
+ }
46
+ try {
47
+ const files = fs.readdirSync(DATA_DIR).filter(f => f.endsWith('.json'));
48
+ for (const file of files) {
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8'));
51
+ if (data.id) conversations[data.id] = data;
52
+ } catch {}
53
+ }
54
+ } catch {}
55
+ }
56
+
57
+ function get(id) {
58
+ return conversations[id] || null;
59
+ }
60
+
61
+ function create(proxyId, maxConversations) {
62
+ // 超过最大会话数时删除最早的
63
+ if (maxConversations > 0) {
64
+ const all = list();
65
+ while (all.length >= maxConversations) {
66
+ const oldest = all.shift(); // list() 已按 lastActivity 升序
67
+ remove(oldest.id);
68
+ }
69
+ }
70
+ const id = 'conv-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
71
+ const conv = { id, proxyId, messages: [], createdAt: Date.now(), lastActivity: Date.now() };
72
+ conversations[id] = conv;
73
+ saveImmediate(conv); // 新建立即写入
74
+ return conv;
75
+ }
76
+
77
+ function touch(conv) {
78
+ conv.lastActivity = Date.now();
79
+ scheduleSave(conv); // debounce 异步写入
80
+ }
81
+
82
+ function remove(id) {
83
+ delete conversations[id];
84
+ if (pendingWrites.has(id)) { clearTimeout(pendingWrites.get(id)); pendingWrites.delete(id); }
85
+ fs.unlink(path.join(DATA_DIR, id + '.json'), () => {});
86
+ }
87
+
88
+ // 返回会话列表(按 lastActivity 升序,不含 messages),用于前端展示
89
+ function list() {
90
+ return Object.values(conversations)
91
+ .map(c => ({
92
+ id: c.id,
93
+ proxyId: c.proxyId,
94
+ createdAt: c.createdAt,
95
+ lastActivity: c.lastActivity,
96
+ messageCount: (c.messages || []).length,
97
+ // 取第一条 user 消息作为标题预览
98
+ preview: ((c.messages || []).find(m => m.role === 'user')?.content || '').slice(0, 60),
99
+ }))
100
+ .sort((a, b) => a.lastActivity - b.lastActivity);
101
+ }
102
+
103
+ // 进程退出时确保所有 debounce 中的数据落盘
104
+ process.on('exit', flushAll);
105
+ process.on('SIGINT', () => { flushAll(); process.exit(0); });
106
+ process.on('SIGTERM', () => { flushAll(); process.exit(0); });
107
+
108
+ module.exports = { init, get, create, touch, remove, list, flushAll };
@@ -303,6 +303,10 @@ function processLine(line, state, targetModel) {
303
303
  return prefix + encodeOpenAIChunk(state.messageId, targetModel, { content: delta.text });
304
304
  }
305
305
 
306
+ if (delta.type === 'thinking_delta' && delta.thinking) {
307
+ return prefix + encodeOpenAIChunk(state.messageId, targetModel, { reasoning_content: delta.thinking });
308
+ }
309
+
306
310
  if (delta.type === 'input_json_delta' && delta.partial_json !== undefined) {
307
311
  if (!state.sentToolInit) {
308
312
  state.sentToolInit = true;
@@ -351,9 +351,13 @@ function createProxyApp(proxyConfigOrGetter) {
351
351
  const requestStart = Date.now();
352
352
  const proxyConfig = getProxyConfig();
353
353
  const inboundProtocol = detectInboundProtocol(req, req.body);
354
- const candidates = buildCandidates(proxyConfig);
354
+ let candidates = buildCandidates(proxyConfig);
355
355
 
356
- if (candidates.length === 0) {
356
+ // 请求级供应商/模型覆盖(来自助手 chat 端点的自定义头)
357
+ const overrideProviderId = req.headers['x-pp-provider-id'];
358
+ const overrideModel = req.headers['x-pp-model'];
359
+
360
+ if (candidates.length === 0 && !overrideProviderId) {
357
361
  return res.status(500).json({ error: 'Proxy target not configured' });
358
362
  }
359
363
 
@@ -362,8 +366,43 @@ function createProxyApp(proxyConfigOrGetter) {
362
366
  const clientIP = req.ip || req.socket?.remoteAddress || '';
363
367
  const proxyName = proxyConfig.name || '';
364
368
  const inboundModel = req.body?.model;
365
- const effectiveModel = proxyConfig.target?.defaultModel || inboundModel;
366
- const baseRequestBody = effectiveModel ? { ...req.body, model: effectiveModel } : { ...req.body };
369
+ let effectiveModel = proxyConfig.target?.defaultModel || inboundModel;
370
+
371
+ // 模型覆盖:优先级高于 defaultModel
372
+ if (overrideModel) {
373
+ effectiveModel = overrideModel;
374
+ }
375
+ let baseRequestBody = effectiveModel ? { ...req.body, model: effectiveModel } : { ...req.body };
376
+
377
+ // 供应商覆盖:筛选或动态构建候选
378
+ if (overrideProviderId) {
379
+ const filtered = candidates.filter(c => c.providerId === overrideProviderId);
380
+ if (filtered.length > 0) {
381
+ candidates = filtered;
382
+ } else {
383
+ // 不在代理候选池中 → 用附加头动态构建临时候选
384
+ const providerUrl = req.headers['x-pp-provider-url'];
385
+ const providerProtocol = req.headers['x-pp-provider-protocol'];
386
+ const providerKeys = req.headers['x-pp-provider-keys'];
387
+ if (providerUrl && providerProtocol) {
388
+ const tempCandidate = {
389
+ providerId: overrideProviderId,
390
+ providerName: overrideProviderId,
391
+ providerUrl,
392
+ protocol: providerProtocol,
393
+ apiKeys: providerKeys ? JSON.parse(providerKeys) : [],
394
+ models: [],
395
+ azureDeployment: '',
396
+ azureApiVersion: '',
397
+ model: '',
398
+ weight: 1,
399
+ };
400
+ candidates = [tempCandidate];
401
+ } else {
402
+ return res.status(400).json({ error: '指定的供应商不在代理候选列表中,且缺少供应商配置' });
403
+ }
404
+ }
405
+ }
367
406
 
368
407
  // Inject cached reasoning for OpenAI inbound (OpenAI protocol lacks reasoning_content)
369
408
  if (inboundProtocol === 'openai') {
@@ -446,7 +485,7 @@ function createProxyApp(proxyConfigOrGetter) {
446
485
 
447
486
  const targetUrl = buildTargetUrl(candidate, req.path, isStream, candidateModel);
448
487
  // Forward client headers (preserve anthropic-beta, user-agent, etc.)
449
- const skipHeaders = new Set(['host', 'connection', 'content-length', 'content-type', 'accept', 'authorization', 'x-api-key', 'anthropic-version']);
488
+ const skipHeaders = new Set(['host', 'connection', 'content-length', 'content-type', 'accept', 'authorization', 'x-api-key', 'anthropic-version', 'x-pp-provider-id', 'x-pp-model', 'x-pp-provider-url', 'x-pp-provider-protocol', 'x-pp-provider-keys']);
450
489
  const headers = {};
451
490
  for (const [key, val] of Object.entries(req.headers)) {
452
491
  if (!skipHeaders.has(key.toLowerCase())) headers[key] = val;
@@ -595,6 +634,8 @@ function createProxyApp(proxyConfigOrGetter) {
595
634
  const flushed = sseConverter.flush();
596
635
  if (flushed) res.write(flushed);
597
636
  }
637
+
638
+ res.end();
598
639
  } catch (err) {
599
640
  recordFailure(proxyId, candidate.providerId);
600
641
  logger.error(`[${requestId}] Stream error:`, err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protocol-proxy",
3
- "version": "2.8.2",
3
+ "version": "2.9.0",
4
4
  "description": "OpenAI / Anthropic 协议转换透明代理",
5
5
  "main": "server.js",
6
6
  "bin": {