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.
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.readOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
6
+ exports.readOriginalConfig = exports.readOpencodeOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
@@ -232,6 +232,73 @@ const readCodexOriginalConfig = () => {
232
232
  }
233
233
  };
234
234
  exports.readCodexOriginalConfig = readCodexOriginalConfig;
235
+ /**
236
+ * 读取 OpenCode 原始配置
237
+ * fallback 场景下仅从备份文件(~/.config/opencode/opencode.json.aicodeswitch_backup)读取
238
+ *
239
+ * OpenCode 的模型形如 "<providerId>/<modelId>",真实上游由用户原始 opencode.json 中的
240
+ * provider 段(options.baseURL + options.apiKey)决定。此处解析备份中的真实 provider。
241
+ */
242
+ const readOpencodeOriginalConfig = () => {
243
+ var _a, _b;
244
+ try {
245
+ const homeDir = os_1.default.homedir();
246
+ const configBakPath = path_1.default.join(homeDir, '.config', 'opencode', 'opencode.json.aicodeswitch_backup');
247
+ if (!fs_1.default.existsSync(configBakPath)) {
248
+ console.log('No OpenCode backup config file found');
249
+ return null;
250
+ }
251
+ const content = fs_1.default.readFileSync(configBakPath, 'utf-8');
252
+ const config = JSON.parse(content);
253
+ // 解析 model:"providerId/modelId"
254
+ const modelField = typeof config.model === 'string' ? config.model : '';
255
+ const [providerId, modelId] = modelField.split('/');
256
+ const providers = (config.provider && typeof config.provider === 'object') ? config.provider : {};
257
+ // 选定真实 provider:优先 model 中声明的 providerId,否则取第一个非 aicodeswitch 的 provider
258
+ let providerConfig;
259
+ let resolvedProviderId = '';
260
+ if (providerId && providers[providerId]) {
261
+ providerConfig = providers[providerId];
262
+ resolvedProviderId = providerId;
263
+ }
264
+ else {
265
+ for (const [pid, pc] of Object.entries(providers)) {
266
+ if (pid !== 'aicodeswitch' && pc && typeof pc === 'object') {
267
+ providerConfig = pc;
268
+ resolvedProviderId = pid;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ const baseUrlRaw = ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _a === void 0 ? void 0 : _a.baseURL) || '';
274
+ const baseUrl = normalizeApiUrl(baseUrlRaw);
275
+ // apiKey 可能内联在 options.apiKey,也可能经 {env:XXX}/{file:xxx} 引用(fallback 无法解析,置空)
276
+ let apiKey = '';
277
+ if (typeof ((_b = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _b === void 0 ? void 0 : _b.apiKey) === 'string') {
278
+ const raw = providerConfig.options.apiKey.trim();
279
+ if (raw && !raw.startsWith('{env:') && !raw.startsWith('{file:')) {
280
+ apiKey = raw;
281
+ }
282
+ }
283
+ if (!baseUrl) {
284
+ console.log('No baseURL found in OpenCode backup config provider:', resolvedProviderId);
285
+ return null;
286
+ }
287
+ const sourceType = inferSourceTypeFromBaseUrlAndWireApi(baseUrl, undefined);
288
+ return {
289
+ apiUrl: baseUrl,
290
+ apiKey,
291
+ authType: inferAuthTypeFromSource(sourceType),
292
+ sourceType,
293
+ model: modelId || modelField || undefined,
294
+ };
295
+ }
296
+ catch (error) {
297
+ console.error('Failed to read OpenCode original config:', error);
298
+ return null;
299
+ }
300
+ };
301
+ exports.readOpencodeOriginalConfig = readOpencodeOriginalConfig;
235
302
  /**
236
303
  * 根据目标类型读取原始配置
237
304
  */
@@ -242,6 +309,9 @@ const readOriginalConfig = (targetType) => {
242
309
  else if (targetType === 'codex') {
243
310
  return (0, exports.readCodexOriginalConfig)();
244
311
  }
312
+ else if (targetType === 'opencode') {
313
+ return (0, exports.readOpencodeOriginalConfig)();
314
+ }
245
315
  return null;
246
316
  };
247
317
  exports.readOriginalConfig = readOriginalConfig;
@@ -68,8 +68,12 @@ class ServicePerformanceTracker {
68
68
  try {
69
69
  const raw = yield promises_1.default.readFile(this.filePath, 'utf-8');
70
70
  const parsed = JSON.parse(raw);
71
- if (parsed && parsed.vendors)
71
+ if (parsed && parsed.vendors) {
72
72
  this.file = parsed;
73
+ // 旧数据文件可能缺新增的 token 字段(sumInputTokens/sumTotalTokens),
74
+ // 直接累加会得到 undefined+N=NaN,污染派生值。这里补齐为 0。
75
+ this.migrateBuckets(this.file);
76
+ }
73
77
  }
74
78
  catch (_a) {
75
79
  // 首次启动或文件损坏,使用空桶
@@ -77,6 +81,42 @@ class ServicePerformanceTracker {
77
81
  }
78
82
  });
79
83
  }
84
+ /** 加载后补齐旧数据缺失的桶字段,避免 undefined 累加成 NaN */
85
+ migrateBuckets(file) {
86
+ for (const v of Object.values(file.vendors)) {
87
+ if (v.vendorRollup)
88
+ this.normalizeAggregate(v.vendorRollup);
89
+ for (const s of Object.values(v.services || {})) {
90
+ if (s.serviceRollup)
91
+ this.normalizeAggregate(s.serviceRollup);
92
+ for (const agg of Object.values(s.models || {})) {
93
+ this.normalizeAggregate(agg);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ normalizeAggregate(agg) {
99
+ this.normalizeBucket(agg.precise);
100
+ this.normalizeBucket(agg.estimated);
101
+ for (const b of Object.values(agg.hourly || {}))
102
+ this.normalizeBucket(b);
103
+ }
104
+ normalizeBucket(b) {
105
+ if (!b)
106
+ return;
107
+ if (typeof b.count !== 'number')
108
+ b.count = 0;
109
+ if (typeof b.sumTtftMs !== 'number')
110
+ b.sumTtftMs = 0;
111
+ if (typeof b.sumTps !== 'number')
112
+ b.sumTps = 0;
113
+ if (typeof b.totalOutputTokens !== 'number')
114
+ b.totalOutputTokens = 0;
115
+ if (typeof b.sumInputTokens !== 'number')
116
+ b.sumInputTokens = 0;
117
+ if (typeof b.sumTotalTokens !== 'number')
118
+ b.sumTotalTokens = 0;
119
+ }
80
120
  /**
81
121
  * 记录一次请求的性能数据点(三级同步聚合)。
82
122
  * 纯内存操作,不阻塞调用方。
@@ -257,7 +297,7 @@ class ServicePerformanceTracker {
257
297
  };
258
298
  }
259
299
  emptyBucket() {
260
- return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0 };
300
+ return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0, sumInputTokens: 0, sumTotalTokens: 0 };
261
301
  }
262
302
  accumulate(agg, m, hour, withExtremes) {
263
303
  var _a;
@@ -268,6 +308,7 @@ class ServicePerformanceTracker {
268
308
  const bucket = m.timingAccuracy === 'precise' ? agg.precise : agg.estimated;
269
309
  const hasTtft = m.timingAccuracy === 'precise' && typeof m.ttftMs === 'number';
270
310
  const hasTps = typeof m.tokensPerSecond === 'number';
311
+ // token 量与计时精度无关,precise/estimated 桶都累加
271
312
  bucket.count += 1;
272
313
  if (hasTtft)
273
314
  bucket.sumTtftMs += m.ttftMs;
@@ -275,18 +316,37 @@ class ServicePerformanceTracker {
275
316
  bucket.sumTps += m.tokensPerSecond;
276
317
  if (m.outputTokens)
277
318
  bucket.totalOutputTokens += m.outputTokens;
278
- // 小时桶(仅精确样本计入走势,避免估算样本污染)
319
+ if (m.inputTokens)
320
+ bucket.sumInputTokens += m.inputTokens;
321
+ if (m.totalTokens)
322
+ bucket.sumTotalTokens += m.totalTokens;
323
+ // 小时走势桶:
324
+ // - count/sumTtftMs/sumTps 仅精确样本计入(避免估算样本污染 TTFT/TPM 均值)
325
+ // - token 三项与精度无关,所有样本都计入走势(含非流式)
326
+ const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
327
+ let touchedHourly = false;
279
328
  if (m.timingAccuracy === 'precise') {
280
- const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
281
329
  hb.count += 1;
282
330
  if (hasTtft)
283
331
  hb.sumTtftMs += m.ttftMs;
284
332
  if (hasTps)
285
333
  hb.sumTps += m.tokensPerSecond;
286
- if (m.outputTokens)
287
- hb.totalOutputTokens += m.outputTokens;
288
- this.trimHourly(agg.hourly);
334
+ touchedHourly = true;
335
+ }
336
+ if (m.outputTokens) {
337
+ hb.totalOutputTokens += m.outputTokens;
338
+ touchedHourly = true;
339
+ }
340
+ if (m.inputTokens) {
341
+ hb.sumInputTokens += m.inputTokens;
342
+ touchedHourly = true;
289
343
  }
344
+ if (m.totalTokens) {
345
+ hb.sumTotalTokens += m.totalTokens;
346
+ touchedHourly = true;
347
+ }
348
+ if (touchedHourly)
349
+ this.trimHourly(agg.hourly);
290
350
  // 极值(仅模型级、仅精确样本)
291
351
  if (withExtremes && m.timingAccuracy === 'precise') {
292
352
  if (hasTtft) {
@@ -315,9 +375,14 @@ class ServicePerformanceTracker {
315
375
  // ---------------- 内部:派生 ----------------
316
376
  derive(agg) {
317
377
  const p = agg.precise;
378
+ const e = agg.estimated;
318
379
  const count = p.count;
319
380
  const avgTtftMs = count > 0 ? p.sumTtftMs / count : 0;
320
381
  const avgTps = count > 0 ? p.sumTps / count : 0;
382
+ // token 量跨 precise+estimated 求和(含非流式样本)
383
+ const totalOutputTokens = p.totalOutputTokens + e.totalOutputTokens;
384
+ const totalInputTokens = p.sumInputTokens + e.sumInputTokens;
385
+ const totalTokens = p.sumTotalTokens + e.sumTotalTokens;
321
386
  return {
322
387
  count,
323
388
  avgTtftMs,
@@ -327,7 +392,9 @@ class ServicePerformanceTracker {
327
392
  minTps: agg.minTps,
328
393
  maxTps: agg.maxTps,
329
394
  errorCount: agg.errorCount,
330
- totalOutputTokens: p.totalOutputTokens,
395
+ totalOutputTokens,
396
+ totalInputTokens,
397
+ totalTokens,
331
398
  successRate: count + agg.errorCount > 0 ? count / (count + agg.errorCount) : 0,
332
399
  };
333
400
  }
@@ -355,6 +422,9 @@ class ServicePerformanceTracker {
355
422
  count: b.count,
356
423
  avgTtftMs: b.count > 0 ? b.sumTtftMs / b.count : 0,
357
424
  avgTpm: b.count > 0 ? (b.sumTps / b.count) * 60 : 0,
425
+ inputTokens: b.sumInputTokens,
426
+ outputTokens: b.totalOutputTokens,
427
+ totalTokens: b.sumTotalTokens,
358
428
  }));
359
429
  }
360
430
  locateService(serviceId) {