aicodeswitch 1.7.0 → 1.8.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,68 @@ 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
+ });
104
+ }
105
+ migrateMaxOutputTokensToModelLimits() {
106
+ return __awaiter(this, void 0, void 0, function* () {
107
+ // SQLite 不支持直接删除列,需要重建表
108
+ // 先临时禁用外键约束
109
+ this.db.pragma('foreign_keys = OFF');
110
+ this.db.exec(`
111
+ CREATE TABLE api_services_new (
112
+ id TEXT PRIMARY KEY,
113
+ vendor_id TEXT NOT NULL,
114
+ name TEXT NOT NULL,
115
+ api_url TEXT NOT NULL,
116
+ api_key TEXT NOT NULL,
117
+ timeout INTEGER,
118
+ source_type TEXT,
119
+ supported_models TEXT,
120
+ model_limits TEXT,
121
+ created_at INTEGER NOT NULL,
122
+ updated_at INTEGER NOT NULL,
123
+ FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
124
+ );
125
+
126
+ INSERT INTO api_services_new
127
+ SELECT
128
+ id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models,
129
+ NULL, -- model_limits 设为 NULL,旧数据需要手动配置
130
+ created_at, updated_at
131
+ FROM api_services;
132
+
133
+ DROP TABLE api_services;
134
+ ALTER TABLE api_services_new RENAME TO api_services;
135
+ `);
136
+ // 重新启用外键约束
137
+ this.db.pragma('foreign_keys = ON');
138
+ console.log('[DB] Migration completed: Replaced max_output_tokens with model_limits');
139
+ });
140
+ }
72
141
  createTables() {
73
142
  this.db.exec(`
74
143
  CREATE TABLE IF NOT EXISTS vendors (
@@ -129,7 +198,7 @@ class DatabaseManager {
129
198
  if (!config) {
130
199
  const defaultConfig = {
131
200
  enableLogging: true,
132
- logRetentionDays: 7,
201
+ logRetentionDays: 30,
133
202
  maxLogSize: 1000,
134
203
  apiKey: '',
135
204
  enableFailover: true, // 默认启用智能故障切换
@@ -170,12 +239,14 @@ class DatabaseManager {
170
239
  }
171
240
  // API Service operations
172
241
  getAPIServices(vendorId) {
242
+ // 每次都重新准备语句以确保获取最新数据
173
243
  const query = vendorId
174
244
  ? 'SELECT * FROM api_services WHERE vendor_id = ? ORDER BY created_at DESC'
175
245
  : '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) => ({
246
+ // 不缓存 prepared statement,每次重新创建以确保读取最新数据
247
+ const stmt = this.db.prepare(query);
248
+ const rows = vendorId ? stmt.all(vendorId) : stmt.all();
249
+ const services = rows.map((row) => ({
179
250
  id: row.id,
180
251
  vendorId: row.vendor_id,
181
252
  name: row.name,
@@ -184,23 +255,33 @@ class DatabaseManager {
184
255
  timeout: row.timeout,
185
256
  sourceType: row.source_type,
186
257
  supportedModels: row.supported_models ? row.supported_models.split(',').map((model) => model.trim()).filter((model) => model.length > 0) : undefined,
258
+ modelLimits: row.model_limits ? JSON.parse(row.model_limits) : undefined,
187
259
  createdAt: row.created_at,
188
260
  updatedAt: row.updated_at,
189
261
  }));
262
+ // 调试日志: 记录读取的服务信息
263
+ if (process.env.NODE_ENV === 'development' && services.length > 0) {
264
+ console.log(`[DB] Read ${services.length} services from database, first service: ${services[0].name} -> ${services[0].apiUrl}`);
265
+ }
266
+ return services;
190
267
  }
191
268
  createAPIService(service) {
192
269
  const id = crypto_1.default.randomUUID();
193
270
  const now = Date.now();
194
271
  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);
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);
197
274
  return Object.assign(Object.assign({}, service), { id, createdAt: now, updatedAt: now });
198
275
  }
199
276
  updateAPIService(id, service) {
200
277
  const now = Date.now();
201
278
  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);
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);
281
+ // 调试日志: 记录更新操作
282
+ if (result.changes > 0 && process.env.NODE_ENV === 'development') {
283
+ console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl} (timeout: ${service.timeout})`);
284
+ }
204
285
  return result.changes > 0;
