aicodeswitch 1.7.1 → 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.
package/README.md CHANGED
@@ -97,7 +97,7 @@ Codex的配置覆盖逻辑一模一样。
97
97
 
98
98
  **有什么用?**
99
99
 
100
- aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat的数据格式,而你在路由中配置的“路由对象“是Claude Code,那么就意味着,这个供应商API的数据,需要经过转换之后才能被Claude Code正确使用。
100
+ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat的数据格式,而你在路由中配置的“客户端工具“是Claude Code,那么就意味着,这个供应商API的数据,需要经过转换之后才能被Claude Code正确使用。
101
101
 
102
102
  ## 路由管理
103
103
 
@@ -105,7 +105,7 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
105
105
 
106
106
  路由是aicodeswitch的核心功能,它负责将不同的对象(目前指Claude Code和Codex)的请求,路由到不同的供应商API服务上。
107
107
 
108
- ### 什么是“路由对象”?
108
+ ### 什么是“客户端工具”?
109
109
 
110
110
  目前指Claude Code或Codex。
111
111
 
@@ -58,6 +58,16 @@ class DatabaseManager {
58
58
  value: void 0
59
59
  });
60
60
  this.db = new better_sqlite3_1.default(path_1.default.join(dataPath, 'app.db'));
61
+ // 启用外键约束(SQLite 默认禁用,必须手动启用才能使 ON DELETE CASCADE 生效)
62
+ this.db.pragma('foreign_keys = ON');
63
+ // 配置数据库以确保实时读取最新数据
64
+ // WAL 模式 + normal 同步模式: 提供最佳的并发性和实时性
65
+ this.db.pragma('journal_mode = WAL');
66
+ this.db.pragma('synchronous = NORMAL');
67
+ // 确保读取操作不会看到旧的数据快照
68
+ // 在 WAL 模式下,默认情况下读取操作不会阻塞写入操作
69
+ // 设置 read_uncommitted = 0 确保读取最新提交的数据
70
+ this.db.pragma('read_uncommitted = 0');
61
71
  this.logDb = new level_1.Level(path_1.default.join(dataPath, 'logs'), { valueEncoding: 'json' });
62
72
  this.accessLogDb = new level_1.Level(path_1.default.join(dataPath, 'access-logs'), { valueEncoding: 'json' });
63
73
  this.errorLogDb = new level_1.Level(path_1.default.join(dataPath, 'error-logs'), { valueEncoding: 'json' });
