aicodeswitch 5.2.13 → 6.0.1
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/bin/restore.js +67 -4
- package/bin/utils/managed-fields.js +12 -0
- package/dist/server/agent-map/activity-extractor.js +8 -0
- package/dist/server/agent-map/agent-map-service.js +364 -65
- package/dist/server/coding-plan.js +3 -0
- package/dist/server/config-managed-fields.js +20 -1
- package/dist/server/config-metadata.js +78 -1
- package/dist/server/conversions/index.js +10 -2
- package/dist/server/conversions/thinking/providers.js +12 -7
- package/dist/server/fs-database.js +10 -1
- package/dist/server/main.js +412 -37
- package/dist/server/original-config-reader.js +71 -1
- package/dist/server/performance-tracker.js +78 -8
- package/dist/server/proxy-server.js +159 -34
- package/dist/server/session-migration.js +1 -0
- package/dist/server/transformers/chunk-collector.js +112 -25
- package/dist/ui/assets/index-BeM4AIzn.js +1187 -0
- package/dist/ui/assets/index-DqRzbMIU.css +1 -0
- package/dist/ui/assets/three-CFpmPosW.js +3802 -0
- package/dist/ui/index.html +3 -2
- package/package.json +3 -1
- package/scripts/dev.js +76 -30
- package/dist/server/tools-service.js +0 -203
- package/dist/server/websocket-service.js +0 -148
- package/dist/ui/assets/index-4liE4bV8.css +0 -1
- package/dist/ui/assets/index-CJMAVkIN.js +0 -813
|
@@ -66,7 +66,14 @@ const compact_1 = require("./conversions/compact");
|
|
|
66
66
|
const coding_plan_1 = require("./coding-plan");
|
|
67
67
|
const coding_plan_headers_1 = require("./coding-plan-headers");
|
|
68
68
|
const auth_1 = require("./auth");
|
|
69
|
-
const SUPPORTED_TARGETS = ['claude-code', 'codex'];
|
|
69
|
+
const SUPPORTED_TARGETS = ['claude-code', 'codex', 'opencode'];
|
|
70
|
+
/**
|
|
71
|
+
* Fallback(回退原始配置)路径的虚拟供应商归属。
|
|
72
|
+
* 该路径转发的请求不属于任何用户配置的供应商/服务,故用一组固定 ID + 名称
|
|
73
|
+
* 把它纳入服务性能统计(service-performance.json),便于在测速面板单独呈现。
|
|
74
|
+
*/
|
|
75
|
+
const FALLBACK_VENDOR_ID = 'fallback-vendor';
|
|
76
|
+
const FALLBACK_VENDOR_NAME = '原始配置 / 直连';
|
|
70
77
|
/** 默认模型列表 */
|
|
71
78
|
const DEFAULT_MODELS = [
|
|
72
79
|
{ id: 'claude-sonnet-4-20250514', owned_by: 'anthropic' },
|
|
@@ -122,6 +129,19 @@ function apiPathToClientFormat(apiPath) {
|
|
|
122
129
|
case '/v1/models': return null;
|
|
123
130
|
}
|
|
124
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* 根据客户端工具类型推断其原生 API 格式(Format)。
|
|
134
|
+
* - claude-code → claude(Anthropic Messages)
|
|
135
|
+
* - codex → responses(OpenAI Responses)
|
|
136
|
+
* - opencode → completions(OpenAI Chat Completions,经由 @ai-sdk/openai-compatible)
|
|
137
|
+
*/
|
|
138
|
+
function clientFormatForTool(tool) {
|
|
139
|
+
if (tool === 'codex')
|
|
140
|
+
return 'responses';
|
|
141
|
+
if (tool === 'opencode')
|
|
142
|
+
return 'completions';
|
|
143
|
+
return 'claude';
|
|
144
|
+
}
|
|
125
145
|
class ClaudeCompactResponseSanitizer extends stream_1.Transform {
|
|
126
146
|
constructor() {
|
|
127
147
|
super({ objectMode: true });
|
|
@@ -275,12 +295,16 @@ class ProxyServer {
|
|
|
275
295
|
* 流式:依据 streamTiming 精确计算 TTFT 与生成阶段吞吐;非流式:端到端估算(estimated)。
|
|
276
296
|
*/
|
|
277
297
|
emitPerformance(params) {
|
|
298
|
+
var _a;
|
|
278
299
|
const tracker = this.performanceTracker;
|
|
279
300
|
if (!tracker)
|
|
280
301
|
return;
|
|
281
302
|
const { statusCode, startTime, usage, streamTiming, service, vendorId, vendorName, model } = params;
|
|
282
303
|
const isError = statusCode >= 400;
|
|
304
|
+
const inputTokens = usage === null || usage === void 0 ? void 0 : usage.inputTokens;
|
|
283
305
|
const outputTokens = usage === null || usage === void 0 ? void 0 : usage.outputTokens;
|
|
306
|
+
const computedTotal = (inputTokens || 0) + (outputTokens || 0);
|
|
307
|
+
const totalTokens = (_a = usage === null || usage === void 0 ? void 0 : usage.totalTokens) !== null && _a !== void 0 ? _a : (computedTotal > 0 ? computedTotal : undefined);
|
|
284
308
|
const responseMs = Date.now() - startTime;
|
|
285
309
|
let ttftMs;
|
|
286
310
|
let tokensPerSecond;
|
|
@@ -296,7 +320,11 @@ class ProxyServer {
|
|
|
296
320
|
else if (outputTokens && responseMs > 0) {
|
|
297
321
|
tokensPerSecond = outputTokens / (responseMs / 1000);
|
|
298
322
|
}
|
|
299
|
-
|
|
323
|
+
// 解析最终归属:Fallback 临时服务没有真实 vendor,service.vendorId 兜底为虚拟供应商;
|
|
324
|
+
// 此时 vendorName 也为空,补上虚拟供应商名,确保不被 recordPerformance 的三元组校验丢弃。
|
|
325
|
+
const effectiveVendorId = vendorId !== null && vendorId !== void 0 ? vendorId : service.vendorId;
|
|
326
|
+
const effectiveVendorName = vendorName !== null && vendorName !== void 0 ? vendorName : (effectiveVendorId === FALLBACK_VENDOR_ID ? FALLBACK_VENDOR_NAME : undefined);
|
|
327
|
+
tracker.recordPerformance(effectiveVendorId, effectiveVendorName, service.id, service.name, model, { ttftMs, tokensPerSecond, outputTokens, inputTokens, totalTokens, timingAccuracy, isError });
|
|
300
328
|
}
|
|
301
329
|
/**
|
|
302
330
|
* 从请求中提取 API Key(支持三种 Header,按优先级依次尝试)
|
|
@@ -364,6 +392,9 @@ class ProxyServer {
|
|
|
364
392
|
if (path === '/codex' || path.startsWith('/codex/')) {
|
|
365
393
|
return 'codex';
|
|
366
394
|
}
|
|
395
|
+
if (path === '/opencode' || path.startsWith('/opencode/')) {
|
|
396
|
+
return 'opencode';
|
|
397
|
+
}
|
|
367
398
|
return undefined;
|
|
368
399
|
}
|
|
369
400
|
inferToolFromRequest(req) {
|
|
@@ -372,6 +403,8 @@ class ProxyServer {
|
|
|
372
403
|
return 'claude-code';
|
|
373
404
|
if (path.startsWith('/codex'))
|
|
374
405
|
return 'codex';
|
|
406
|
+
if (path.startsWith('/opencode'))
|
|
407
|
+
return 'opencode';
|
|
375
408
|
return 'claude-code';
|
|
376
409
|
}
|
|
377
410
|
buildRelayTags(relayed, useOriginalConfig = false) {
|
|
@@ -732,7 +765,7 @@ class ProxyServer {
|
|
|
732
765
|
tags: this.buildRelayTags(hasRelayAttempt),
|
|
733
766
|
});
|
|
734
767
|
// 确定目标类型
|
|
735
|
-
const targetType =
|
|
768
|
+
const targetType = this.inferToolFromRequest(req);
|
|
736
769
|
// 记录错误日志 - 包含请求详情和最后失败的服务信息
|
|
737
770
|
const _lastFailedVendor = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
|
|
738
771
|
yield this.dbManager.addErrorLog({
|
|
@@ -798,7 +831,7 @@ class ProxyServer {
|
|
|
798
831
|
}
|
|
799
832
|
// Add error log - 包含请求详情
|
|
800
833
|
if (!accessKeyCtx) {
|
|
801
|
-
const targetType =
|
|
834
|
+
const targetType = this.inferToolFromRequest(req);
|
|
802
835
|
yield this.dbManager.addErrorLog({
|
|
803
836
|
timestamp: Date.now(),
|
|
804
837
|
method: req.method,
|
|
@@ -840,6 +873,8 @@ class ProxyServer {
|
|
|
840
873
|
this.app.use('/claude-code', this.createFixedRouteHandler('claude-code'));
|
|
841
874
|
this.app.use('/codex/', this.createFixedRouteHandler('codex'));
|
|
842
875
|
this.app.use('/codex', this.createFixedRouteHandler('codex'));
|
|
876
|
+
this.app.use('/opencode/', this.createFixedRouteHandler('opencode'));
|
|
877
|
+
this.app.use('/opencode', this.createFixedRouteHandler('opencode'));
|
|
843
878
|
}
|
|
844
879
|
createFixedRouteHandler(targetType) {
|
|
845
880
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1169,6 +1204,9 @@ class ProxyServer {
|
|
|
1169
1204
|
else if (req.path.startsWith('/codex/')) {
|
|
1170
1205
|
tool = 'codex';
|
|
1171
1206
|
}
|
|
1207
|
+
else if (req.path.startsWith('/opencode/')) {
|
|
1208
|
+
tool = 'opencode';
|
|
1209
|
+
}
|
|
1172
1210
|
if (!tool)
|
|
1173
1211
|
return undefined;
|
|
1174
1212
|
// 优先检查会话级路由绑定
|
|
@@ -1209,6 +1247,9 @@ class ProxyServer {
|
|
|
1209
1247
|
else if (req.path.startsWith('/codex/')) {
|
|
1210
1248
|
targetType = 'codex';
|
|
1211
1249
|
}
|
|
1250
|
+
else if (req.path.startsWith('/opencode/')) {
|
|
1251
|
+
targetType = 'opencode';
|
|
1252
|
+
}
|
|
1212
1253
|
if (!targetType) {
|
|
1213
1254
|
return false;
|
|
1214
1255
|
}
|
|
@@ -1260,10 +1301,11 @@ class ProxyServer {
|
|
|
1260
1301
|
const tempService = {
|
|
1261
1302
|
id: 'fallback-service',
|
|
1262
1303
|
name: 'Original Config',
|
|
1304
|
+
vendorId: FALLBACK_VENDOR_ID,
|
|
1263
1305
|
apiUrl: originalConfig.apiUrl,
|
|
1264
1306
|
apiKey: originalConfig.apiKey,
|
|
1265
1307
|
authType: originalConfig.authType,
|
|
1266
|
-
sourceType: originalConfig.sourceType || (targetType === 'claude-code' ? 'claude' : 'openai'),
|
|
1308
|
+
sourceType: originalConfig.sourceType || (targetType === 'claude-code' ? 'claude' : targetType === 'opencode' ? 'openai-chat' : 'openai'),
|
|
1267
1309
|
createdAt: Date.now(),
|
|
1268
1310
|
updatedAt: Date.now(),
|
|
1269
1311
|
};
|
|
@@ -3094,6 +3136,16 @@ class ProxyServer {
|
|
|
3094
3136
|
return sessionId[0] || null;
|
|
3095
3137
|
}
|
|
3096
3138
|
}
|
|
3139
|
+
else if (type === 'opencode') {
|
|
3140
|
+
// OpenCode 经 @ai-sdk/openai-compatible 发送 chat completions,尝试从 headers 提取 session 标识
|
|
3141
|
+
const sessionId = request.headers['session-id'] || request.headers['session_id'] || request.headers['x-session-id'];
|
|
3142
|
+
if (typeof sessionId === 'string') {
|
|
3143
|
+
return sessionId;
|
|
3144
|
+
}
|
|
3145
|
+
if (Array.isArray(sessionId)) {
|
|
3146
|
+
return sessionId[0] || null;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3097
3149
|
return null;
|
|
3098
3150
|
}
|
|
3099
3151
|
/**
|
|
@@ -3283,6 +3335,14 @@ class ProxyServer {
|
|
|
3283
3335
|
if (tool === 'codex' && this.isClaudeSource(source)) {
|
|
3284
3336
|
return `${apiUrl}/v1/messages`;
|
|
3285
3337
|
}
|
|
3338
|
+
// opencode 请求 claude 类型接口,使用 claude messages 接口处理
|
|
3339
|
+
if (tool === 'opencode' && this.isClaudeSource(source)) {
|
|
3340
|
+
return `${apiUrl}/v1/messages`;
|
|
3341
|
+
}
|
|
3342
|
+
// opencode 请求 openai(responses) 类型接口,使用 responses 接口处理
|
|
3343
|
+
if (tool === 'opencode' && this.isOpenAISource(source)) {
|
|
3344
|
+
return `${apiUrl}/v1/responses`;
|
|
3345
|
+
}
|
|
3286
3346
|
// 透传路径
|
|
3287
3347
|
return `${apiUrl}${originalPath}`;
|
|
3288
3348
|
}
|
|
@@ -3296,7 +3356,7 @@ class ProxyServer {
|
|
|
3296
3356
|
* @returns 转换后往服务商API接口的数据
|
|
3297
3357
|
*/
|
|
3298
3358
|
transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig, sanitizeBody) {
|
|
3299
|
-
const clientFormat = tool
|
|
3359
|
+
const clientFormat = clientFormatForTool(tool);
|
|
3300
3360
|
const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(source);
|
|
3301
3361
|
const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig, sanitizeBody });
|
|
3302
3362
|
const body = result.body;
|
|
@@ -3316,7 +3376,7 @@ class ProxyServer {
|
|
|
3316
3376
|
* @param responseData
|
|
3317
3377
|
*/
|
|
3318
3378
|
transformResponseToTool(tool, source, responseData) {
|
|
3319
|
-
const clientFormat = tool
|
|
3379
|
+
const clientFormat = clientFormatForTool(tool);
|
|
3320
3380
|
const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(source);
|
|
3321
3381
|
return (0, index_1.transformResponse)({ fromFormat: upstreamFormat, toFormat: clientFormat, response: responseData });
|
|
3322
3382
|
}
|
|
@@ -3327,7 +3387,7 @@ class ProxyServer {
|
|
|
3327
3387
|
* @returns 转换器实例和相关信息
|
|
3328
3388
|
*/
|
|
3329
3389
|
transformSSEToTool(targetType, sourceType) {
|
|
3330
|
-
const clientFormat = targetType
|
|
3390
|
+
const clientFormat = clientFormatForTool(targetType);
|
|
3331
3391
|
const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(sourceType);
|
|
3332
3392
|
if (upstreamFormat === clientFormat) {
|
|
3333
3393
|
return { converter: null };
|
|
@@ -3347,8 +3407,28 @@ class ProxyServer {
|
|
|
3347
3407
|
});
|
|
3348
3408
|
return { converter: adapter, extractUsage };
|
|
3349
3409
|
}
|
|
3410
|
+
/**
|
|
3411
|
+
* 将 SSEEventCollector 归一化后的 usage({input_tokens, output_tokens, total_tokens, cache_read_input_tokens})
|
|
3412
|
+
* 映射为 TokenUsage。collector 已遍历全部事件并合并 Anthropic message_start/message_delta + OpenAI/Gemini 字段,
|
|
3413
|
+
* 因此这里无需再区分上游格式。
|
|
3414
|
+
*/
|
|
3415
|
+
tokenUsageFromCollected(extracted) {
|
|
3416
|
+
var _a;
|
|
3417
|
+
if (!extracted)
|
|
3418
|
+
return undefined;
|
|
3419
|
+
const inputTokens = extracted.input_tokens || 0;
|
|
3420
|
+
const outputTokens = extracted.output_tokens || 0;
|
|
3421
|
+
const computedTotal = inputTokens + outputTokens;
|
|
3422
|
+
const totalTokens = (_a = extracted.total_tokens) !== null && _a !== void 0 ? _a : (computedTotal > 0 ? computedTotal : undefined);
|
|
3423
|
+
return {
|
|
3424
|
+
inputTokens,
|
|
3425
|
+
outputTokens,
|
|
3426
|
+
totalTokens,
|
|
3427
|
+
cacheReadInputTokens: extracted.cache_read_input_tokens || 0,
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3350
3430
|
extractTokenUsageFromResponse(responseData, sourceType) {
|
|
3351
|
-
var _a, _b, _c, _d, _e, _f;
|
|
3431
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
3352
3432
|
if (!responseData)
|
|
3353
3433
|
return undefined;
|
|
3354
3434
|
const format = (0, source_type_mapping_1.sourceTypeToFormat)(sourceType);
|
|
@@ -3364,14 +3444,15 @@ class ProxyServer {
|
|
|
3364
3444
|
};
|
|
3365
3445
|
}
|
|
3366
3446
|
if (format === 'completions' || format === 'responses') {
|
|
3367
|
-
|
|
3447
|
+
// 标准 Responses 非流式 usage 在顶层;个别上游/中转会嵌在 response.usage 下,做兜底
|
|
3448
|
+
const usage = (_a = responseData === null || responseData === void 0 ? void 0 : responseData.usage) !== null && _a !== void 0 ? _a : (_b = responseData === null || responseData === void 0 ? void 0 : responseData.response) === null || _b === void 0 ? void 0 : _b.usage;
|
|
3368
3449
|
if (!usage)
|
|
3369
3450
|
return undefined;
|
|
3370
3451
|
return {
|
|
3371
3452
|
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || (usage === null || usage === void 0 ? void 0 : usage.prompt_tokens) || 0,
|
|
3372
3453
|
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || (usage === null || usage === void 0 ? void 0 : usage.completion_tokens) || 0,
|
|
3373
3454
|
totalTokens: (usage === null || usage === void 0 ? void 0 : usage.total_tokens) || 0,
|
|
3374
|
-
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cached_tokens) || (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
3455
|
+
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cached_tokens) || (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || ((_c = usage === null || usage === void 0 ? void 0 : usage.input_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens) || 0,
|
|
3375
3456
|
};
|
|
3376
3457
|
}
|
|
3377
3458
|
if (format === 'claude') {
|
|
@@ -3379,7 +3460,7 @@ class ProxyServer {
|
|
|
3379
3460
|
return {
|
|
3380
3461
|
inputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) || 0,
|
|
3381
3462
|
outputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) || 0,
|
|
3382
|
-
totalTokens: ((
|
|
3463
|
+
totalTokens: ((_d = responseData === null || responseData === void 0 ? void 0 : responseData.input_tokens) !== null && _d !== void 0 ? _d : 0) + ((_e = responseData === null || responseData === void 0 ? void 0 : responseData.output_tokens) !== null && _e !== void 0 ? _e : 0) || undefined,
|
|
3383
3464
|
cacheReadInputTokens: (responseData === null || responseData === void 0 ? void 0 : responseData.cache_read_input_tokens) || 0,
|
|
3384
3465
|
};
|
|
3385
3466
|
}
|
|
@@ -3389,7 +3470,7 @@ class ProxyServer {
|
|
|
3389
3470
|
return {
|
|
3390
3471
|
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
3391
3472
|
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
3392
|
-
totalTokens: ((
|
|
3473
|
+
totalTokens: ((_f = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _f !== void 0 ? _f : 0) + ((_g = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _g !== void 0 ? _g : 0) || undefined,
|
|
3393
3474
|
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
3394
3475
|
};
|
|
3395
3476
|
}
|
|
@@ -3409,7 +3490,7 @@ class ProxyServer {
|
|
|
3409
3490
|
return {
|
|
3410
3491
|
inputTokens: (usage === null || usage === void 0 ? void 0 : usage.input_tokens) || 0,
|
|
3411
3492
|
outputTokens: (usage === null || usage === void 0 ? void 0 : usage.output_tokens) || 0,
|
|
3412
|
-
totalTokens: ((
|
|
3493
|
+
totalTokens: ((_h = usage === null || usage === void 0 ? void 0 : usage.input_tokens) !== null && _h !== void 0 ? _h : 0) + ((_j = usage === null || usage === void 0 ? void 0 : usage.output_tokens) !== null && _j !== void 0 ? _j : 0) || undefined,
|
|
3413
3494
|
cacheReadInputTokens: (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) || 0,
|
|
3414
3495
|
};
|
|
3415
3496
|
}
|
|
@@ -3427,10 +3508,37 @@ class ProxyServer {
|
|
|
3427
3508
|
const sessionId = this.defaultExtractSessionId(req, targetType) || '-';
|
|
3428
3509
|
// Agent Map:在途请求注册(active 状态判定依据)
|
|
3429
3510
|
const _accessKeyCtxAtStart = req._accessKeyCtx;
|
|
3511
|
+
// 后台类请求(count_tokens / compact / background 规则)不重置「本轮通知标记」,避免主任务结束后的
|
|
3512
|
+
// 续发请求重复触发结束通知
|
|
3513
|
+
const _isBgRequest = rule.contentType === 'background'
|
|
3514
|
+
|| rule.contentType === 'compact'
|
|
3515
|
+
|| this.isCountTokensPath(req.path)
|
|
3516
|
+
|| this.isCountTokensPath(req.originalUrl);
|
|
3517
|
+
// 思考类请求:复用路由同款识别(rule.contentType==='thinking' 或请求体带 thinking/reasoning 信号)。
|
|
3518
|
+
// 思考期间用更宽的静默上限,避免思考静默被误判为「上游停滞」而弹通知。
|
|
3519
|
+
const _isThinking = rule.contentType === 'thinking' || this.hasThinkingSignal(req.body);
|
|
3520
|
+
res.locals._agentMapThinking = _isThinking;
|
|
3430
3521
|
agent_map_1.agentMapService.startRequest(sessionId, targetType, {
|
|
3431
3522
|
source: _accessKeyCtxAtStart ? 'access-key' : 'global',
|
|
3432
3523
|
keyId: (_a = _accessKeyCtxAtStart === null || _accessKeyCtxAtStart === void 0 ? void 0 : _accessKeyCtxAtStart.accessKey) === null || _a === void 0 ? void 0 : _a.id,
|
|
3433
3524
|
keyName: (_b = _accessKeyCtxAtStart === null || _accessKeyCtxAtStart === void 0 ? void 0 : _accessKeyCtxAtStart.accessKey) === null || _b === void 0 ? void 0 : _b.name,
|
|
3525
|
+
background: _isBgRequest,
|
|
3526
|
+
thinking: _isThinking,
|
|
3527
|
+
});
|
|
3528
|
+
// 安全网:无论走哪条分支(含编程套餐拒绝、配额拦截、异常等早退,导致 finalizeLog 未被调用),
|
|
3529
|
+
// 都在响应关闭时确保 endRequest 配对执行一次,杜绝在途计数泄漏(「永远卡在进行中」)。
|
|
3530
|
+
res.on('close', () => {
|
|
3531
|
+
// 仅保证 endRequest 配对执行一次(防在途计数泄漏)。用独立标记 _agentMapEnded,
|
|
3532
|
+
// 不再与 finalizeLog 的 onFinalized 共用标记 —— 否则 res 'close' 先于 finalizeLog 触发时
|
|
3533
|
+
// 会把标记置位,导致 finalizeLog 跳过 onFinalized(requestCount / model / token 永不更新)。
|
|
3534
|
+
if (res.locals._agentMapEnded)
|
|
3535
|
+
return;
|
|
3536
|
+
res.locals._agentMapEnded = true;
|
|
3537
|
+
agent_map_1.agentMapService.endRequest(sessionId, {
|
|
3538
|
+
isStream: !!res.locals._agentMapStream,
|
|
3539
|
+
thinking: !!res.locals._agentMapThinking,
|
|
3540
|
+
});
|
|
3541
|
+
agent_map_1.agentMapService.reevaluate(sessionId);
|
|
3434
3542
|
});
|
|
3435
3543
|
const vendor = this.dbManager.getVendorByServiceId(service.id);
|
|
3436
3544
|
console.log(`\x1b[32m[Request Start]\x1b[0m client=${targetType}, session=${sessionId}, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}, model=${rule.targetModel || ((_c = req.body) === null || _c === void 0 ? void 0 : _c.model) || '-'}`);
|
|
@@ -3450,7 +3558,7 @@ class ProxyServer {
|
|
|
3450
3558
|
let logged = false;
|
|
3451
3559
|
const extraTagsForLog = [];
|
|
3452
3560
|
// 编程套餐限制检查
|
|
3453
|
-
const clientFormat = targetType
|
|
3561
|
+
const clientFormat = clientFormatForTool(targetType);
|
|
3454
3562
|
if (!this.checkCodingPlan(req, res, service, clientFormat))
|
|
3455
3563
|
return;
|
|
3456
3564
|
// Compact 请求消息清理:确保 tool_use/tool_result 配对完整
|
|
@@ -3611,10 +3719,19 @@ class ProxyServer {
|
|
|
3611
3719
|
service, vendorId: vendor === null || vendor === void 0 ? void 0 : vendor.id, vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3612
3720
|
model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
3613
3721
|
});
|
|
3614
|
-
// Agent Map:在途请求注销 + 活动/状态采集广播(独立于 enableLogging
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3722
|
+
// Agent Map:在途请求注销 + 活动/状态采集广播(独立于 enableLogging)
|
|
3723
|
+
// 注意:endRequest 与 onFinalized 用各自独立的标记,二者解耦。
|
|
3724
|
+
// 这样即便 res 'close' 安全网已先执行 endRequest(_agentMapEnded=true),onFinalized 仍会在此执行 ——
|
|
3725
|
+
// 修复「requestCount / model / token 不更新」:旧实现二者共用 _agentMapRecorded,'close' 抢先置位会让此处整块跳过。
|
|
3726
|
+
if (!res.locals._agentMapEnded) {
|
|
3727
|
+
res.locals._agentMapEnded = true;
|
|
3728
|
+
agent_map_1.agentMapService.endRequest(sessionId, {
|
|
3729
|
+
isStream: !!res.locals._agentMapStream,
|
|
3730
|
+
thinking: !!res.locals._agentMapThinking,
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
if (!res.locals._agentMapFinalized) {
|
|
3734
|
+
res.locals._agentMapFinalized = true;
|
|
3618
3735
|
const _akCtx = req._accessKeyCtx;
|
|
3619
3736
|
const _tokensDelta = (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) ||
|
|
3620
3737
|
(((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0));
|
|
@@ -3631,6 +3748,9 @@ class ProxyServer {
|
|
|
3631
3748
|
// 避免规则未配置 targetModel 时回退成编程工具提交的 req.body.model
|
|
3632
3749
|
model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
3633
3750
|
tokensDelta: _tokensDelta,
|
|
3751
|
+
// 输入/输出拆分(3D 连线两段用);usageForLog 已规范化为驼峰字段
|
|
3752
|
+
inputTokensDelta: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0,
|
|
3753
|
+
outputTokensDelta: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0,
|
|
3634
3754
|
body: req.body,
|
|
3635
3755
|
downstreamResponseBody: downstreamResponseBodyForLog !== null && downstreamResponseBodyForLog !== void 0 ? downstreamResponseBodyForLog : responseBodyForLog,
|
|
3636
3756
|
responseBody: responseBodyForLog,
|
|
@@ -3806,6 +3926,9 @@ class ProxyServer {
|
|
|
3806
3926
|
serviceName: service.name,
|
|
3807
3927
|
model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || ((_o = req.body) === null || _o === void 0 ? void 0 : _o.model),
|
|
3808
3928
|
totalTokens,
|
|
3929
|
+
// 输入/输出拆分持久化(与 totalTokens 同口径),供 Agent Map 重启后恢复
|
|
3930
|
+
inputTokens: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0,
|
|
3931
|
+
outputTokens: (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0,
|
|
3809
3932
|
highIqMode: rule.contentType === 'high-iq' ? true : existingSession === null || existingSession === void 0 ? void 0 : existingSession.highIqMode,
|
|
3810
3933
|
highIqRuleId: rule.contentType === 'high-iq' ? rule.id : existingSession === null || existingSession === void 0 ? void 0 : existingSession.highIqRuleId,
|
|
3811
3934
|
highIqEnabledAt: rule.contentType === 'high-iq'
|
|
@@ -3998,6 +4121,9 @@ class ProxyServer {
|
|
|
3998
4121
|
else if (targetType === 'codex' && req.path.startsWith('/codex')) {
|
|
3999
4122
|
pathToRequest = req.path.slice('/codex'.length);
|
|
4000
4123
|
}
|
|
4124
|
+
else if (targetType === 'opencode' && req.path.startsWith('/opencode')) {
|
|
4125
|
+
pathToRequest = req.path.slice('/opencode'.length);
|
|
4126
|
+
}
|
|
4001
4127
|
// 使用 mapRequestPathToUpstreamUrl 统一构建上游 URL
|
|
4002
4128
|
const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
|
|
4003
4129
|
const apiUrl = this.resolveEffectiveApiUrl(service);
|
|
@@ -4149,9 +4275,19 @@ class ProxyServer {
|
|
|
4149
4275
|
const serializer = new streaming_1.SSESerializerTransform();
|
|
4150
4276
|
const downstreamChunkCollector = new chunk_collector_1.ChunkCollectorTransform(() => {
|
|
4151
4277
|
rules_status_service_1.rulesStatusBroadcaster.refreshRuleInUse(route.id, rule.id);
|
|
4278
|
+
// Agent Map 心跳:每个流经的下游 chunk 都视为「会话仍活跃」,刷新活动时钟(节流由 ChunkCollector 内部 5s 上限控制)
|
|
4279
|
+
// 同时把当前流式请求的实时累计 usage(input/output)一并推送,使前端节点在流式过程中随 token 增长实时上移
|
|
4280
|
+
const u = eventCollector.extractUsage();
|
|
4281
|
+
agent_map_1.agentMapService.heartbeat(sessionId, u ? {
|
|
4282
|
+
inputTokens: u.input_tokens || 0,
|
|
4283
|
+
outputTokens: u.output_tokens || 0,
|
|
4284
|
+
} : undefined);
|
|
4152
4285
|
});
|
|
4153
4286
|
// 服务性能打点:记录首/末 SSE 事件时间,用于 TTFT 与生成阶段吞吐
|
|
4154
4287
|
streamTiming = new stream_timing_transform_1.StreamTimingTransform(startTime);
|
|
4288
|
+
// Agent Map:SSE 流已建立 —— 记一个在途流,状态机据此区分「同步/流式」活跃判定
|
|
4289
|
+
res.locals._agentMapStream = true;
|
|
4290
|
+
agent_map_1.agentMapService.markStreaming(sessionId);
|
|
4155
4291
|
const compactResponseSanitizer = rule.contentType === 'compact' && targetType === 'claude-code'
|
|
4156
4292
|
? new ClaudeCompactResponseSanitizer()
|
|
4157
4293
|
: null;
|
|
@@ -4160,7 +4296,7 @@ class ProxyServer {
|
|
|
4160
4296
|
const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
|
|
4161
4297
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4162
4298
|
// 使用 transformSSEToTool 方法选择转换器
|
|
4163
|
-
const { converter
|
|
4299
|
+
const { converter } = this.transformSSEToTool(targetType, sourceType);
|
|
4164
4300
|
this.copyResponseHeaders(responseHeaders, res);
|
|
4165
4301
|
// 收集日志:responseBody/streamChunks 记录上游原始响应;downstreamResponseBody 记录实际下发内容
|
|
4166
4302
|
const finalizeChunks = () => {
|
|
@@ -4177,13 +4313,7 @@ class ProxyServer {
|
|
|
4177
4313
|
extractedUsage = converterUsage || extractedUsage;
|
|
4178
4314
|
}
|
|
4179
4315
|
}
|
|
4180
|
-
|
|
4181
|
-
if (extractUsage && extractedUsage) {
|
|
4182
|
-
usageForLog = extractUsage(extractedUsage);
|
|
4183
|
-
}
|
|
4184
|
-
else if (extractedUsage) {
|
|
4185
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
4186
|
-
}
|
|
4316
|
+
usageForLog = this.tokenUsageFromCollected(extractedUsage);
|
|
4187
4317
|
};
|
|
4188
4318
|
// 监听 res 的错误事件
|
|
4189
4319
|
res.on('error', (err) => {
|
|
@@ -4984,7 +5114,7 @@ class ProxyServer {
|
|
|
4984
5114
|
// 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
|
|
4985
5115
|
const originalModel = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
|
|
4986
5116
|
const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
|
|
4987
|
-
const { converter
|
|
5117
|
+
const { converter } = this.transformSSEByFormat(clientFormat, sourceType);
|
|
4988
5118
|
this.copyResponseHeaders(responseHeaders, res);
|
|
4989
5119
|
res.status(response.status);
|
|
4990
5120
|
const finalizeStreamChunks = () => {
|
|
@@ -4997,12 +5127,7 @@ class ProxyServer {
|
|
|
4997
5127
|
if (converterUsage)
|
|
4998
5128
|
extractedUsage = converterUsage;
|
|
4999
5129
|
}
|
|
5000
|
-
|
|
5001
|
-
usageForLog = extractUsage(extractedUsage);
|
|
5002
|
-
}
|
|
5003
|
-
else if (extractedUsage) {
|
|
5004
|
-
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
5005
|
-
}
|
|
5130
|
+
usageForLog = this.tokenUsageFromCollected(extractedUsage);
|
|
5006
5131
|
};
|
|
5007
5132
|
try {
|
|
5008
5133
|
yield new Promise((resolve, reject) => {
|
|
@@ -276,6 +276,7 @@ function generateMigrationPrompt(content, targetTool) {
|
|
|
276
276
|
const toolLabels = {
|
|
277
277
|
'claude-code': 'Claude Code',
|
|
278
278
|
'codex': 'Codex',
|
|
279
|
+
'opencode': 'OpenCode',
|
|
279
280
|
};
|
|
280
281
|
const sourceLabel = toolLabels[content.sourceTool];
|
|
281
282
|
const targetLabel = toolLabels[targetTool];
|
|
@@ -320,41 +320,128 @@ class SSEEventCollectorTransform extends stream_1.Transform {
|
|
|
320
320
|
this.events = [];
|
|
321
321
|
}
|
|
322
322
|
/**
|
|
323
|
-
* 从events中提取usage
|
|
323
|
+
* 从events中提取usage信息。
|
|
324
|
+
*
|
|
325
|
+
* 关键:必须遍历**全部**事件并合并,而不是命中第一个就返回——
|
|
326
|
+
* Anthropic 流式把用量拆在两个事件里:`message_start.message.usage` 带 input_tokens,
|
|
327
|
+
* `message_delta.usage` 带(累计的)output_tokens。旧实现命中 message_delta 即返回,
|
|
328
|
+
* 导致 input_tokens 永远丢失。同时统一 OpenAI(prompt_tokens/completion_tokens) 与
|
|
329
|
+
* Gemini(usageMetadata) 的字段命名,返回归一化的 {input_tokens, output_tokens, total_tokens, cache_read_input_tokens}。
|
|
324
330
|
*/
|
|
325
331
|
extractUsage() {
|
|
332
|
+
var _a, _b, _c;
|
|
333
|
+
let input_tokens;
|
|
334
|
+
let output_tokens;
|
|
335
|
+
let total_tokens;
|
|
336
|
+
let cache_read_input_tokens;
|
|
326
337
|
for (const event of this.events) {
|
|
327
338
|
if (!event.data)
|
|
328
339
|
continue;
|
|
340
|
+
let data;
|
|
329
341
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
342
|
+
data = JSON.parse(event.data);
|
|
343
|
+
}
|
|
344
|
+
catch (_d) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// Anthropic: message_start 携带 input(在 message.usage 下)
|
|
348
|
+
const msgUsage = (_a = data === null || data === void 0 ? void 0 : data.message) === null || _a === void 0 ? void 0 : _a.usage;
|
|
349
|
+
if (msgUsage) {
|
|
350
|
+
if (typeof msgUsage.input_tokens === 'number')
|
|
351
|
+
input_tokens = msgUsage.input_tokens;
|
|
352
|
+
if (typeof msgUsage.cache_read_input_tokens === 'number')
|
|
353
|
+
cache_read_input_tokens = msgUsage.cache_read_input_tokens;
|
|
354
|
+
}
|
|
355
|
+
// 通用 usage 对象(Anthropic message_delta.usage / OpenAI usage)
|
|
356
|
+
const usage = data === null || data === void 0 ? void 0 : data.usage;
|
|
357
|
+
if (usage) {
|
|
358
|
+
if (typeof usage.input_tokens === 'number')
|
|
359
|
+
input_tokens = usage.input_tokens;
|
|
360
|
+
if (typeof usage.output_tokens === 'number')
|
|
361
|
+
output_tokens = usage.output_tokens; // message_delta 累计值,取最后一次
|
|
362
|
+
if (typeof usage.cache_read_input_tokens === 'number')
|
|
363
|
+
cache_read_input_tokens = usage.cache_read_input_tokens;
|
|
364
|
+
if (typeof usage.prompt_tokens === 'number')
|
|
365
|
+
input_tokens = usage.prompt_tokens;
|
|
366
|
+
if (typeof usage.completion_tokens === 'number')
|
|
367
|
+
output_tokens = usage.completion_tokens;
|
|
368
|
+
if (typeof usage.total_tokens === 'number')
|
|
369
|
+
total_tokens = usage.total_tokens;
|
|
370
|
+
if (typeof usage.cached_tokens === 'number')
|
|
371
|
+
cache_read_input_tokens = usage.cached_tokens;
|
|
372
|
+
}
|
|
373
|
+
// OpenAI Responses 流式:usage 在最终 response.completed / response.done 事件的 data.response.usage 下
|
|
374
|
+
// (Codex 默认走 Responses 流式,此前漏读此处导致 Codex token 恒为 0)
|
|
375
|
+
const respUsage = (_b = data === null || data === void 0 ? void 0 : data.response) === null || _b === void 0 ? void 0 : _b.usage;
|
|
376
|
+
if (respUsage) {
|
|
377
|
+
if (typeof respUsage.input_tokens === 'number')
|
|
378
|
+
input_tokens = respUsage.input_tokens;
|
|
379
|
+
if (typeof respUsage.output_tokens === 'number')
|
|
380
|
+
output_tokens = respUsage.output_tokens;
|
|
381
|
+
if (typeof respUsage.total_tokens === 'number')
|
|
382
|
+
total_tokens = respUsage.total_tokens;
|
|
383
|
+
if (typeof respUsage.prompt_tokens === 'number')
|
|
384
|
+
input_tokens = respUsage.prompt_tokens;
|
|
385
|
+
if (typeof respUsage.completion_tokens === 'number')
|
|
386
|
+
output_tokens = respUsage.completion_tokens;
|
|
387
|
+
const cached = (_c = respUsage.input_tokens_details) === null || _c === void 0 ? void 0 : _c.cached_tokens;
|
|
388
|
+
if (typeof cached === 'number')
|
|
389
|
+
cache_read_input_tokens = cached;
|
|
390
|
+
}
|
|
391
|
+
// OpenAI: choices[].usage(部分上游把 usage 放在最后一个 choice 上)
|
|
392
|
+
if (Array.isArray(data === null || data === void 0 ? void 0 : data.choices) && data.choices.length > 0) {
|
|
393
|
+
const lastChoice = data.choices[data.choices.length - 1];
|
|
394
|
+
const cu = lastChoice === null || lastChoice === void 0 ? void 0 : lastChoice.usage;
|
|
395
|
+
if (cu) {
|
|
396
|
+
if (typeof cu.prompt_tokens === 'number')
|
|
397
|
+
input_tokens = cu.prompt_tokens;
|
|
398
|
+
if (typeof cu.completion_tokens === 'number')
|
|
399
|
+
output_tokens = cu.completion_tokens;
|
|
400
|
+
if (typeof cu.total_tokens === 'number')
|
|
401
|
+
total_tokens = cu.total_tokens;
|
|
351
402
|
}
|
|
352
403
|
}
|
|
353
|
-
|
|
354
|
-
|
|
404
|
+
// Gemini: usageMetadata
|
|
405
|
+
const um = data === null || data === void 0 ? void 0 : data.usageMetadata;
|
|
406
|
+
if (um) {
|
|
407
|
+
if (typeof um.promptTokenCount === 'number')
|
|
408
|
+
input_tokens = um.promptTokenCount;
|
|
409
|
+
if (typeof um.candidatesTokenCount === 'number')
|
|
410
|
+
output_tokens = um.candidatesTokenCount;
|
|
411
|
+
if (typeof um.totalTokenCount === 'number')
|
|
412
|
+
total_tokens = um.totalTokenCount;
|
|
413
|
+
if (typeof um.cachedContentTokenCount === 'number')
|
|
414
|
+
cache_read_input_tokens = um.cachedContentTokenCount;
|
|
355
415
|
}
|
|
416
|
+
// 顶级裸字段兜底
|
|
417
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.input_tokens) === 'number')
|
|
418
|
+
input_tokens = data.input_tokens;
|
|
419
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.output_tokens) === 'number')
|
|
420
|
+
output_tokens = data.output_tokens;
|
|
421
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.prompt_tokens) === 'number')
|
|
422
|
+
input_tokens = data.prompt_tokens;
|
|
423
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.completion_tokens) === 'number')
|
|
424
|
+
output_tokens = data.completion_tokens;
|
|
425
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.total_tokens) === 'number')
|
|
426
|
+
total_tokens = data.total_tokens;
|
|
427
|
+
}
|
|
428
|
+
if (input_tokens === undefined && output_tokens === undefined && total_tokens === undefined) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const result = {};
|
|
432
|
+
if (input_tokens !== undefined)
|
|
433
|
+
result.input_tokens = input_tokens;
|
|
434
|
+
if (output_tokens !== undefined)
|
|
435
|
+
result.output_tokens = output_tokens;
|
|
436
|
+
if (total_tokens !== undefined) {
|
|
437
|
+
result.total_tokens = total_tokens;
|
|
438
|
+
}
|
|
439
|
+
else if (input_tokens !== undefined && output_tokens !== undefined) {
|
|
440
|
+
result.total_tokens = input_tokens + output_tokens;
|
|
356
441
|
}
|
|
357
|
-
|
|
442
|
+
if (cache_read_input_tokens !== undefined)
|
|
443
|
+
result.cache_read_input_tokens = cache_read_input_tokens;
|
|
444
|
+
return result;
|
|
358
445
|
}
|
|
359
446
|
}
|
|
360
447
|
exports.SSEEventCollectorTransform = SSEEventCollectorTransform;
|