205
286
  }
206
287
  deleteAPIService(id) {
@@ -544,8 +625,8 @@ class DatabaseManager {
544
625
  // Import API services
545
626
  for (const service of importData.apiServices) {
546
627
  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);
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);
549
630
  }
550
631
  // Import routes
551
632
  for (const route of importData.routes) {
@@ -228,10 +228,24 @@ class ProxyServer {
228
228
  requestHeaders: this.normalizeHeaders(req.headers),
229
229
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
230
230
  });
231
- res.status(503).json({
232
- error: 'All services failed',
233
- details: lastError === null || lastError === void 0 ? void 0 : lastError.message
234
- });
231
+ // 根据路径判断目标类型并返回适当的错误格式
232
+ const isClaudeCode = req.path.startsWith('/claude-code/');
233
+ if (isClaudeCode) {
234
+ const claudeError = {
235
+ type: 'error',
236
+ error: {
237
+ type: 'api_error',
238
+ message: 'All API services failed. Please try again later.'
239
+ }
240
+ };
241
+ res.status(503).json(claudeError);
242
+ }
243
+ else {
244
+ res.status(503).json({
245
+ error: 'All services failed',
246
+ details: lastError === null || lastError === void 0 ? void 0 : lastError.message
247
+ });
248
+ }
235
249
  }
236
250
  catch (error) {
237
251
  console.error('Proxy error:', error);
@@ -256,7 +270,21 @@ class ProxyServer {
256
270
  requestHeaders: this.normalizeHeaders(req.headers),
257
271
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
258
272
  });
259
- res.status(500).json({ error: error.message });
273
+ // 根据路径判断目标类型并返回适当的错误格式
274
+ const isClaudeCode = req.path.startsWith('/claude-code/');
275
+ if (isClaudeCode) {
276
+ const claudeError = {
277
+ type: 'error',
278
+ error: {
279
+ type: 'api_error',
280
+ message: error.message || 'Internal server error'
281
+ }
282
+ };
283
+ res.status(500).json(claudeError);
284
+ }
285
+ else {
286
+ res.status(500).json({ error: error.message });
287
+ }
260
288
  }
261
289
  }));
262
290
  }
@@ -350,10 +378,24 @@ class ProxyServer {
350
378
  requestHeaders: this.normalizeHeaders(req.headers),
351
379
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
352
380
  });
353
- res.status(503).json({
354
- error: 'All services failed',
355
- details: lastError === null || lastError === void 0 ? void 0 : lastError.message
356
- });
381
+ // 根据路径判断目标类型并返回适当的错误格式
382
+ const isClaudeCode = req.path.startsWith('/claude-code/');
383
+ if (isClaudeCode) {
384
+ const claudeError = {
385
+ type: 'error',
386
+ error: {
387
+ type: 'api_error',
388
+ message: 'All API services failed. Please try again later.'
389
+ }
390
+ };
391
+ res.status(503).json(claudeError);
392
+ }
393
+ else {
394
+ res.status(503).json({
395
+ error: 'All services failed',
396
+ details: lastError === null || lastError === void 0 ? void 0 : lastError.message
397
+ });
398
+ }
357
399
  }
