coding-tool-x 3.5.4 → 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.
- package/CHANGELOG.md +7 -0
- package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-gvYu5sCM.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-CPlH8Ehd.js} +1 -1
- package/dist/web/assets/{Home-BYtCM3rK.js → Home-B-qbu3uk.js} +1 -1
- package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-B2tQ_YUq.js} +1 -1
- package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-kDadoXXs.js} +1 -1
- package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-eLgITwTV.js} +1 -1
- package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-B7zEB5Op.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C-RzB3ud.js} +1 -1
- package/dist/web/assets/{index-B02wDWNC.css → index-BHeh2z0i.css} +1 -1
- package/dist/web/assets/index-DG00t-zy.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/codex-proxy-server.js +24 -59
- package/src/server/gemini-proxy-server.js +25 -66
- package/src/server/opencode-proxy-server.js +24 -59
- package/src/server/proxy-server.js +18 -30
- package/src/server/services/base/response-usage-parser.js +187 -0
- package/src/server/services/proxy-log-helper.js +21 -3
- package/src/server/services/statistics-service.js +7 -0
- package/dist/web/assets/index-CHwVofQH.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DG00t-zy.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-aWwwFAao.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendors-Fza9uSYn.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-BaTCPPL5.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/icons-DlxD2wZJ.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BHeh2z0i.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
|
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('
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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
|
-
|
|
4534
|
-
|
|
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
|
-
|
|
4581
|
-
|
|
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:
|
|
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 (
|
|
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) {
|