coding-tool-x 3.5.3 → 3.5.5

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 (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/web/assets/Analytics-gvYu5sCM.js +25 -0
  3. package/dist/web/assets/{ConfigTemplates-uvPIB9bY.js → ConfigTemplates-CPlH8Ehd.js} +1 -1
  4. package/dist/web/assets/{Home-C3w31EDB.js → Home-B-qbu3uk.js} +1 -1
  5. package/dist/web/assets/{PluginManager-CfvgUebQ.js → PluginManager-B2tQ_YUq.js} +1 -1
  6. package/dist/web/assets/{ProjectList-C16vMDcU.js → ProjectList-kDadoXXs.js} +1 -1
  7. package/dist/web/assets/{SessionList-DWuhaeMb.js → SessionList-eLgITwTV.js} +1 -1
  8. package/dist/web/assets/{SkillManager-CRMUhw4v.js → SkillManager-B7zEB5Op.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-BOX_nqej.js → WorkspaceManager-C-RzB3ud.js} +1 -1
  10. package/dist/web/assets/{icons-B5Pl4lrD.js → icons-DlxD2wZJ.js} +1 -1
  11. package/dist/web/assets/{index-B02wDWNC.css → index-BHeh2z0i.css} +1 -1
  12. package/dist/web/assets/index-DG00t-zy.js +2 -0
  13. package/dist/web/assets/{naive-ui-Bdxp09n2.js → naive-ui-BaTCPPL5.js} +1 -1
  14. package/dist/web/assets/{vendors-CKPV1OAU.js → vendors-Fza9uSYn.js} +1 -1
  15. package/dist/web/assets/vue-vendor-aWwwFAao.js +45 -0
  16. package/dist/web/index.html +6 -6
  17. package/package.json +1 -1
  18. package/src/commands/daemon.js +87 -44
  19. package/src/server/codex-proxy-server.js +24 -59
  20. package/src/server/gemini-proxy-server.js +25 -66
  21. package/src/server/opencode-proxy-server.js +24 -59
  22. package/src/server/proxy-server.js +18 -30
  23. package/src/server/services/base/response-usage-parser.js +187 -0
  24. package/src/server/services/mcp-client.js +37 -13
  25. package/src/server/services/proxy-log-helper.js +21 -3
  26. package/src/server/services/statistics-service.js +7 -0
  27. package/dist/web/assets/Analytics-B653rHbb.js +0 -39
  28. package/dist/web/assets/index-B1ujw2sM.js +0 -2
  29. package/dist/web/assets/vue-vendor-3bf-fPGP.js +0 -45
@@ -15,6 +15,7 @@ const { getEffectiveApiKey } = require('./services/codex-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
17
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
18
+ const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
18
19
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
19
20
 
20
21
  let proxyServer = null;
@@ -225,6 +226,13 @@ async function startCodexProxyServer(options = {}) {
225
226
  // 更新 rawBody 以匹配修改后的 body
226
227
  req.rawBody = Buffer.from(JSON.stringify(req.body));
227
228
 
229
+ // 将原始模型和重定向模型存入 metadata,用于日志记录
230
+ const meta = requestMetadata.get(req);
231
+ if (meta) {
232
+ meta.originalModel = originalModel;
233
+ meta.redirectedModel = redirectedModel;
234
+ }
235
+
228
236
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
229
237
  const cachedRedirects = printedRedirectCache.get(channel.id) || {};
230
238
  if (cachedRedirects[originalModel] !== redirectedModel) {
@@ -324,14 +332,7 @@ async function startCodexProxyServer(options = {}) {
324
332
  });
325
333
 
326
334
  let buffer = '';
327
- let tokenData = {
328
- inputTokens: 0,
329
- outputTokens: 0,
330
- cachedTokens: 0,
331
- reasoningTokens: 0,
332
- totalTokens: 0,
333
- model: ''
334
- };
335
+ let tokenData = createTokenData();
335
336
  let usageRecorded = false;
336
337
  const parsedStream = createDecodedStream(proxyRes);
337
338
 
@@ -347,6 +348,8 @@ async function startCodexProxyServer(options = {}) {
347
348
  tokens: {
348
349
  input: tokenData.inputTokens,
349
350
  output: tokenData.outputTokens,
351
+ cacheCreation: tokenData.cacheCreation,
352
+ cacheRead: tokenData.cacheRead,
350
353
  cached: tokenData.cachedTokens,
351
354
  reasoning: tokenData.reasoningTokens,
352
355
  total: tokenData.totalTokens
@@ -355,7 +358,7 @@ async function startCodexProxyServer(options = {}) {
355
358
  broadcastLog,
356
359
  recordRequest: recordCodexRequest,
357
360
  recordSuccess,
358
- allowBroadcast: !isResponseClosed
361
+ allowBroadcast: true
359
362
  });
360
363
 
361
364
  if (!result) {
@@ -367,7 +370,6 @@ async function startCodexProxyServer(options = {}) {
367
370
  }
368
371
 
369
372
  parsedStream.on('data', (chunk) => {
370
- // 如果响应已关闭,停止处理
371
373
  if (isResponseClosed) {
372
374
  return;
373
375
  }
@@ -376,64 +378,34 @@ async function startCodexProxyServer(options = {}) {
376
378
 
377
379
  // 检查是否是 SSE 流
378
380
  if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
379
- // 处理 SSE 事件
380
381
  const events = buffer.split('\n\n');
381
382
  buffer = events.pop() || '';
382
383
 
383
- events.forEach((eventText, index) => {
384
+ events.forEach((eventText) => {
384
385
  if (!eventText.trim()) return;
385
386
 
386
387
  try {
387
388
  const lines = eventText.split('\n');
389
+ let eventType = '';
388
390
  let data = '';
389
391
 
390
392
  lines.forEach(line => {
391
- if (line.startsWith('data:')) {
393
+ if (line.startsWith('event:')) {
394
+ eventType = line.substring(6).trim();
395
+ } else if (line.startsWith('data:')) {
392
396
  data = line.substring(5).trim();
393
397
  }
394
398
  });
395
399
 
396
- if (!data) return;
397
-
398
- if (data === '[DONE]') return;
400
+ if (!data || data === '[DONE]') return;
399
401
 
400
402
  const parsed = JSON.parse(data);
403
+ const usage = parseSSEUsage(parsed, eventType);
404
+ mergeUsageIntoTokenData(tokenData, usage);
401
405
 
402
- // OpenAI Responses API: 在 response.completed 事件中获取 usage
403
- if (parsed.type === 'response.completed' && parsed.response) {
404
- // 从 response 对象中提取模型和 usage
405
- if (parsed.response.model) {
406
- tokenData.model = parsed.response.model;
407
- }
408
-
409
- if (parsed.response.usage) {
410
- tokenData.inputTokens = parsed.response.usage.input_tokens || 0;
411
- tokenData.outputTokens = parsed.response.usage.output_tokens || 0;
412
- tokenData.totalTokens = parsed.response.usage.total_tokens || 0;
413
-
414
- // 提取详细信息
415
- if (parsed.response.usage.input_tokens_details) {
416
- tokenData.cachedTokens = parsed.response.usage.input_tokens_details.cached_tokens || 0;
417
- }
418
- if (parsed.response.usage.output_tokens_details) {
419
- tokenData.reasoningTokens = parsed.response.usage.output_tokens_details.reasoning_tokens || 0;
420
- }
421
- }
406
+ if (usage.isDone) {
407
+ recordUsageIfReady();
422
408
  }
423
-
424
- // 兼容其他格式:直接在顶层的 model 和 usage
425
- if (parsed.model && !tokenData.model) {
426
- tokenData.model = parsed.model;
427
- }
428
-
429
- if (parsed.usage && tokenData.inputTokens === 0) {
430
- // 兼容 Responses API 和 Chat Completions API
431
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
432
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
433
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
434
- }
435
-
436
- recordUsageIfReady();
437
409
  } catch (err) {
438
410
  // 忽略解析错误
439
411
  }
@@ -446,15 +418,8 @@ async function startCodexProxyServer(options = {}) {
446
418
  if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
447
419
  try {
448
420
  const parsed = JSON.parse(buffer);
449
- if (parsed.model) {
450
- tokenData.model = parsed.model;
451
- }
452
- if (parsed.usage) {
453
- // 兼容两种格式
454
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
455
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
456
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
457
- }
421
+ const usage = parseNonStreamingUsage(parsed);
422
+ mergeUsageIntoTokenData(tokenData, usage);
458
423
  } catch (err) {
459
424
  // 忽略解析错误
460
425
  }
@@ -15,6 +15,7 @@ const { getEffectiveApiKey } = require('./services/gemini-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
17
  const { redirectModel: redirectModelBase, resolveTargetUrl } = require('./services/base/proxy-utils');
18
+ const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
18
19
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
19
20
 
20
21
  let proxyServer = null;
@@ -207,6 +208,14 @@ async function startGeminiProxyServer(options = {}) {
207
208
  // 替换 URL 中的模型名称
208
209
  req.url = req.url.replace(`/models/${originalModel}`, `/models/${redirectedModel}`);
209
210
 
211
+ // 将原始模型和重定向模型存入 metadata,用于日志记录
212
+ const meta = requestMetadata.get(req);
213
+ if (meta) {
214
+ meta.originalModel = originalModel;
215
+ meta.redirectedModel = redirectedModel;
216
+ meta.modelFromUrl = redirectedModel;
217
+ }
218
+
210
219
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
211
220
  const cachedRedirects = printedGeminiRedirectCache.get(channel.id) || {};
212
221
  if (cachedRedirects[originalModel] !== redirectedModel) {
@@ -306,14 +315,7 @@ async function startGeminiProxyServer(options = {}) {
306
315
  });
307
316
 
308
317
  let buffer = '';
309
- let tokenData = {
310
- inputTokens: 0,
311
- outputTokens: 0,
312
- cachedTokens: 0,
313
- reasoningTokens: 0,
314
- totalTokens: 0,
315
- model: ''
316
- };
318
+ let tokenData = createTokenData();
317
319
  let usageRecorded = false;
318
320
  const parsedStream = createDecodedStream(proxyRes);
319
321
 
@@ -333,6 +335,8 @@ async function startGeminiProxyServer(options = {}) {
333
335
  tokens: {
334
336
  input: tokenData.inputTokens,
335
337
  output: tokenData.outputTokens,
338
+ cacheCreation: tokenData.cacheCreation,
339
+ cacheRead: tokenData.cacheRead,
336
340
  cached: tokenData.cachedTokens,
337
341
  reasoning: tokenData.reasoningTokens,
338
342
  total: tokenData.totalTokens
@@ -341,7 +345,7 @@ async function startGeminiProxyServer(options = {}) {
341
345
  broadcastLog,
342
346
  recordRequest: recordGeminiRequest,
343
347
  recordSuccess,
344
- allowBroadcast: !isResponseClosed
348
+ allowBroadcast: true
345
349
  });
346
350
 
347
351
  if (!result) {
@@ -353,7 +357,6 @@ async function startGeminiProxyServer(options = {}) {
353
357
  }
354
358
 
355
359
  parsedStream.on('data', (chunk) => {
356
- // 如果响应已关闭,停止处理
357
360
  if (isResponseClosed) {
358
361
  return;
359
362
  }
@@ -362,56 +365,34 @@ async function startGeminiProxyServer(options = {}) {
362
365
 
363
366
  // 检查是否是 SSE 流
364
367
  if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
365
- // 处理 SSE 事件
366
368
  const events = buffer.split('\n\n');
367
369
  buffer = events.pop() || '';
368
370
 
369
- events.forEach((eventText, index) => {
371
+ events.forEach((eventText) => {
370
372
  if (!eventText.trim()) return;
371
373
 
372
374
  try {
373
375
  const lines = eventText.split('\n');
376
+ let eventType = '';
374
377
  let data = '';
375
378
 
376
379
  lines.forEach(line => {
377
- if (line.startsWith('data:')) {
380
+ if (line.startsWith('event:')) {
381
+ eventType = line.substring(6).trim();
382
+ } else if (line.startsWith('data:')) {
378
383
  data = line.substring(5).trim();
379
384
  }
380
385
  });
381
386
 
382
- if (!data) return;
383
-
384
- if (data === '[DONE]') return;
387
+ if (!data || data === '[DONE]') return;
385
388
 
386
389
  const parsed = JSON.parse(data);
390
+ const usage = parseSSEUsage(parsed, eventType);
391
+ mergeUsageIntoTokenData(tokenData, usage);
387
392
 
388
- // 提取模型信息
389
- if (parsed.model && !tokenData.model) {
390
- tokenData.model = parsed.model;
391
- }
392
-
393
- // 提取 usage 信息 (支持 OpenAI 和 Gemini 原生格式)
394
- if (parsed.usage) {
395
- tokenData.inputTokens = parsed.usage.prompt_tokens || parsed.usage.input_tokens || 0;
396
- tokenData.outputTokens = parsed.usage.completion_tokens || parsed.usage.output_tokens || 0;
397
- tokenData.totalTokens = parsed.usage.total_tokens || 0;
398
-
399
- // Gemini 可能包含缓存信息
400
- if (parsed.usage.prompt_tokens_details) {
401
- tokenData.cachedTokens = parsed.usage.prompt_tokens_details.cached_tokens || 0;
402
- }
403
- } else if (parsed.usageMetadata) {
404
- tokenData.inputTokens = parsed.usageMetadata.promptTokenCount || 0;
405
- tokenData.outputTokens = parsed.usageMetadata.candidatesTokenCount || 0;
406
- tokenData.totalTokens = parsed.usageMetadata.totalTokenCount || 0;
407
-
408
- // Gemini 缓存信息
409
- if (parsed.usageMetadata.cachedContentTokenCount) {
410
- tokenData.cachedTokens = parsed.usageMetadata.cachedContentTokenCount;
411
- }
393
+ if (usage.isDone) {
394
+ recordUsageIfReady();
412
395
  }
413
-
414
- recordUsageIfReady();
415
396
  } catch (err) {
416
397
  // 忽略解析错误
417
398
  }
@@ -424,30 +405,8 @@ async function startGeminiProxyServer(options = {}) {
424
405
  if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
425
406
  try {
426
407
  const parsed = JSON.parse(buffer);
427
- if (parsed.model) {
428
- tokenData.model = parsed.model;
429
- }
430
-
431
- // OpenAI 格式
432
- if (parsed.usage) {
433
- tokenData.inputTokens = parsed.usage.prompt_tokens || parsed.usage.input_tokens || 0;
434
- tokenData.outputTokens = parsed.usage.completion_tokens || parsed.usage.output_tokens || 0;
435
- tokenData.totalTokens = parsed.usage.total_tokens || 0;
436
-
437
- if (parsed.usage.prompt_tokens_details) {
438
- tokenData.cachedTokens = parsed.usage.prompt_tokens_details.cached_tokens || 0;
439
- }
440
- }
441
- // Gemini 原生格式
442
- else if (parsed.usageMetadata) {
443
- tokenData.inputTokens = parsed.usageMetadata.promptTokenCount || 0;
444
- tokenData.outputTokens = parsed.usageMetadata.candidatesTokenCount || 0;
445
- tokenData.totalTokens = parsed.usageMetadata.totalTokenCount || 0;
446
-
447
- if (parsed.usageMetadata.cachedContentTokenCount) {
448
- tokenData.cachedTokens = parsed.usageMetadata.cachedContentTokenCount;
449
- }
450
- }
408
+ const usage = parseNonStreamingUsage(parsed);
409
+ mergeUsageIntoTokenData(tokenData, usage);
451
410
  } catch (err) {
452
411
  // 忽略解析错误
453
412
  }
@@ -22,6 +22,7 @@ const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./se
22
22
  const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
23
23
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
24
24
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
25
+ const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
25
26
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
26
27
 
27
28
  let proxyServer = null;
@@ -4322,6 +4323,13 @@ async function startOpenCodeProxyServer(options = {}) {
4322
4323
  // 更新 rawBody 以匹配修改后的 body
4323
4324
  req.rawBody = Buffer.from(JSON.stringify(req.body));
4324
4325
 
4326
+ // 将原始模型和重定向模型存入 metadata,用于日志记录
4327
+ const meta = requestMetadata.get(req);
4328
+ if (meta) {
4329
+ meta.originalModel = originalModel;
4330
+ meta.redirectedModel = redirectedModel;
4331
+ }
4332
+
4325
4333
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
4326
4334
  const cachedRedirects = printedRedirectCache.get(channel.id) || {};
4327
4335
  if (cachedRedirects[originalModel] !== redirectedModel) {
@@ -4456,14 +4464,7 @@ async function startOpenCodeProxyServer(options = {}) {
4456
4464
  });
4457
4465
 
4458
4466
  let buffer = '';
4459
- let tokenData = {
4460
- inputTokens: 0,
4461
- outputTokens: 0,
4462
- cachedTokens: 0,
4463
- reasoningTokens: 0,
4464
- totalTokens: 0,
4465
- model: ''
4466
- };
4467
+ let tokenData = createTokenData();
4467
4468
  let usageRecorded = false;
4468
4469
 
4469
4470
  function recordUsageIfReady() {
@@ -4478,6 +4479,8 @@ async function startOpenCodeProxyServer(options = {}) {
4478
4479
  tokens: {
4479
4480
  input: tokenData.inputTokens,
4480
4481
  output: tokenData.outputTokens,
4482
+ cacheCreation: tokenData.cacheCreation,
4483
+ cacheRead: tokenData.cacheRead,
4481
4484
  cached: tokenData.cachedTokens,
4482
4485
  reasoning: tokenData.reasoningTokens,
4483
4486
  total: tokenData.totalTokens
@@ -4486,7 +4489,7 @@ async function startOpenCodeProxyServer(options = {}) {
4486
4489
  broadcastLog,
4487
4490
  recordRequest: recordOpenCodeRequest,
4488
4491
  recordSuccess,
4489
- allowBroadcast: !isResponseClosed
4492
+ allowBroadcast: true
4490
4493
  });
4491
4494
 
4492
4495
  if (!result) {
@@ -4498,7 +4501,6 @@ async function startOpenCodeProxyServer(options = {}) {
4498
4501
  }
4499
4502
 
4500
4503
  proxyRes.on('data', (chunk) => {
4501
- // 如果响应已关闭,停止处理
4502
4504
  if (isResponseClosed) {
4503
4505
  return;
4504
4506
  }
@@ -4507,64 +4509,34 @@ async function startOpenCodeProxyServer(options = {}) {
4507
4509
 
4508
4510
  // 检查是否是 SSE 流
4509
4511
  if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
4510
- // 处理 SSE 事件
4511
4512
  const events = buffer.split('\n\n');
4512
4513
  buffer = events.pop() || '';
4513
4514
 
4514
- events.forEach((eventText, index) => {
4515
+ events.forEach((eventText) => {
4515
4516
  if (!eventText.trim()) return;
4516
4517
 
4517
4518
  try {
4518
4519
  const lines = eventText.split('\n');
4520
+ let eventType = '';
4519
4521
  let data = '';
4520
4522
 
4521
4523
  lines.forEach(line => {
4522
- if (line.startsWith('data:')) {
4524
+ if (line.startsWith('event:')) {
4525
+ eventType = line.substring(6).trim();
4526
+ } else if (line.startsWith('data:')) {
4523
4527
  data = line.substring(5).trim();
4524
4528
  }
4525
4529
  });
4526
4530
 
4527
- if (!data) return;
4528
-
4529
- if (data === '[DONE]') return;
4531
+ if (!data || data === '[DONE]') return;
4530
4532
 
4531
4533
  const parsed = JSON.parse(data);
4534
+ const usage = parseSSEUsage(parsed, eventType);
4535
+ mergeUsageIntoTokenData(tokenData, usage);
4532
4536
 
4533
- // OpenAI Responses API: 在 response.completed 事件中获取 usage
4534
- if (parsed.type === 'response.completed' && parsed.response) {
4535
- // 从 response 对象中提取模型和 usage
4536
- if (parsed.response.model) {
4537
- tokenData.model = parsed.response.model;
4538
- }
4539
-
4540
- if (parsed.response.usage) {
4541
- tokenData.inputTokens = parsed.response.usage.input_tokens || 0;
4542
- tokenData.outputTokens = parsed.response.usage.output_tokens || 0;
4543
- tokenData.totalTokens = parsed.response.usage.total_tokens || 0;
4544
-
4545
- // 提取详细信息
4546
- if (parsed.response.usage.input_tokens_details) {
4547
- tokenData.cachedTokens = parsed.response.usage.input_tokens_details.cached_tokens || 0;
4548
- }
4549
- if (parsed.response.usage.output_tokens_details) {
4550
- tokenData.reasoningTokens = parsed.response.usage.output_tokens_details.reasoning_tokens || 0;
4551
- }
4552
- }
4553
- }
4554
-
4555
- // 兼容其他格式:直接在顶层的 model 和 usage
4556
- if (parsed.model && !tokenData.model) {
4557
- tokenData.model = parsed.model;
4558
- }
4559
-
4560
- if (parsed.usage && tokenData.inputTokens === 0) {
4561
- // 兼容 Responses API 和 Chat Completions API
4562
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
4563
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
4564
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
4537
+ if (usage.isDone) {
4538
+ recordUsageIfReady();
4565
4539
  }
4566
-
4567
- recordUsageIfReady();
4568
4540
  } catch (err) {
4569
4541
  // 忽略解析错误
4570
4542
  }
@@ -4577,15 +4549,8 @@ async function startOpenCodeProxyServer(options = {}) {
4577
4549
  if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
4578
4550
  try {
4579
4551
  const parsed = JSON.parse(buffer);
4580
- if (parsed.model) {
4581
- tokenData.model = parsed.model;
4582
- }
4583
- if (parsed.usage) {
4584
- // 兼容两种格式
4585
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
4586
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
4587
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
4588
- }
4552
+ const usage = parseNonStreamingUsage(parsed);
4553
+ mergeUsageIntoTokenData(tokenData, usage);
4589
4554
  } catch (err) {
4590
4555
  // 忽略解析错误
4591
4556
  }
@@ -20,6 +20,7 @@ const { getEffectiveApiKey } = require('./services/channels');
20
20
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
21
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
22
22
  const { redirectModel } = require('./services/base/proxy-utils');
23
+ const { parseSSEUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
23
24
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
24
25
 
25
26
  let proxyServer = null;
@@ -294,6 +295,13 @@ async function startProxyServer(options = {}) {
294
295
  // 更新 rawBody 以匹配修改后的 body
295
296
  req.rawBody = Buffer.from(JSON.stringify(req.body));
296
297
 
298
+ // 将原始模型和重定向模型存入 metadata,用于日志记录
299
+ const meta = requestMetadata.get(req);
300
+ if (meta) {
301
+ meta.originalModel = originalModel;
302
+ meta.redirectedModel = redirectedModel;
303
+ }
304
+
297
305
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
298
306
  const cachedRedirects = printedRedirectCache.get(channel.id) || {};
299
307
  if (cachedRedirects[originalModel] !== redirectedModel) {
@@ -387,13 +395,7 @@ async function startProxyServer(options = {}) {
387
395
  });
388
396
 
389
397
  let buffer = '';
390
- let tokenData = {
391
- inputTokens: 0,
392
- outputTokens: 0,
393
- cacheCreation: 0,
394
- cacheRead: 0,
395
- model: ''
396
- };
398
+ let tokenData = createTokenData();
397
399
  let usageRecorded = false;
398
400
  const parsedStream = createDecodedStream(proxyRes);
399
401
 
@@ -408,13 +410,16 @@ async function startProxyServer(options = {}) {
408
410
  input: tokenData.inputTokens,
409
411
  output: tokenData.outputTokens,
410
412
  cacheCreation: tokenData.cacheCreation,
411
- cacheRead: tokenData.cacheRead
413
+ cacheRead: tokenData.cacheRead,
414
+ cached: tokenData.cachedTokens,
415
+ reasoning: tokenData.reasoningTokens,
416
+ total: tokenData.totalTokens
412
417
  },
413
418
  calculateCost,
414
419
  broadcastLog,
415
420
  recordRequest,
416
421
  recordSuccess,
417
- allowBroadcast: !isResponseClosed
422
+ allowBroadcast: true
418
423
  });
419
424
 
420
425
  if (!result) return false;
@@ -446,30 +451,13 @@ async function startProxyServer(options = {}) {
446
451
  }
447
452
  });
448
453
 
449
- if (!data) return;
454
+ if (!data || data === '[DONE]') return;
450
455
 
451
456
  const parsed = JSON.parse(data);
457
+ const usage = parseSSEUsage(parsed, eventType);
458
+ mergeUsageIntoTokenData(tokenData, usage);
452
459
 
453
- if (eventType === 'message_start' && parsed.message && parsed.message.model) {
454
- tokenData.model = parsed.message.model;
455
- }
456
-
457
- if (parsed.usage) {
458
- if (parsed.usage.input_tokens !== undefined) {
459
- tokenData.inputTokens = parsed.usage.input_tokens;
460
- }
461
- if (parsed.usage.output_tokens !== undefined) {
462
- tokenData.outputTokens = parsed.usage.output_tokens;
463
- }
464
- if (parsed.usage.cache_creation_input_tokens !== undefined) {
465
- tokenData.cacheCreation = parsed.usage.cache_creation_input_tokens;
466
- }
467
- if (parsed.usage.cache_read_input_tokens !== undefined) {
468
- tokenData.cacheRead = parsed.usage.cache_read_input_tokens;
469
- }
470
- }
471
-
472
- if (eventType === 'message_stop') {
460
+ if (usage.isDone) {
473
461
  recordUsageIfReady();
474
462
  }
475
463
  } catch (err) {