358
400
  catch (error) {
359
401
  console.error(`Fixed route error for ${targetType}:`, error);
@@ -415,7 +457,12 @@ class ProxyServer {
415
457
  */
416
458
  getServiceById(serviceId) {
417
459
  const allServices = this.dbManager.getAPIServices();
418
- 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;
419
466
  }
420
467
  findMatchingRule(routeId, req) {
421
468
  return __awaiter(this, void 0, void 0, function* () {
@@ -726,6 +773,77 @@ class ProxyServer {
726
773
  isOpenAIChatSource(sourceType) {
727
774
  return sourceType === 'openai-chat' || sourceType === 'deepseek-chat';
728
775
  }
776
+ /**
777
+ * 判断模型是否应该使用 max_completion_tokens 字段
778
+ * GPT 的新模型(如 o1 系列)使用 max_completion_tokens
779
+ */
780
+ shouldUseMaxCompletionTokens(model) {
781
+ if (!model)
782
+ return false;
783
+ const lowerModel = model.toLowerCase();
784
+ // o1 系列模型使用 max_completion_tokens
785
+ return lowerModel.includes('o1-') ||
786
+ lowerModel.startsWith('o1') ||
787
+ lowerModel.includes('gpt-4.1') ||
788
+ lowerModel.includes('gpt-4o') ||
789
+ lowerModel.startsWith('chatgpt-');
790
+ }
791
+ /**
792
+ * 获取 max tokens 字段的名称
793
+ */
794
+ getMaxTokensFieldName(model) {
795
+ return this.shouldUseMaxCompletionTokens(model) ? 'max_completion_tokens' : 'max_tokens';
796
+ }
797
+ /**
798
+ * 应用 max_output_tokens 限制
799
+ * 根据服务的 modelLimits 配置,对具体模型应用 max_tokens/max_completion_tokens 限制
800
+ */
801
+ applyMaxOutputTokensLimit(body, service) {
802
+ if (!service.modelLimits || !body || typeof body !== 'object') {
803
+ return body;
804
+ }
805
+ const result = Object.assign({}, body);
806
+ const model = result.model;
807
+ if (!model) {
808
+ return body;
809
+ }
810
+ // 查找该模型的限制配置
811
+ // 支持精确匹配和前缀匹配(例如:gpt-4 可以匹配 gpt-4-turbo)
812
+ let maxOutputLimit;
813
+ // 1. 先尝试精确匹配
814
+ if (typeof service.modelLimits[model] === 'number') {
815
+ maxOutputLimit = service.modelLimits[model];
816
+ }
817
+ else {
818
+ // 2. 尝试前缀匹配(查找配置中以模型名开头的项)
819
+ const matchedKey = Object.keys(service.modelLimits).find(key => model.startsWith(key) || key.startsWith(model));
820
+ if (matchedKey && typeof service.modelLimits[matchedKey] === 'number') {
821
+ maxOutputLimit = service.modelLimits[matchedKey];
822
+ }
823
+ }
824
+ if (maxOutputLimit === undefined) {
825
+ // 没有找到配置,直接透传
826
+ return body;
827
+ }
828
+ const maxTokensFieldName = this.getMaxTokensFieldName(model);
829
+ // 获取请求中的 max_tokens 或 max_completion_tokens 值
830
+ const requestedMaxTokens = result[maxTokensFieldName] || result.max_tokens;
831
+ // 如果请求中指定了 max_tokens,并且超过配置的限制,则限制为配置的最大值
832
+ if (typeof requestedMaxTokens === 'number' && requestedMaxTokens > maxOutputLimit) {
833
+ console.log(`[Proxy] Limiting ${maxTokensFieldName} from ${requestedMaxTokens} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
834
+ result[maxTokensFieldName] = maxOutputLimit;
835
+ // 如果使用了 max_completion_tokens,清理旧的 max_tokens 字段
836
+ if (maxTokensFieldName === 'max_completion_tokens' && result.max_tokens !== undefined) {
837
+ delete result.max_tokens;
838
+ }
839
+ }
840
+ else if (requestedMaxTokens === undefined) {
841
+ // 如果请求中没有指定 max_tokens,则使用配置的最大值
842
+ console.log(`[Proxy] Setting ${maxTokensFieldName} to ${maxOutputLimit} for model ${model} in service ${service.name}`);
843
+ result[maxTokensFieldName] = maxOutputLimit;
844
+ }
845
+ return result;
846
+ }
729
847
  applyModelOverride(body, rule) {
730
848
  // 如果 targetModel 为空或不存在,保留原始 model(透传)
731
849
  if (!rule.targetModel)
@@ -873,7 +991,7 @@ class ProxyServer {
873
991
  }
874
992
  proxyRequest(req, res, route, rule, service) {
875
993
  return __awaiter(this, void 0, void 0, function* () {
876
- var _a;
994
+ var _a, _b;
877
995
  res.locals.skipLog = true;
878
996
  const startTime = Date.now();
879
997
  const sourceType = (service.sourceType || 'openai-chat');
@@ -948,6 +1066,8 @@ class ProxyServer {
948
1066
  return;
949
1067
  }
950
1068
  }
1069
+ // 应用 max_output_tokens 限制
1070
+ requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
951
1071
  const streamRequested = this.isStreamRequested(req, requestBody);
952
1072
  // Build the full URL by appending the request path to the service API URL
953
1073
  let pathToAppend = req.path;
@@ -974,9 +1094,14 @@ class ProxyServer {
974
1094
  config.data = requestBody;
975
1095
  }
976
1096
  // 记录实际发出的请求信息作为日志的一部分
1097
+ const actualModel = (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '';
1098
+ const maxTokensFieldName = this.getMaxTokensFieldName(actualModel);
1099
+ const actualMaxTokens = (requestBody === null || requestBody === void 0 ? void 0 : requestBody[maxTokensFieldName]) || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.max_tokens);
977
1100
  upstreamRequestForLog = {
978
1101
  url: `${service.apiUrl}${mappedPath}`,
979
- model: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model) || '',
1102
+ model: actualModel,
1103
+ maxTokens: actualMaxTokens,
1104
+ maxTokensField: maxTokensFieldName,
980
1105
  };
981
1106
  const response = yield (0, axios_1.default)(config);
982
1107
  const responseHeaders = response.headers || {};
@@ -1118,25 +1243,44 @@ class ProxyServer {
1118
1243
  }
1119
1244
  catch (error) {
1120
1245
  console.error('Proxy error:', error);
1121
- yield finalizeLog(500, error.message);
1246
+ // 检测是否是 timeout 错误
1247
+ const isTimeout = error.code === 'ECONNABORTED' ||
1248
+ ((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
1249
+ (error.errno && error.errno === 'ETIMEDOUT');
1250
+ const errorMessage = isTimeout
1251
+ ? 'Request timeout - the upstream API took too long to respond'
1252
+ : (error.message || 'Internal server error');
1253
+ yield finalizeLog(500, errorMessage);
1122
1254
  // 根据请求类型返回适当格式的错误响应
1123
1255
  const streamRequested = this.isStreamRequested(req, req.body || {});
1124
- if (streamRequested && route.targetType === 'claude-code') {
1125
- // 对于 Claude Code 的流式请求,返回 SSE 格式的错误响应
1126
- res.setHeader('Content-Type', 'text/event-stream');
1127
- res.setHeader('Cache-Control', 'no-cache');
1128
- res.setHeader('Connection', 'keep-alive');
1129
- res.status(500);
1130
- // 发送错误事件
1131
- const errorEvent = `event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`;
1132
- const doneEvent = `data: [DONE]\n\n`;
1133
- res.write(errorEvent);
1134
- res.write(doneEvent);
1135
- res.end();
1256
+ if (route.targetType === 'claude-code') {
1257
+ // 对于 Claude Code,返回符合 Claude API 标准的错误响应
1258
+ const claudeError = {
1259
+ type: 'error',
1260
+ error: {
1261
+ type: isTimeout ? 'api_error' : 'api_error',
1262
+ message: errorMessage
1263
+ }
1264
+ };
1265
+ if (streamRequested) {
1266
+ // 流式请求:使用 SSE 格式
1267
+ res.setHeader('Content-Type', 'text/event-stream');
1268
+ res.setHeader('Cache-Control', 'no-cache');
1269
+ res.setHeader('Connection', 'keep-alive');
1270
+ res.status(200);
1271
+ // 发送错误事件(使用 Claude API 的标准格式)
1272
+ const errorEvent = `event: error\ndata: ${JSON.stringify(claudeError)}\n\n`;
1273
+ res.write(errorEvent);
1274
+ res.end();
1275
+ }
1276
+ else {
1277
+ // 非流式请求:返回 JSON 格式
1278
+ res.status(500).json(claudeError);
1279
+ }
1136
1280
  }
1137
1281
  else {
1138
- // 对于非流式请求,返回 JSON 格式的错误响应
1139
- res.status(500).json({ error: error.message });
1282
+ // 对于 Codex,返回 JSON 格式的错误响应
1283
+ res.status(500).json({ error: errorMessage });
1140
1284
  }
1141
1285
  }
1142
1286
  });