@@ -66,9 +76,137 @@ class DatabaseManager {
66
76
  initialize() {
67
77
  return __awaiter(this, void 0, void 0, function* () {
68
78
  this.createTables();
79
+ yield this.runMigrations();
69
80
  yield this.ensureDefaultConfig();
70
81
  });
71
82
  }
83
+ runMigrations() {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ const columns = this.db.pragma('table_info(api_services)');
86
+ // 检查是否有旧的 max_output_tokens 字段(单值版本)
87
+ const hasOldMaxOutputTokens = columns.some((col) => col.name === 'max_output_tokens');
88
+ // 检查是否已经有新的 model_limits 字段(JSON 版本)
89
+ const hasModelLimits = columns.some((col) => col.name === 'model_limits');
90
+ if (!hasModelLimits) {
91
+ if (hasOldMaxOutputTokens) {
92
+ // 如果有旧字段,先删除旧字段(SQLite 不支持 ALTER TABLE DROP COLUMN,需要重建表)
93
+ console.log('[DB] Running migration: Replacing max_output_tokens with model_limits');
94
+ yield this.migrateMaxOutputTokensToModelLimits();
95
+ }
96
+ else {
97
+ // 直接添加新字段
98
+ console.log('[DB] Running migration: Adding model_limits column to api_services table');
99
+ this.db.exec('ALTER TABLE api_services ADD COLUMN model_limits TEXT;');
100
+ console.log('[DB] Migration completed: model_limits column added');
101
+ }
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
+ }
143
+ });
144
+ }
145
+ migrateMaxOutputTokensToModelLimits() {
146
+ return __awaiter(this, void 0, void 0, function* () {
147
+ // SQLite 不支持直接删除列,需要重建表
148
+ // 先临时禁用外键约束
149
+ this.db.pragma('foreign_keys = OFF');
150
+ this.db.exec(`
151
+ CREATE TABLE api_services_new (
152
+ id TEXT PRIMARY KEY,
153
+ vendor_id TEXT NOT NULL,
154
+ name TEXT NOT NULL,
155
+ api_url TEXT NOT NULL,
156
+ api_key TEXT NOT NULL,
157
+ timeout INTEGER,
158
+ source_type TEXT,
159
+ supported_models TEXT,
160
+ model_limits TEXT,
161
+ created_at INTEGER NOT NULL,
162
+ updated_at INTEGER NOT NULL,
163
+ FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
164
+ );
165
+
166
+ INSERT INTO api_services_new
167
+ SELECT
168
+ id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models,
169
+ NULL, -- model_limits 设为 NULL,旧数据需要手动配置
170
+ created_at, updated_at
171
+ FROM api_services;
172
+
173
+ DROP TABLE api_services;
174
+ ALTER TABLE api_services_new RENAME TO api_services;
175
+ `);
176
+ // 重新启用外键约束
177
+ this.db.pragma('foreign_keys = ON');
178
+ console.log('[DB] Migration completed: Replaced max_output_tokens with model_limits');
179
+ });
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
+ }
72
210
  createTables() {
73
211
  this.db.exec(`
74
212
  CREATE TABLE IF NOT EXISTS vendors (
@@ -85,7 +223,6 @@ class DatabaseManager {
85
223
  name TEXT NOT NULL,
86
224
  api_url TEXT NOT NULL,
87
225
  api_key TEXT NOT NULL,
88
- timeout INTEGER,
89
226
  source_type TEXT,
90
227
  supported_models TEXT,
91
228
  created_at INTEGER NOT NULL,
@@ -111,6 +248,11 @@ class DatabaseManager {
111
248
  target_model TEXT,
112
249
  replaced_model TEXT,
113
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,
114
256
  created_at INTEGER NOT NULL,
115
257
  updated_at INTEGER NOT NULL,
116
258
  FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
@@ -129,7 +271,7 @@ class DatabaseManager {
129
271
  if (!config) {
130
272
  const defaultConfig = {
131
273
  enableLogging: true,
132
- logRetentionDays: 7,
274
+ logRetentionDays: 30,
133
275
  maxLogSize: 1000,
134
276
  apiKey: '',
135
277
  enableFailover: true, // 默认启用智能故障切换
@@ -170,37 +312,48 @@ class DatabaseManager {
170
312
  }
171
313
  // API Service operations
172
314
  getAPIServices(vendorId) {
315
+ // 每次都重新准备语句以确保获取最新数据
173
316
  const query = vendorId
174
317
  ? 'SELECT * FROM api_services WHERE vendor_id = ? ORDER BY created_at DESC'
175
318
  : 'SELECT * FROM api_services ORDER BY created_at DESC';
176
- const stmt = vendorId ? this.db.prepare(query).bind(vendorId) : this.db.prepare(query);
177
- const rows = stmt.all();
178
- return rows.map((row) => ({
319
+ // 不缓存 prepared statement,每次重新创建以确保读取最新数据
320
+ const stmt = this.db.prepare(query);
321
+ const rows = vendorId ? stmt.all(vendorId) : stmt.all();
322
+ const services = rows.map((row) => ({
179
323
  id: row.id,
180
324
  vendorId: row.vendor_id,
181
325
  name: row.name,
182
326
  apiUrl: row.api_url,
183
327
  apiKey: row.api_key,
184
- timeout: row.timeout,
185
328
  sourceType: row.source_type,
186
329
  supportedModels: row.supported_models ? row.supported_models.split(',').map((model) => model.trim()).filter((model) => model.length > 0) : undefined,
330
+ modelLimits: row.model_limits ? JSON.parse(row.model_limits) : undefined,
187
331
  createdAt: row.created_at,
188
332
  updatedAt: row.updated_at,
189
333
  }));
334
+ // 调试日志: 记录读取的服务信息
335
+ if (process.env.NODE_ENV === 'development' && services.length > 0) {
336
+ console.log(`[DB] Read ${services.length} services from database, first service: ${services[0].name} -> ${services[0].apiUrl}`);
337
+ }
338
+ return services;
190
339
  }
191
340
  createAPIService(service) {
192
341
  const id = crypto_1.default.randomUUID();
193
342
  const now = Date.now();
194
343
  this.db
195
- .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
196
- .run(id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : 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);
197
346
  return Object.assign(Object.assign({}, service), { id, createdAt: now, updatedAt: now });
198
347
  }
199
348
  updateAPIService(id, service) {
200
349
  const now = Date.now();
201
350
  const result = this.db
202
- .prepare('UPDATE api_services SET name = ?, api_url = ?, api_key = ?, timeout = ?, source_type = ?, supported_models = ?, updated_at = ? WHERE id = ?')
203
- .run(service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : 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);
353
+ // 调试日志: 记录更新操作
354
+ if (result.changes > 0 && process.env.NODE_ENV === 'development') {
355
+ console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl}`);
356
+ }
204
357
  return result.changes > 0;
