aicodeswitch 1.8.0 → 1.9.0

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.
@@ -100,6 +100,46 @@ class DatabaseManager {
100
100
  console.log('[DB] Migration completed: model_limits column added');
101
101
  }
102
102
  }
103
+ // 检查rules表是否有token相关字段
104
+ const rulesColumns = this.db.pragma('table_info(rules)');
105
+ const hasTokenLimit = rulesColumns.some((col) => col.name === 'token_limit');
106
+ const hasTotalTokensUsed = rulesColumns.some((col) => col.name === 'total_tokens_used');
107
+ const hasResetInterval = rulesColumns.some((col) => col.name === 'reset_interval');
108
+ const hasLastResetAt = rulesColumns.some((col) => col.name === 'last_reset_at');
109
+ if (!hasTokenLimit) {
110
+ console.log('[DB] Running migration: Adding token_limit column to rules table');
111
+ this.db.exec('ALTER TABLE rules ADD COLUMN token_limit INTEGER;');
112
+ console.log('[DB] Migration completed: token_limit column added');
113
+ }
114
+ if (!hasTotalTokensUsed) {
115
+ console.log('[DB] Running migration: Adding total_tokens_used column to rules table');
116
+ this.db.exec('ALTER TABLE rules ADD COLUMN total_tokens_used INTEGER DEFAULT 0;');
117
+ console.log('[DB] Migration completed: total_tokens_used column added');
118
+ }
119
+ if (!hasResetInterval) {
120
+ console.log('[DB] Running migration: Adding reset_interval column to rules table');
121
+ this.db.exec('ALTER TABLE rules ADD COLUMN reset_interval INTEGER;');
122
+ console.log('[DB] Migration completed: reset_interval column added');
123
+ }
124
+ if (!hasLastResetAt) {
125
+ console.log('[DB] Running migration: Adding last_reset_at column to rules table');
126
+ this.db.exec('ALTER TABLE rules ADD COLUMN last_reset_at INTEGER;');
127
+ console.log('[DB] Migration completed: last_reset_at column added');
128
+ }
129
+ // 检查rules表是否有timeout字段
130
+ const hasRuleTimeout = rulesColumns.some((col) => col.name === 'timeout');
131
+ if (!hasRuleTimeout) {
132
+ console.log('[DB] Running migration: Adding timeout column to rules table');
133
+ this.db.exec('ALTER TABLE rules ADD COLUMN timeout INTEGER;');
134
+ console.log('[DB] Migration completed: timeout column added to rules');
135
+ }
136
+ // 检查api_services表是否有timeout字段,如果有则移除
137
+ const hasServiceTimeout = columns.some((col) => col.name === 'timeout');
138
+ if (hasServiceTimeout) {
139
+ console.log('[DB] Running migration: Removing timeout column from api_services table');
140
+ yield this.migrateRemoveServiceTimeout();
141
+ console.log('[DB] Migration completed: timeout column removed from api_services');
142
+ }
103
143
  });
104
144
  }
105
145
  migrateMaxOutputTokensToModelLimits() {
@@ -138,6 +178,35 @@ class DatabaseManager {
138
178
  console.log('[DB] Migration completed: Replaced max_output_tokens with model_limits');
139
179
  });
140
180
  }
