aicodeswitch 5.2.11 → 5.2.12
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/dist/server/fs-database.js +3 -3
- package/dist/server/main.js +77 -0
- package/dist/server/performance-tracker.js +377 -0
- package/dist/server/proxy-server.js +82 -18
- package/dist/server/rules-status-service.js +21 -6
- package/dist/server/transformers/stream-timing-transform.js +63 -0
- package/dist/ui/assets/index-BFVjD9Y2.js +799 -0
- package/dist/ui/assets/index-Dm34-4zP.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-DR6cZIa7.js +0 -799
- package/dist/ui/assets/index-MjMlew6J.css +0 -1
|
@@ -2806,9 +2806,9 @@ class FileSystemDatabaseManager {
|
|
|
2806
2806
|
serviceStats.avgResponseTime =
|
|
2807
2807
|
(serviceStats.avgResponseTime * (serviceStats.totalRequests - 1) + responseTime) / serviceStats.totalRequests;
|
|
2808
2808
|
}
|
|
2809
|
-
// 更新 byModel
|
|
2810
|
-
if (log.
|
|
2811
|
-
const modelName = log.
|
|
2809
|
+
// 更新 byModel(按实际转发的模型统计,优先 targetModel 而非客户端提交的 requestModel)
|
|
2810
|
+
if (log.targetModel || log.requestModel) {
|
|
2811
|
+
const modelName = log.targetModel || log.requestModel || 'Unknown';
|
|
2812
2812
|
let modelStats = this.statistics.byModel.find(s => s.modelName === modelName);
|
|
2813
2813
|
if (!modelStats) {
|
|
2814
2814
|
modelStats = { modelName, totalRequests: 0, totalTokens: 0, avgResponseTime: 0 };
|
package/dist/server/main.js
CHANGED
|
@@ -24,6 +24,7 @@ const proxy_server_1 = require("./proxy-server");
|
|
|
24
24
|
const index_1 = require("./access-keys/index");
|
|
25
25
|
const manager_1 = require("./access-keys/manager");
|
|
26
26
|
const policy_manager_1 = require("./access-keys/policy-manager");
|
|
27
|
+
const performance_tracker_1 = require("./performance-tracker");
|
|
27
28
|
const os_1 = __importDefault(require("os"));
|
|
28
29
|
const auth_1 = require("./auth");
|
|
29
30
|
const version_check_1 = require("./version-check");
|
|
@@ -3088,6 +3089,64 @@ ${instruction}
|
|
|
3088
3089
|
}
|
|
3089
3090
|
res.json(alerts);
|
|
3090
3091
|
})));
|
|
3092
|
+
// ============ 服务性能统计(全局,与 AUTH 无关) ============
|
|
3093
|
+
const requirePerfTracker = (res) => {
|
|
3094
|
+
const tracker = proxyServer.getPerformanceTracker();
|
|
3095
|
+
if (!tracker) {
|
|
3096
|
+
res.status(503).json({ error: 'Performance tracker not initialized' });
|
|
3097
|
+
}
|
|
3098
|
+
return tracker;
|
|
3099
|
+
};
|
|
3100
|
+
// 全部 API 服务平铺一览(含所属供应商)
|
|
3101
|
+
app.get('/api/performance/services-overview', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3102
|
+
const tracker = requirePerfTracker(res);
|
|
3103
|
+
if (!tracker)
|
|
3104
|
+
return;
|
|
3105
|
+
res.json(tracker.getServicesOverview());
|
|
3106
|
+
})));
|
|
3107
|
+
// 全部供应商一览
|
|
3108
|
+
app.get('/api/performance/vendors', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3109
|
+
const tracker = requirePerfTracker(res);
|
|
3110
|
+
if (!tracker)
|
|
3111
|
+
return;
|
|
3112
|
+
res.json(tracker.getVendorsOverview());
|
|
3113
|
+
})));
|
|
3114
|
+
// 某供应商详情(rollup + 其下服务)
|
|
3115
|
+
app.get('/api/performance/vendors/:vendorId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3116
|
+
const tracker = requirePerfTracker(res);
|
|
3117
|
+
if (!tracker)
|
|
3118
|
+
return;
|
|
3119
|
+
const detail = tracker.getVendorDetail(req.params.vendorId);
|
|
3120
|
+
if (!detail) {
|
|
3121
|
+
res.status(404).json({ error: 'Vendor not found' });
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
res.json(detail);
|
|
3125
|
+
})));
|
|
3126
|
+
// 某服务详情(rollup + 其下模型)
|
|
3127
|
+
app.get('/api/performance/services/:serviceId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3128
|
+
const tracker = requirePerfTracker(res);
|
|
3129
|
+
if (!tracker)
|
|
3130
|
+
return;
|
|
3131
|
+
const detail = tracker.getServiceDetail(req.params.serviceId);
|
|
3132
|
+
if (!detail) {
|
|
3133
|
+
res.status(404).json({ error: 'Service not found' });
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
res.json(detail);
|
|
3137
|
+
})));
|
|
3138
|
+
// 单模型详情(派生 + 小时走势 + 极值)
|
|
3139
|
+
app.get('/api/performance/services/:serviceId/models/:model', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3140
|
+
const tracker = requirePerfTracker(res);
|
|
3141
|
+
if (!tracker)
|
|
3142
|
+
return;
|
|
3143
|
+
const detail = tracker.getModelDetail(req.params.serviceId, decodeURIComponent(req.params.model));
|
|
3144
|
+
if (!detail) {
|
|
3145
|
+
res.status(404).json({ error: 'Model not found' });
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
res.json(detail);
|
|
3149
|
+
})));
|
|
3091
3150
|
// 写入MCP配置到Claude Code或Codex的全局配置文件
|
|
3092
3151
|
const writeMCPConfig = (targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3093
3152
|
try {
|
|
@@ -3335,6 +3394,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3335
3394
|
catch (error) {
|
|
3336
3395
|
console.error('[Server] AccessKey module initialization failed:', error);
|
|
3337
3396
|
}
|
|
3397
|
+
// Initialize Service Performance Tracker (全局统计,与 AUTH 无关)
|
|
3398
|
+
const performanceTracker = new performance_tracker_1.ServicePerformanceTracker(dataDir);
|
|
3399
|
+
try {
|
|
3400
|
+
yield performanceTracker.initialize();
|
|
3401
|
+
performanceTracker.startAutoFlush();
|
|
3402
|
+
proxyServer.setPerformanceTracker(performanceTracker);
|
|
3403
|
+
}
|
|
3404
|
+
catch (error) {
|
|
3405
|
+
console.error('[Server] Performance tracker initialization failed:', error);
|
|
3406
|
+
}
|
|
3338
3407
|
// 恢复已写入本地的 AccessKey(在代理配置写入之后、AccessKey 模块初始化之后)
|
|
3339
3408
|
try {
|
|
3340
3409
|
applyWriteLocalRecords(proxyServer);
|
|
@@ -3439,6 +3508,14 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3439
3508
|
catch (error) {
|
|
3440
3509
|
console.error('[Shutdown ...] AccessKey module shutdown failed:', error);
|
|
3441
3510
|
}
|
|
3511
|
+
// Flush 服务性能统计(全局桶)后停止定时刷盘
|
|
3512
|
+
try {
|
|
3513
|
+
performanceTracker.stopAutoFlush();
|
|
3514
|
+
yield performanceTracker.flush();
|
|
3515
|
+
}
|
|
3516
|
+
catch (error) {
|
|
3517
|
+
console.error('[Shutdown ...] Performance tracker flush failed:', error);
|
|
3518
|
+
}
|
|
3442
3519
|
dbManager.close();
|
|
3443
3520
|
// 清理规则状态广播器(关闭 SSE 连接)
|
|
3444
3521
|
rules_status_service_1.rulesStatusBroadcaster.destroy();
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ServicePerformanceTracker = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* ServicePerformanceTracker - 服务性能统计全局聚合模块
|
|
18
|
+
*
|
|
19
|
+
* 设计要点(详见 docs/PRD/service-performance-tpm.md):
|
|
20
|
+
* - 全局统计,与 AUTH 模式无关;普通路由 + AccessKey 路由流量统一采集。
|
|
21
|
+
* - 两个数据点:TTFT(首 Token 返回时间)、TPM(每分钟吐 token 数)。
|
|
22
|
+
* - 三级聚合:供应商 → 服务 → 模型;上卷基于 sum+count 加权,avg 由 sum/count 派生。
|
|
23
|
+
* - 走势按小时桶(键 "YYYY-MM-DD HH",保留 72 桶)。
|
|
24
|
+
* - 内存增量 + debounce(5s) flush + 原子写(tmp+rename)。
|
|
25
|
+
*
|
|
26
|
+
* recordPerformance 为纯内存同步操作,可在请求完成路径无开销调用。
|
|
27
|
+
*/
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
30
|
+
const HOURLY_BUCKET_LIMIT = 72; // 保留最近 72 个小时桶(约 3 天)
|
|
31
|
+
class ServicePerformanceTracker {
|
|
32
|
+
constructor(dataPath) {
|
|
33
|
+
Object.defineProperty(this, "dataPath", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: void 0
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(this, "file", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: { vendors: {} }
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(this, "dirty", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: false
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(this, "flushTimer", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: null
|
|
56
|
+
});
|
|
57
|
+
Object.defineProperty(this, "FLUSH_INTERVAL", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
value: 5000
|
|
62
|
+
}); // 5s
|
|
63
|
+
this.dataPath = dataPath;
|
|
64
|
+
}
|
|
65
|
+
/** 初始化:加载已有数据文件 */
|
|
66
|
+
initialize() {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
try {
|
|
69
|
+
const raw = yield promises_1.default.readFile(this.filePath, 'utf-8');
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
if (parsed && parsed.vendors)
|
|
72
|
+
this.file = parsed;
|
|
73
|
+
}
|
|
74
|
+
catch (_a) {
|
|
75
|
+
// 首次启动或文件损坏,使用空桶
|
|
76
|
+
this.file = { vendors: {} };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 记录一次请求的性能数据点(三级同步聚合)。
|
|
82
|
+
* 纯内存操作,不阻塞调用方。
|
|
83
|
+
*/
|
|
84
|
+
recordPerformance(vendorId, vendorName, serviceId, serviceName, model, metrics, timestamp = Date.now()) {
|
|
85
|
+
if (!vendorId || !serviceId || !model)
|
|
86
|
+
return;
|
|
87
|
+
const hour = this.formatHourKey(timestamp);
|
|
88
|
+
// 模型级(最细,维护极值)
|
|
89
|
+
const modelAgg = this.ensureModel(vendorId, vendorName, serviceId, serviceName, model);
|
|
90
|
+
this.accumulate(modelAgg, metrics, hour, /* withExtremes */ true);
|
|
91
|
+
// 服务级上卷
|
|
92
|
+
const serviceAgg = this.ensureService(vendorId, vendorName, serviceId, serviceName);
|
|
93
|
+
this.accumulate(serviceAgg, metrics, hour, /* withExtremes */ false);
|
|
94
|
+
// 供应商级上卷
|
|
95
|
+
const vendorAgg = this.ensureVendor(vendorId, vendorName);
|
|
96
|
+
this.accumulate(vendorAgg, metrics, hour, /* withExtremes */ false);
|
|
97
|
+
this.dirty = true;
|
|
98
|
+
}
|
|
99
|
+
// ---------------- 读取(派生视图) ----------------
|
|
100
|
+
/** 全部供应商一览(vendorRollup 派生) */
|
|
101
|
+
getVendorsOverview() {
|
|
102
|
+
return Object.entries(this.file.vendors).map(([vendorId, v]) => ({
|
|
103
|
+
vendorId,
|
|
104
|
+
vendorName: v.vendorName,
|
|
105
|
+
derived: this.derive(v.vendorRollup),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/** 全部 API 服务平铺一览(含所属供应商),用于「API 服务」维度对比 */
|
|
109
|
+
getServicesOverview() {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const [vendorId, v] of Object.entries(this.file.vendors)) {
|
|
112
|
+
for (const [serviceId, s] of Object.entries(v.services)) {
|
|
113
|
+
out.push({
|
|
114
|
+
serviceId,
|
|
115
|
+
serviceName: s.serviceName,
|
|
116
|
+
vendorId,
|
|
117
|
+
vendorName: v.vendorName,
|
|
118
|
+
derived: this.derive(s.serviceRollup),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** 某供应商:自身 rollup + 其下所有服务 rollup */
|
|
125
|
+
getVendorDetail(vendorId) {
|
|
126
|
+
const v = this.file.vendors[vendorId];
|
|
127
|
+
if (!v)
|
|
128
|
+
return null;
|
|
129
|
+
return {
|
|
130
|
+
vendorName: v.vendorName,
|
|
131
|
+
derived: this.derive(v.vendorRollup),
|
|
132
|
+
hourly: this.trendFrom(vendorId, undefined, undefined),
|
|
133
|
+
services: Object.entries(v.services).map(([serviceId, s]) => ({
|
|
134
|
+
serviceId,
|
|
135
|
+
serviceName: s.serviceName,
|
|
136
|
+
derived: this.derive(s.serviceRollup),
|
|
137
|
+
})),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** 某服务:自身 rollup + 其下所有模型 */
|
|
141
|
+
getServiceDetail(serviceId) {
|
|
142
|
+
const found = this.locateService(serviceId);
|
|
143
|
+
if (!found)
|
|
144
|
+
return null;
|
|
145
|
+
const { vendorId, vendorName, serviceEntry } = found;
|
|
146
|
+
return {
|
|
147
|
+
vendorId,
|
|
148
|
+
vendorName,
|
|
149
|
+
serviceName: serviceEntry.serviceName,
|
|
150
|
+
derived: this.derive(serviceEntry.serviceRollup),
|
|
151
|
+
hourly: this.trendFrom(vendorId, serviceId, undefined),
|
|
152
|
+
models: Object.entries(serviceEntry.models).map(([model, agg]) => ({
|
|
153
|
+
model,
|
|
154
|
+
derived: this.derive(agg),
|
|
155
|
+
})),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** 单模型:派生 + 小时走势 + 极值 */
|
|
159
|
+
getModelDetail(serviceId, model) {
|
|
160
|
+
const found = this.locateService(serviceId);
|
|
161
|
+
if (!found)
|
|
162
|
+
return null;
|
|
163
|
+
const agg = found.serviceEntry.models[model];
|
|
164
|
+
if (!agg)
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
derived: this.derive(agg),
|
|
168
|
+
hourly: this.trendFrom(found.vendorId, serviceId, model),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ---------------- 持久化 ----------------
|
|
172
|
+
flush() {
|
|
173
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
if (!this.dirty)
|
|
175
|
+
return;
|
|
176
|
+
this.dirty = false;
|
|
177
|
+
yield this.save();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
startAutoFlush() {
|
|
181
|
+
if (this.flushTimer)
|
|
182
|
+
return;
|
|
183
|
+
this.flushTimer = setInterval(() => {
|
|
184
|
+
this.flush().catch(err => console.error('[PerformanceTracker] Auto flush error:', err));
|
|
185
|
+
}, this.FLUSH_INTERVAL);
|
|
186
|
+
}
|
|
187
|
+
stopAutoFlush() {
|
|
188
|
+
if (this.flushTimer) {
|
|
189
|
+
clearInterval(this.flushTimer);
|
|
190
|
+
this.flushTimer = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
get filePath() {
|
|
194
|
+
return path_1.default.join(this.dataPath, 'service-performance.json');
|
|
195
|
+
}
|
|
196
|
+
save() {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
const tmp = this.filePath + '.tmp';
|
|
199
|
+
yield promises_1.default.mkdir(path_1.default.dirname(this.filePath), { recursive: true });
|
|
200
|
+
yield promises_1.default.writeFile(tmp, JSON.stringify(this.file, null, 2), 'utf-8');
|
|
201
|
+
yield promises_1.default.rename(tmp, this.filePath);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// ---------------- 内部:结构创建 ----------------
|
|
205
|
+
ensureVendor(vendorId, vendorName) {
|
|
206
|
+
let v = this.file.vendors[vendorId];
|
|
207
|
+
if (!v) {
|
|
208
|
+
v = {
|
|
209
|
+
vendorName,
|
|
210
|
+
vendorRollup: this.emptyAggregate(),
|
|
211
|
+
services: {},
|
|
212
|
+
};
|
|
213
|
+
this.file.vendors[vendorId] = v;
|
|
214
|
+
}
|
|
215
|
+
if (vendorName && !v.vendorName)
|
|
216
|
+
v.vendorName = vendorName;
|
|
217
|
+
return v.vendorRollup;
|
|
218
|
+
}
|
|
219
|
+
ensureService(vendorId, vendorName, serviceId, serviceName) {
|
|
220
|
+
var _a;
|
|
221
|
+
const v = (_a = this.file.vendors[vendorId]) !== null && _a !== void 0 ? _a : (this.file.vendors[vendorId] = {
|
|
222
|
+
vendorName, vendorRollup: this.emptyAggregate(), services: {},
|
|
223
|
+
});
|
|
224
|
+
let s = v.services[serviceId];
|
|
225
|
+
if (!s) {
|
|
226
|
+
s = {
|
|
227
|
+
serviceName,
|
|
228
|
+
serviceRollup: this.emptyAggregate(),
|
|
229
|
+
models: {},
|
|
230
|
+
updatedAt: Date.now(),
|
|
231
|
+
};
|
|
232
|
+
v.services[serviceId] = s;
|
|
233
|
+
}
|
|
234
|
+
if (serviceName && !s.serviceName)
|
|
235
|
+
s.serviceName = serviceName;
|
|
236
|
+
s.updatedAt = Date.now();
|
|
237
|
+
return s.serviceRollup;
|
|
238
|
+
}
|
|
239
|
+
ensureModel(vendorId, vendorName, serviceId, serviceName, model) {
|
|
240
|
+
// 确保供应商 + 服务节点存在(service rollup 由 recordPerformance 单独累加)
|
|
241
|
+
this.ensureService(vendorId, vendorName, serviceId, serviceName);
|
|
242
|
+
const s = this.file.vendors[vendorId].services[serviceId];
|
|
243
|
+
let m = s.models[model];
|
|
244
|
+
if (!m) {
|
|
245
|
+
m = this.emptyAggregate();
|
|
246
|
+
s.models[model] = m;
|
|
247
|
+
}
|
|
248
|
+
return m;
|
|
249
|
+
}
|
|
250
|
+
// ---------------- 内部:累加 ----------------
|
|
251
|
+
emptyAggregate() {
|
|
252
|
+
return {
|
|
253
|
+
precise: this.emptyBucket(),
|
|
254
|
+
estimated: this.emptyBucket(),
|
|
255
|
+
errorCount: 0,
|
|
256
|
+
hourly: {},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
emptyBucket() {
|
|
260
|
+
return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0 };
|
|
261
|
+
}
|
|
262
|
+
accumulate(agg, m, hour, withExtremes) {
|
|
263
|
+
var _a;
|
|
264
|
+
if (m.isError) {
|
|
265
|
+
agg.errorCount += 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const bucket = m.timingAccuracy === 'precise' ? agg.precise : agg.estimated;
|
|
269
|
+
const hasTtft = m.timingAccuracy === 'precise' && typeof m.ttftMs === 'number';
|
|
270
|
+
const hasTps = typeof m.tokensPerSecond === 'number';
|
|
271
|
+
bucket.count += 1;
|
|
272
|
+
if (hasTtft)
|
|
273
|
+
bucket.sumTtftMs += m.ttftMs;
|
|
274
|
+
if (hasTps)
|
|
275
|
+
bucket.sumTps += m.tokensPerSecond;
|
|
276
|
+
if (m.outputTokens)
|
|
277
|
+
bucket.totalOutputTokens += m.outputTokens;
|
|
278
|
+
// 小时桶(仅精确样本计入走势,避免估算样本污染)
|
|
279
|
+
if (m.timingAccuracy === 'precise') {
|
|
280
|
+
const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
|
|
281
|
+
hb.count += 1;
|
|
282
|
+
if (hasTtft)
|
|
283
|
+
hb.sumTtftMs += m.ttftMs;
|
|
284
|
+
if (hasTps)
|
|
285
|
+
hb.sumTps += m.tokensPerSecond;
|
|
286
|
+
if (m.outputTokens)
|
|
287
|
+
hb.totalOutputTokens += m.outputTokens;
|
|
288
|
+
this.trimHourly(agg.hourly);
|
|
289
|
+
}
|
|
290
|
+
// 极值(仅模型级、仅精确样本)
|
|
291
|
+
if (withExtremes && m.timingAccuracy === 'precise') {
|
|
292
|
+
if (hasTtft) {
|
|
293
|
+
if (agg.minTtftMs === undefined || m.ttftMs < agg.minTtftMs)
|
|
294
|
+
agg.minTtftMs = m.ttftMs;
|
|
295
|
+
if (agg.maxTtftMs === undefined || m.ttftMs > agg.maxTtftMs)
|
|
296
|
+
agg.maxTtftMs = m.ttftMs;
|
|
297
|
+
}
|
|
298
|
+
if (hasTps) {
|
|
299
|
+
if (agg.minTps === undefined || m.tokensPerSecond < agg.minTps)
|
|
300
|
+
agg.minTps = m.tokensPerSecond;
|
|
301
|
+
if (agg.maxTps === undefined || m.tokensPerSecond > agg.maxTps)
|
|
302
|
+
agg.maxTps = m.tokensPerSecond;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
trimHourly(hourly) {
|
|
307
|
+
const keys = Object.keys(hourly);
|
|
308
|
+
if (keys.length <= HOURLY_BUCKET_LIMIT)
|
|
309
|
+
return;
|
|
310
|
+
keys.sort(); // "YYYY-MM-DD HH" 字典序即时间序
|
|
311
|
+
const drop = keys.length - HOURLY_BUCKET_LIMIT;
|
|
312
|
+
for (let i = 0; i < drop; i++)
|
|
313
|
+
delete hourly[keys[i]];
|
|
314
|
+
}
|
|
315
|
+
// ---------------- 内部:派生 ----------------
|
|
316
|
+
derive(agg) {
|
|
317
|
+
const p = agg.precise;
|
|
318
|
+
const count = p.count;
|
|
319
|
+
const avgTtftMs = count > 0 ? p.sumTtftMs / count : 0;
|
|
320
|
+
const avgTps = count > 0 ? p.sumTps / count : 0;
|
|
321
|
+
return {
|
|
322
|
+
count,
|
|
323
|
+
avgTtftMs,
|
|
324
|
+
avgTpm: avgTps * 60,
|
|
325
|
+
minTtftMs: agg.minTtftMs,
|
|
326
|
+
maxTtftMs: agg.maxTtftMs,
|
|
327
|
+
minTps: agg.minTps,
|
|
328
|
+
maxTps: agg.maxTps,
|
|
329
|
+
errorCount: agg.errorCount,
|
|
330
|
+
totalOutputTokens: p.totalOutputTokens,
|
|
331
|
+
successRate: count + agg.errorCount > 0 ? count / (count + agg.errorCount) : 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
trendFrom(vendorId, serviceId, model) {
|
|
335
|
+
var _a, _b;
|
|
336
|
+
const v = this.file.vendors[vendorId];
|
|
337
|
+
if (!v)
|
|
338
|
+
return [];
|
|
339
|
+
let agg;
|
|
340
|
+
if (model && serviceId) {
|
|
341
|
+
agg = (_a = v.services[serviceId]) === null || _a === void 0 ? void 0 : _a.models[model];
|
|
342
|
+
}
|
|
343
|
+
else if (serviceId) {
|
|
344
|
+
agg = (_b = v.services[serviceId]) === null || _b === void 0 ? void 0 : _b.serviceRollup;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
agg = v.vendorRollup;
|
|
348
|
+
}
|
|
349
|
+
if (!agg)
|
|
350
|
+
return [];
|
|
351
|
+
return Object.entries(agg.hourly)
|
|
352
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
353
|
+
.map(([hour, b]) => ({
|
|
354
|
+
hour,
|
|
355
|
+
count: b.count,
|
|
356
|
+
avgTtftMs: b.count > 0 ? b.sumTtftMs / b.count : 0,
|
|
357
|
+
avgTpm: b.count > 0 ? (b.sumTps / b.count) * 60 : 0,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
locateService(serviceId) {
|
|
361
|
+
for (const [vendorId, v] of Object.entries(this.file.vendors)) {
|
|
362
|
+
const s = v.services[serviceId];
|
|
363
|
+
if (s)
|
|
364
|
+
return { vendorId, vendorName: v.vendorName, serviceEntry: s };
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
formatHourKey(ts) {
|
|
369
|
+
const d = new Date(ts);
|
|
370
|
+
const yyyy = d.getFullYear();
|
|
371
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
372
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
373
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
374
|
+
return `${yyyy}-${mm}-${dd} ${hh}`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
exports.ServicePerformanceTracker = ServicePerformanceTracker;
|