205
358
  }
206
359
  deleteAPIService(id) {
@@ -267,6 +420,11 @@ class DatabaseManager {
267
420
  targetModel: row.target_model,
268
421
  replacedModel: row.replaced_model,
269
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,
270
428
  createdAt: row.created_at,
271
429
  updatedAt: row.updated_at,
272
430
  }));
@@ -275,21 +433,68 @@ class DatabaseManager {
275
433
  const id = crypto_1.default.randomUUID();
276
434
  const now = Date.now();
277
435
  this.db
278
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
279
- .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);
280
438
  return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
281
439
  }
282
440
  updateRule(id, route) {
283
441
  const now = Date.now();
284
442
  const result = this.db
285
- .prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, updated_at = ? WHERE id = ?')
286
- .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);
287
445
  return result.changes > 0;
288
446
  }
289
447
  deleteRule(id) {
290
448
  const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
291
449
  return result.changes > 0;
292
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
+ }
293
498
  // Log operations
294
499
  addLog(log) {
295
500
  return __awaiter(this, void 0, void 0, function* () {
@@ -544,8 +749,8 @@ class DatabaseManager {
544
749
  // Import API services
545
750
  for (const service of importData.apiServices) {
546
751
  this.db
547
- .prepare('INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
548
- .run(service.id, service.vendorId, service.name, service.apiUrl, service.apiKey, service.timeout || null, service.sourceType || null, service.supportedModels ? service.supportedModels.join(',') : 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);
549
754
  }
550
755
  // Import routes
551
756
  for (const route of importData.routes) {
@@ -556,8 +761,8 @@ class DatabaseManager {
556
761
  // Import rules
557
762
  for (const rule of importData.rules) {
558
763
  this.db
559
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
560
- .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);
561
766
  }
562
767
  // Update config
563
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;
@@ -457,7 +457,12 @@ class ProxyServer {
457
457
  */
458
458
  getServiceById(serviceId) {
459
459
  const allServices = this.dbManager.getAPIServices();
460
- return allServices.find(service => service.id === serviceId);
460
+ const service = allServices.find(s => s.id === serviceId);
461
+ // 调试日志: 记录获取的服务信息
462
+ if (process.env.NODE_ENV === 'development' && service) {
463
+ console.log(`[Proxy] getServiceById(${serviceId}): ${service.name} -> ${service.apiUrl}`);
464
+ }
465
+ return service;
461
466
  }
462
467
  findMatchingRule(routeId, req) {
463
468
  return __awaiter(this, void 0, void 0, function* () {
@@ -471,32 +476,53 @@ class ProxyServer {
471
476
  const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
472
477
  rule.replacedModel &&
473
478
  requestModel.includes(rule.replacedModel));
474
- // 过滤黑名单
479
+ // 过滤黑名单和token限制
475
480
  for (const rule of modelMappingRules) {
476
481
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, rule.contentType);
477
- if (!isBlacklisted) {
478
- return rule;
482
+ if (isBlacklisted) {
483
+ continue;
479
484
  }
485
+ // 检查并重置到期的规则
486
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
487
+ // 检查token限制
488
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
489
+ continue; // 跳过超限规则
490
+ }
491
+ return rule;
480
492
  }
481
493
  }
482
494
  // 2. 查找其他内容类型的规则
483
495
  const contentType = this.determineContentType(req);
484
496
  const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
485
- // 过滤黑名单
497
+ // 过滤黑名单和token限制
486
498
  for (const rule of contentTypeRules) {
487
499
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
488
- if (!isBlacklisted) {
489
- return rule;
500
+ if (isBlacklisted) {
501
+ continue;
490
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;
491
510
  }
492
511
  // 3. 最后返回 default 规则
493
512
  const defaultRules = rules.filter(rule => rule.contentType === 'default');
494
- // 过滤黑名单
513
+ // 过滤黑名单和token限制
495
514
  for (const rule of defaultRules) {
496
515
  const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, 'default');
497
- if (!isBlacklisted) {
498
- return rule;
516
+ if (isBlacklisted) {
517
+ continue;
518
+ }
519
+ // 检查并重置到期的规则
520
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
521
+ // 检查token限制
522
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
523
+ continue; // 跳过超限规则
499
524
  }
525
+ return rule;
500
526
  }
501
527
  return undefined;
502
528
  });
@@ -522,6 +548,23 @@ class ProxyServer {
522
548
  // 3. Default rules
523
549
  const defaultRules = rules.filter(rule => rule.contentType === 'default');
524
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
+ }
525
568
  return candidates;
526
569
  }
527
570
  determineContentType(req) {
@@ -768,6 +811,77 @@ class ProxyServer {
768
811
  isOpenAIChatSource(sourceType) {
769
812
  return sourceType === 'openai-chat' || sourceType === 'deepseek-chat';
770
813
  }
814
+ /**
815
+ * 判断模型是否应该使用 max_completion_tokens 字段
816
+ * GPT 的新模型(如 o1 系列)使用 max_completion_tokens
817
+ */
818
+ shouldUseMaxCompletionTokens(model) {
819
+ if (!model)
820
+ return false;
821
+ const lowerModel = model.toLowerCase();
822
+ // o1 系列模型使用 max_completion_tokens
823
+ return lowerModel.includes('o1-') ||
824
+ lowerModel.startsWith('o1') ||
825
+ lowerModel.includes('gpt-4.1') ||
826
+ lowerModel.includes('gpt-4o') ||
827
+ lowerModel.startsWith('chatgpt-');
828
+ }
829
+ /**
830
+ * 获取 max tokens 字段的名称
831
+ */
832
+ getMaxTokensFieldName(model) {
833
+ return this.shouldUseMaxCompletionTokens(model) ? 'max_completion_tokens' : 'max_tokens';
834
+ }
835
+ /**
836
+ * 应用 max_output_tokens 限制
837
+ * 根据服务的 modelLimits 配置,对具体模型应用 max_tokens/max_completion_tokens 限制
838
+ */
839
+ applyMaxOutputTokensLimit(body, service) {
840
+ if (!service.modelLimits || !body || typeof body !== 'object') {
841
+ return body;
842
+ }
843
+ const result = Object.assign({}, body);
844
+ const model = result.model;
845
+ if (!model) {
846
+ return body;
847
+ }
848
+ // 查找该模型的限制配置
849
+ // 支持精确匹配和前缀匹配(例如:gpt-4 可以匹配 gpt-4-turbo)
850
+ let maxOutputLimit;
851
+ // 1. 先尝试精确匹配
852
+ if (typeof service.modelLimits[model] === 'number') {
853
+ maxOutputLimit = service.modelLimits[model];
854
+ }
855
+ else {
856
+ // 2. 尝试前缀匹配(查找配置中以模型名开头的项)
857
+ const matchedKey = Object.keys(service.modelLimits).find(key => model.startsWith(key) || key.startsWith(model));
858
+ if (matchedKey && typeof service.modelLimits[matchedKey] === 'number') {
859
+ maxOutputLimit = service.modelLimits[matchedKey];
860
+ }
861
+ }
862
+ if (maxOutputLimit === undefined) {
863
+ // 没有找到配置,直接透传
864
+ return body;
865
+ }
866
+ const maxTokensFieldName = this.getMaxTokensFieldName(model);
867
+ // 获取请求中的 max_tokens 或 max_completion_tokens 值
868
+ const requestedMaxTokens = result[maxTokensFieldName] || result.max_tokens;
869
+ // 如果请求中指定了 max_tokens,并且超过配置的限制,则限制为配置的最大值
870
+ if (typeof requestedMaxTokens === 'number' && requestedMaxTokens > maxOutputLimit) {
871
+ console.log(`[Proxy] Limiting ${maxTokensFieldName} from ${requestedMaxTokens} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
872
+ result[maxTokensFieldName] = maxOutputLimit;
873
+ // 如果使用了 max_completion_tokens,清理旧的 max_tokens 字段
874
+ if (maxTokensFieldName === 'max_completion_tokens' && result.max_tokens !== undefined) {
875
+ delete result.max_tokens;
876
+ }
877
+ }
878
+ else if (requestedMaxTokens === undefined) {
879
+ // 如果请求中没有指定 max_tokens,则使用配置的最大值
880
+ console.log(`[Proxy] Setting ${maxTokensFieldName} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
881
+ result[maxTokensFieldName] = maxOutputLimit;
882
+ }
883
+ return result;
884
+ }
771
885
  applyModelOverride(body, rule) {
772
886
  // 如果 targetModel 为空或不存在,保留原始 model(透传)
773
887
  if (!rule.targetModel)
@@ -932,6 +1046,10 @@ class ProxyServer {
932
1046
  var _a, _b;
933
1047
  if (logged || !((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableLogging))
934
1048
  return;
1049
+ // 只记录来自编程工具的请求
1050
+ if (!SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
1051
+ return;
1052
+ }
935
1053
  logged = true;
936
1054
  // 获取供应商信息
937
1055
  const vendors = this.dbManager.getVendors();
@@ -950,6 +1068,7 @@ class ProxyServer {
950
1068
  usage: usageForLog,
951
1069
  error,
952
1070
  // 新增字段
1071
+ ruleId: rule.id,
953
1072
  targetType,
954
1073
  targetServiceId: service.id,
955
1074
  targetServiceName: service.name,
@@ -962,6 +1081,13 @@ class ProxyServer {
962
1081
  streamChunks: streamChunksForLog,
963
1082
  upstreamRequest: upstreamRequestForLog,
964
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
+ }
965
1091
  });
966
1092
  try {
967
1093
  if (targetType === 'claude-code') {
@@ -990,6 +1116,8 @@ class ProxyServer {
990
1116
  return;
991
1117
  }
992
1118
  }
1119
+ // 应用 max_output_tokens 限制
1120
+ requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
993
1121
  const streamRequested = this.isStreamRequested(req, requestBody);
994
1122
  // Build the full URL by appending the request path to the service API URL
995
1123
  let pathToAppend = req.path;
@@ -1005,7 +1133,7 @@ class ProxyServer {
1005
1133
  method: req.method,
1006
1134
  url: `${service.apiUrl}${mappedPath}`,
1007
1135
  headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
1008
- timeout: service.timeout || 3000000, // 默认300秒
1136
+ timeout: rule.timeout || 3000000, // 默认300秒
1009
1137
  validateStatus: () => true,
1010
1138
  responseType: streamRequested ? 'stream' : 'json',
1011
1139
  };
@@ -1016,9 +1144,14 @@ class ProxyServer {
1016
1144
  config.data = requestBody;
1017
1145
  }
1018
1146
  // 记录实际发出的请求信息作为日志的一部分
1147
+ const actualModel = (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '';
1148
+ const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
1149
+ const actualMaxTokens = (requestBody === null || requestBody === void 0 ? void 0 : requestBody[maxTokensFieldName]) || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.max_tokens);
1019
1150
  upstreamRequestForLog = {
1020
1151
  url: `${service.apiUrl}${mappedPath}`,
1021
- model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '',
1152
+ model: actualModel,
1153
+ maxTokens: actualMaxTokens,
1154
+ maxTokensField: maxTokensFieldName,
1022
1155
  };
1023
1156
  const response = yield (0, axios_1.default)(config);
1024
1157
  const responseHeaders = response.headers || {};