181
+ migrateRemoveServiceTimeout() {
182
+ return __awaiter(this, void 0, void 0, function* () {
183
+ // SQLite 不支持直接删除列,需要重建表
184
+ this.db.pragma('foreign_keys = OFF');
185
+ this.db.exec(`
186
+ CREATE TABLE api_services_new (
187
+ id TEXT PRIMARY KEY,
188
+ vendor_id TEXT NOT NULL,
189
+ name TEXT NOT NULL,
190
+ api_url TEXT NOT NULL,
191
+ api_key TEXT NOT NULL,
192
+ source_type TEXT,
193
+ supported_models TEXT,
194
+ model_limits TEXT,
195
+ created_at INTEGER NOT NULL,
196
+ updated_at INTEGER NOT NULL,
197
+ FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
198
+ );
199
+
200
+ INSERT INTO api_services_new (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at)
201
+ SELECT id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at
202
+ FROM api_services;
203
+
204
+ DROP TABLE api_services;
205
+ ALTER TABLE api_services_new RENAME TO api_services;
206
+ `);
207
+ this.db.pragma('foreign_keys = ON');
208
+ });
209
+ }
141
210
  createTables() {
142
211
  this.db.exec(`
143
212
  CREATE TABLE IF NOT EXISTS vendors (
@@ -154,7 +223,6 @@ class DatabaseManager {
154
223
  name TEXT NOT NULL,
155
224
  api_url TEXT NOT NULL,
156
225
  api_key TEXT NOT NULL,
157
- timeout INTEGER,
158
226
  source_type TEXT,
159
227
  supported_models TEXT,
160
228
  created_at INTEGER NOT NULL,
@@ -180,6 +248,11 @@ class DatabaseManager {
180
248
  target_model TEXT,
181
249
  replaced_model TEXT,
182
250
  sort_order INTEGER DEFAULT 0,
251
+ timeout INTEGER,
252
+ token_limit INTEGER,
253
+ total_tokens_used INTEGER DEFAULT 0,
254
+ reset_interval INTEGER,
255
+ last_reset_at INTEGER,
183
256
  created_at INTEGER NOT NULL,
184
257
  updated_at INTEGER NOT NULL,
185
258
  FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
@@ -252,7 +325,6 @@ class DatabaseManager {
252
325
  name: row.name,
253
326
  apiUrl: row.api_url,
254
327
  apiKey: row.api_key,
255
- timeout: row.timeout,
256
328
  sourceType: row.source_type,
257
329
  supportedModels: row.supported_models ? row.supported_models.split(',').map((model) => model.trim()).filter((model) => model.length > 0) : undefined,
258
330
  modelLimits: row.model_limits ? JSON.parse(row.model_limits) : undefined,
@@ -269,18 +341,18 @@ class DatabaseManager {
269
341
  const id = crypto_1.default.randomUUID();
270
342
  const now = Date.now();
271
343
  this.db
272
- .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
273
- .run(id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, now);
344
+ .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
345
+ .run(id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, now);
274
346
  return Object.assign(Object.assign({}, service), { id, createdAt: now, updatedAt: now });
275
347
  }
276
348
  updateAPIService(id, service) {
277
349
  const now = Date.now();
278
350
  const result = this.db
279
- .prepare('UPDATE api_services SET name = ?, api_url = ?, api_key = ?, timeout = ?, source_type = ?, supported_models = ?, model_limits = ?, updated_at = ? WHERE id = ?')
280
- .run(service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, id);
351
+ .prepare('UPDATE api_services SET name = ?, api_url = ?, api_key = ?, source_type = ?, supported_models = ?, model_limits = ?, updated_at = ? WHERE id = ?')
352
+ .run(service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, now, id);
281
353
  // 调试日志: 记录更新操作
282
354
  if (result.changes > 0 && process.env.NODE_ENV === 'development') {
283
- console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl} (timeout: ${service.timeout})`);
355
+ console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl}`);
284
356
  }
285
357
  return result.changes > 0;
286
358
  }
@@ -348,6 +420,11 @@ class DatabaseManager {
348
420
  targetModel: row.target_model,
349
421
  replacedModel: row.replaced_model,
350
422
  sortOrder: row.sort_order,
423
+ timeout: row.timeout,
424
+ tokenLimit: row.token_limit,
425
+ totalTokensUsed: row.total_tokens_used,
426
+ resetInterval: row.reset_interval,
427
+ lastResetAt: row.last_reset_at,
351
428
  createdAt: row.created_at,
352
429
  updatedAt: row.updated_at,
353
430
  }));
@@ -356,21 +433,68 @@ class DatabaseManager {
356
433
  const id = crypto_1.default.randomUUID();
357
434
  const now = Date.now();
358
435
  this.db
359
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
360
- .run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, now);
436
+ .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
437
+ .run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout || null, route.tokenLimit || null, route.totalTokensUsed || 0, route.resetInterval || null, route.lastResetAt || null, now, now);
361
438
  return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
362
439
  }
363
440
  updateRule(id, route) {
364
441
  const now = Date.now();
365
442
  const result = this.db
366
- .prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, updated_at = ? WHERE id = ?')
367
- .run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, id);
443
+ .prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, updated_at = ? WHERE id = ?')
444
+ .run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout !== undefined ? route.timeout : null, route.tokenLimit !== undefined ? route.tokenLimit : null, route.resetInterval !== undefined ? route.resetInterval : null, now, id);
368
445
  return result.changes > 0;
369
446
  }
370
447
  deleteRule(id) {
371
448
  const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
372
449
  return result.changes > 0;
373
450
  }
451
+ /**
452
+ * 增加规则的token使用量
453
+ * @param ruleId 规则ID
454
+ * @param tokensUsed 使用的token数量
455
+ * @returns 是否成功
456
+ */
457
+ incrementRuleTokenUsage(ruleId, tokensUsed) {
458
+ const result = this.db
459
+ .prepare('UPDATE rules SET total_tokens_used = total_tokens_used + ? WHERE id = ?')
460
+ .run(tokensUsed, ruleId);
461
+ return result.changes > 0;
462
+ }
463
+ /**
464
+ * 重置规则的token使用量
465
+ * @param ruleId 规则ID
466
+ * @returns 是否成功
467
+ */
468
+ resetRuleTokenUsage(ruleId) {
469
+ const now = Date.now();
470
+ const result = this.db
471
+ .prepare('UPDATE rules SET total_tokens_used = 0, last_reset_at = ? WHERE id = ?')
472
+ .run(now, ruleId);
473
+ return result.changes > 0;
474
+ }
475
+ /**
476
+ * 检查并重置到期的规则
477
+ * 如果规则设置了reset_interval且已经到了重置时间,则自动重置token使用量
478
+ * @param ruleId 规则ID
479
+ * @returns 是否进行了重置
480
+ */
481
+ checkAndResetRuleIfNeeded(ruleId) {
482
+ const rule = this.db
483
+ .prepare('SELECT reset_interval, last_reset_at FROM rules WHERE id = ?')
484
+ .get(ruleId);
485
+ if (!rule || !rule.reset_interval) {
486
+ return false; // 没有设置重置间隔
487
+ }
488
+ const now = Date.now();
489
+ const resetIntervalMs = rule.reset_interval * 60 * 60 * 1000; // 小时转毫秒
490
+ const lastResetAt = rule.last_reset_at || 0;
491
+ // 检查是否已经到了重置时间
492
+ if (now - lastResetAt >= resetIntervalMs) {
493
+ this.resetRuleTokenUsage(ruleId);
494
+ return true;
495
+ }
496
+ return false;
497
+ }
374
498
  // Log operations
375
499
  addLog(log) {
376
500
  return __awaiter(this, void 0, void 0, function* () {
@@ -625,8 +749,8 @@ class DatabaseManager {
625
749
  // Import API services
626
750
  for (const service of importData.apiServices) {
627
751
  this.db
628
- .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
629
- .run(service.id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, service.createdAt, service.updatedAt);
752
+ .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, source_type, supported_models, model_limits, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
753
+ .run(service.id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : null, service.modelLimits ? JSON.stringify(service.modelLimits) : null, service.createdAt, service.updatedAt);
630
754
  }
631
755
  // Import routes
632
756
  for (const route of importData.routes) {
@@ -637,8 +761,8 @@ class DatabaseManager {
637
761
  // Import rules
638
762
  for (const rule of importData.rules) {
639
763
  this.db
640
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
641
- .run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.createdAt, rule.updatedAt);
764
+ .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
765
+ .run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.timeout || null, rule.tokenLimit || null, rule.totalTokensUsed || 0, rule.resetInterval || null, rule.lastResetAt || null, rule.createdAt, rule.updatedAt);
642
766
  }
643
767
  // Update config
644
768
  this.updateConfig(importData.config);
@@ -282,6 +282,7 @@ const registerRoutes = (dbManager, proxyServer) => {
282
282
  app.post('/api/rules', (req, res) => res.json(dbManager.createRule(req.body)));
283
283
  app.put('/api/rules/:id', (req, res) => res.json(dbManager.updateRule(req.params.id, req.body)));
284
284
  app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
285
+ app.put('/api/rules/:id/reset-tokens', (req, res) => res.json(dbManager.resetRuleTokenUsage(req.params.id)));
285
286
  app.get('/api/logs', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
286
287
  const rawLimit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : NaN;
287
288
  const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
@@ -476,32 +476,53 @@ class ProxyServer {
476
476
  const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
477
477
  rule.replacedModel &&
478
478
  requestModel.includes(rule.replacedModel));
479
- // 过滤黑名单
479
+ // 过滤黑名单和token限制
480
480
  for (const rule of modelMappingRules) {
481
481
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, rule.contentType);
482
- if (!isBlacklisted) {
483
- return rule;
482
+ if (isBlacklisted) {
483
+ continue;
484
+ }
485
+ // 检查并重置到期的规则
486
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
487
+ // 检查token限制
488
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
489
+ continue; // 跳过超限规则
484
490
  }
491
+ return rule;
485
492
  }
486
493
  }
487
494
  // 2. 查找其他内容类型的规则
488
495
  const contentType = this.determineContentType(req);
489
496
  const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
490
- // 过滤黑名单
497
+ // 过滤黑名单和token限制
491
498
  for (const rule of contentTypeRules) {
492
499
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
493
- if (!isBlacklisted) {
494
- return rule;
500
+ if (isBlacklisted) {
501
+ continue;
495
502
  }
503
+ // 检查并重置到期的规则
504
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
505
+ // 检查token限制
506
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
507
+ continue; // 跳过超限规则
508
+ }
509
+ return rule;
496
510
  }
497
511
  // 3. 最后返回 default 规则
498
512
  const defaultRules = rules.filter(rule => rule.contentType === 'default');
499
- // 过滤黑名单
513
+ // 过滤黑名单和token限制
500
514
  for (const rule of defaultRules) {
501
515
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, 'default');
502
- if (!isBlacklisted) {
503
- return rule;
516
+ if (isBlacklisted) {
517
+ continue;
504
518
  }
519
+ // 检查并重置到期的规则
520
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
521
+ // 检查token限制
522
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
523
+ continue; // 跳过超限规则
524
+ }
525
+ return rule;
505
526
  }
506
527
  return undefined;
507
528
  });
@@ -527,6 +548,23 @@ class ProxyServer {
527
548
  // 3. Default rules
528
549
  const defaultRules = rules.filter(rule => rule.contentType === 'default');
529
550
  candidates.push(...defaultRules);
551
+ // 4. 检查并重置到期的规则
552
+ candidates.forEach(rule => {
553
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
554
+ });
555
+ // 5. 过滤掉超过token限制的规则(仅在有多个候选规则时)
556
+ if (candidates.length > 1) {
557
+ const filteredCandidates = candidates.filter(rule => {
558
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined) {
559
+ return rule.totalTokensUsed < rule.tokenLimit;
560
+ }
561
+ return true; // 没有设置限制的规则总是可用
562
+ });
563
+ // 如果过滤后还有规则,使用过滤后的结果
564
+ if (filteredCandidates.length > 0) {
565
+ return filteredCandidates;
566
+ }
567
+ }
530
568
  return candidates;
531
569
  }
532
570
  determineContentType(req) {
@@ -1008,6 +1046,10 @@ class ProxyServer {
1008
1046
  var _a, _b;
1009
1047
  if (logged || !((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging))
1010
1048
  return;
1049
+ // 只记录来自编程工具的请求
1050
+ if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
1051
+ return;
1052
+ }
1011
1053
  logged = true;
1012
1054
  // 获取供应商信息
1013
1055
  const vendors = this.dbManager.getVendors();
@@ -1026,6 +1068,7 @@ class ProxyServer {
1026
1068
  usage: usageForLog,
1027
1069
  error,
1028
1070
  // 新增字段
1071
+ ruleId: rule.id,
1029
1072
  targetType,
1030
1073
  targetServiceId: service.id,
1031
1074
  targetServiceName: service.name,
@@ -1038,6 +1081,13 @@ class ProxyServer {
1038
1081
  streamChunks: streamChunksForLog,
1039
1082
  upstreamRequest: upstreamRequestForLog,
1040
1083
  });
1084
+ // 更新规则的token使用量(只在成功请求时更新)
1085
+ if (usageForLog && statusCode < 400) {
1086
+ const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
1087
+ if (totalTokens > 0) {
1088
+ this.dbManager.incrementRuleTokenUsage(rule.id, totalTokens);
1089
+ }
1090
+ }
1041
1091
  });
1042
1092
  try {
1043
1093
  if (targetType === 'claude-code') {
@@ -1083,7 +1133,7 @@ class ProxyServer {
1083
1133
  method: req.method,
1084
1134
  url: `${service.apiUrl}${mappedPath}`,
1085
1135
  headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
1086
- timeout: service.timeout || 3000000, // 默认300秒
1136
+ timeout: rule.timeout || 3000000, // 默认300秒
1087
1137
  validateStatus: () => true,
1088
1138
  responseType: streamRequested ? 'stream' : 'json',
1089
1139
  };