aicodeswitch 3.9.2 → 3.9.4

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.
@@ -209,6 +209,8 @@ class FileSystemDatabaseManager {
209
209
  yield this.loadAllData();
210
210
  // 执行数据源类型迁移(在加载数据之后)
211
211
  yield this.migrateSourceTypes();
212
+ // OpenAI base URL 迁移:将末尾 /v1 自动移除
213
+ yield this.migrateOpenAIBaseUrls();
212
214
  // 确保默认配置
213
215
  yield this.ensureDefaultConfig();
214
216
  });
@@ -357,6 +359,41 @@ class FileSystemDatabaseManager {
357
359
  console.log(`[TypeMigration] Migration completed. Migrated ${migratedCount} services.`);
358
360
  });
359
361
  }
362
+ /**
363
+ * 迁移 OpenAI base URL(在初始化时执行)
364
+ * 仅处理 sourceType=openai 且 apiUrl 末尾为 /v1 的服务
365
+ */
366
+ migrateOpenAIBaseUrls() {
367
+ return __awaiter(this, void 0, void 0, function* () {
368
+ console.log('[OpenAIBaseUrlMigration] Checking for OpenAI base URL migration...');
369
+ let migratedCount = 0;
370
+ for (const vendor of this.vendors) {
371
+ if (!vendor.services)
372
+ continue;
373
+ for (const service of vendor.services) {
374
+ if (service.sourceType !== 'openai' || typeof service.apiUrl !== 'string') {
375
+ continue;
376
+ }
377
+ const trimmedUrl = service.apiUrl.trim();
378
+ if (!/\/v1\/?$/i.test(trimmedUrl)) {
379
+ continue;
380
+ }
381
+ const migratedUrl = trimmedUrl.replace(/\/v1\/?$/i, '');
382
+ if (migratedUrl && migratedUrl !== service.apiUrl) {
383
+ console.log(`[OpenAIBaseUrlMigration] Migrated service "${service.name}": ${service.apiUrl} -> ${migratedUrl}`);
384
+ service.apiUrl = migratedUrl;
385
+ migratedCount++;
386
+ }
387
+ }
388
+ }
389
+ if (migratedCount === 0) {
390
+ console.log('[OpenAIBaseUrlMigration] No migration needed');
391
+ return;
392
+ }
393
+ yield this.saveVendors();
394
+ console.log(`[OpenAIBaseUrlMigration] Migration completed. Migrated ${migratedCount} services.`);
395
+ });
396
+ }
360
397
  /**
361
398
  * 迁移导入数据中的类型
362
399
  * 用于导入功能,自动将旧类型转换为新类型
@@ -364,7 +401,13 @@ class FileSystemDatabaseManager {
364
401
  migrateVendorsOnImport(vendors) {
365
402
  return vendors.map(vendor => {
366
403
  var _a;
367
- return (Object.assign(Object.assign({}, vendor), { services: (_a = vendor.services) === null || _a === void 0 ? void 0 : _a.map(service => (Object.assign(Object.assign({}, service), { sourceType: service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined }))) }));
404
+ return (Object.assign(Object.assign({}, vendor), { services: (_a = vendor.services) === null || _a === void 0 ? void 0 : _a.map(service => {
405
+ const normalizedSourceType = service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined;
406
+ const normalizedApiUrl = normalizedSourceType === 'openai' && typeof service.apiUrl === 'string'
407
+ ? service.apiUrl.trim().replace(/\/v1\/?$/i, '')
408
+ : service.apiUrl;
409
+ return Object.assign(Object.assign({}, service), { sourceType: normalizedSourceType, apiUrl: normalizedApiUrl });
410
+ }) }));
368
411
  });
369
412
  }
370
413
  saveVendors() {
@@ -104,6 +104,31 @@ const asyncHandler = (handler) => (req, res, next) => {
104
104
  next(err);
105
105
  });
106
106
  };
107
+ const OPENAI_V1_SUFFIX_RE = /\/v1\/?$/i;
108
+ const validateOpenAIServiceBaseUrl = (service) => {
109
+ if ((service === null || service === void 0 ? void 0 : service.sourceType) !== 'openai') {
110
+ return null;
111
+ }
112
+ if (typeof service.apiUrl !== 'string') {
113
+ return null;
114
+ }
115
+ if (!OPENAI_V1_SUFFIX_RE.test(service.apiUrl.trim())) {
116
+ return null;
117
+ }
118
+ return 'OpenAI 数据源请填写不包含 /v1 的 base URL,例如:https://api.openai.com';
119
+ };
120
+ const validateOpenAIServiceBaseUrlsInVendorPayload = (vendorBody) => {
121
+ if (!Array.isArray(vendorBody === null || vendorBody === void 0 ? void 0 : vendorBody.services)) {
122
+ return null;
123
+ }
124
+ for (const service of vendorBody.services) {
125
+ const error = validateOpenAIServiceBaseUrl(service);
126
+ if (error) {
127
+ return error;
128
+ }
129
+ }
130
+ return null;
131
+ };
107
132
  const writeClaudeConfig = (dbManager, enableAgentTeams, enableBypassPermissionsSupport) => __awaiter(void 0, void 0, void 0, function* () {
108
133
  try {
109
134
  const homeDir = os_1.default.homedir();
@@ -902,8 +927,22 @@ const registerRoutes = (dbManager, proxyServer) => {
902
927
  }
903
928
  });
904
929
  app.get('/api/vendors', (_req, res) => res.json(dbManager.getVendors()));
905
- app.post('/api/vendors', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.createVendor(req.body)); })));
906
- app.put('/api/vendors/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.updateVendor(req.params.id, req.body)); })));
930
+ app.post('/api/vendors', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
931
+ const error = validateOpenAIServiceBaseUrlsInVendorPayload(req.body);
932
+ if (error) {
933
+ res.status(400).json({ error });
934
+ return;
935
+ }
936
+ res.json(yield dbManager.createVendor(req.body));
937
+ })));
938
+ app.put('/api/vendors/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
939
+ const error = validateOpenAIServiceBaseUrlsInVendorPayload(req.body);
940
+ if (error) {
941
+ res.status(400).json({ error });
942
+ return;
943
+ }
944
+ res.json(yield dbManager.updateVendor(req.params.id, req.body));
945
+ })));
907
946
  app.delete('/api/vendors/:id', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
908
947
  try {
909
948
  const result = yield dbManager.deleteVendor(req.params.id);
@@ -921,11 +960,29 @@ const registerRoutes = (dbManager, proxyServer) => {
921
960
  });
922
961
  app.post('/api/services', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
923
962
  console.log('[创建服务] 请求数据:', JSON.stringify(req.body, null, 2));
963
+ const error = validateOpenAIServiceBaseUrl(req.body);
964
+ if (error) {
965
+ res.status(400).json({ error });
966
+ return;
967
+ }
924
968
  const result = yield dbManager.createAPIService(req.body);
925
969
  console.log('[创建服务] 创建结果:', JSON.stringify(result, null, 2));
926
970
  res.json(result);
927
971
  })));
928
- app.put('/api/services/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.updateAPIService(req.params.id, req.body)); })));
972
+ app.put('/api/services/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
973
+ const existingService = dbManager.getAPIService(req.params.id);
974
+ if (!existingService) {
975
+ res.status(404).json({ error: '服务不存在' });
976
+ return;
977
+ }
978
+ const mergedService = Object.assign(Object.assign({}, existingService), req.body);
979
+ const error = validateOpenAIServiceBaseUrl(mergedService);
980
+ if (error) {
981
+ res.status(400).json({ error });
982
+ return;
983
+ }
984
+ res.json(yield dbManager.updateAPIService(req.params.id, req.body));
985
+ })));
929
986
  app.delete('/api/services/:id', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
930
987
  console.log('[删除服务] 请求 ID:', req.params.id);
931
988
  try {
@@ -887,7 +887,8 @@ class ProxyServer {
887
887
  return undefined;
888
888
  const body = req.body;
889
889
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
890
- const contentType = forcedContentType || this.determineContentType(req);
890
+ const route = this.dbManager.getRoutes().find(r => r.id === routeId);
891
+ const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
891
892
  // 高智商规则优先于 model-mapping,确保 !!/推断命中时不会被模型映射覆盖
892
893
  if (contentType === 'high-iq') {
893
894
  const highIqRules = enabledRules.filter(rule => rule.contentType === 'high-iq');
@@ -910,7 +911,32 @@ class ProxyServer {
910
911
  return rule;
911
912
  }
912
913
  }
913
- // 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
914
+ // 1. 查找其他内容类型的规则
915
+ const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
916
+ // 过滤黑名单和token限制
917
+ for (const rule of contentTypeRules) {
918
+ const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
919
+ if (isBlacklisted) {
920
+ continue;
921
+ }
922
+ // 检查并重置到期的规则
923
+ this.dbManager.checkAndResetRuleIfNeeded(rule.id);
924
+ this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
925
+ // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
926
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
927
+ continue; // 跳过超限规则
928
+ }
929
+ // 检查请求次数限制
930
+ if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
931
+ continue; // 跳过超限规则
932
+ }
933
+ // 检查频率限制
934
+ if (this.isFrequencyLimitExceeded(rule)) {
935
+ continue; // 跳过达到频率限制的规则
936
+ }
937
+ return rule;
938
+ }
939
+ // 2. 然后查找 model-mapping 类型的规则
914
940
  if (requestModel) {
915
941
  const modelMappingRules = enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
916
942
  rule.replacedModel &&
@@ -939,31 +965,6 @@ class ProxyServer {
939
965
  return rule;
940
966
  }
941
967
  }
942
- // 2. 查找其他内容类型的规则
943
- const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
944
- // 过滤黑名单和token限制
945
- for (const rule of contentTypeRules) {
946
- const isBlacklisted = yield this.dbManager.isServiceBlacklisted(rule.targetServiceId, routeId, contentType);
947
- if (isBlacklisted) {
948
- continue;
949
- }
950
- // 检查并重置到期的规则
951
- this.dbManager.checkAndResetRuleIfNeeded(rule.id);
952
- this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
953
- // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
954
- if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
955
- continue; // 跳过超限规则
956
- }
957
- // 检查请求次数限制
958
- if (rule.requestCountLimit && rule.totalRequestsUsed !== undefined && rule.totalRequestsUsed >= rule.requestCountLimit) {
959
- continue; // 跳过超限规则
960
- }
961
- // 检查频率限制
962
- if (this.isFrequencyLimitExceeded(rule)) {
963
- continue; // 跳过达到频率限制的规则
964
- }
965
- return rule;
966
- }
967
968
  // 3. 最后返回 default 规则
968
969
  const defaultRules = enabledRules.filter(rule => rule.contentType === 'default');
969
970
  // 过滤黑名单和token限制
@@ -1003,7 +1004,8 @@ class ProxyServer {
1003
1004
  const body = req.body;
1004
1005
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
1005
1006
  const candidates = [];
1006
- const contentType = forcedContentType || this.determineContentType(req);
1007
+ const route = this.dbManager.getRoutes().find(r => r.id === routeId);
1008
+ const contentType = forcedContentType || this.determineContentType(req, (route === null || route === void 0 ? void 0 : route.targetType) || 'claude-code', routeId);
1007
1009
  const prioritizeContentType = contentType === 'high-iq';
1008
1010
  const modelMappingRules = requestModel
1009
1011
  ? enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
@@ -1133,7 +1135,7 @@ class ProxyServer {
1133
1135
  }
1134
1136
  }
1135
1137
  }
1136
- determineContentType(req) {
1138
+ determineContentType(req, targetType, routeId) {
1137
1139
  const body = req.body;
1138
1140
  if (!body)
1139
1141
  return 'default';
@@ -1145,8 +1147,10 @@ class ProxyServer {
1145
1147
  if (explicitType) {
1146
1148
  return explicitType;
1147
1149
  }
1150
+ // 获取sessionId用于session级别的检测(如long-context)
1151
+ const sessionId = this.defaultExtractSessionId(req, targetType);
1148
1152
  for (const detector of this.getContentTypeDetectors()) {
1149
- if (detector.match(req, body)) {
1153
+ if (detector.match(req, body, sessionId, routeId)) {
1150
1154
  return detector.type;
1151
1155
  }
1152
1156
  }
@@ -1158,17 +1162,17 @@ class ProxyServer {
1158
1162
  type: 'image-understanding',
1159
1163
  match: (_req, body) => this.containsImageContent(body.messages) || this.containsImageContent(body.input),
1160
1164
  },
1161
- {
1162
- type: 'thinking',
1163
- match: (_req, body) => this.hasThinkingSignal(body),
1164
- },
1165
1165
  {
1166
1166
  type: 'high-iq',
1167
1167
  match: (_req, body) => this.hasHighIqSignal(body),
1168
1168
  },
1169
1169
  {
1170
1170
  type: 'long-context',
1171
- match: (_req, body) => this.hasLongContextSignal(body),
1171
+ match: (_req, body, sessionId, routeId) => this.hasLongContextSignal(body, sessionId, routeId),
1172
+ },
1173
+ {
1174
+ type: 'thinking',
1175
+ match: (_req, body) => this.hasThinkingSignal(body),
1172
1176
  },
1173
1177
  {
1174
1178
  type: 'background',
@@ -1574,8 +1578,8 @@ class ProxyServer {
1574
1578
  ];
1575
1579
  return candidates.some((value) => value === true || value === 'background');
1576
1580
  }
1577
- hasLongContextSignal(body) {
1578
- var _a, _b;
1581
+ hasLongContextSignal(body, sessionId, routeId) {
1582
+ var _a, _b, _c;
1579
1583
  const explicit = [
1580
1584
  body === null || body === void 0 ? void 0 : body.long_context,
1581
1585
  body === null || body === void 0 ? void 0 : body.longContext,
@@ -1585,6 +1589,22 @@ class ProxyServer {
1585
1589
  if (explicit.some((value) => value === true)) {
1586
1590
  return true;
1587
1591
  }
1592
+ // 检查session累积tokens
1593
+ if (sessionId && routeId) {
1594
+ const session = this.dbManager.getSession(sessionId);
1595
+ if (session && session.totalTokens > 0) {
1596
+ // 查找该route下的long-context规则,获取阈值配置
1597
+ const rules = this.getRulesByRouteId(routeId);
1598
+ const longContextRule = rules === null || rules === void 0 ? void 0 : rules.find(rule => rule.contentType === 'long-context' && !rule.isDisabled);
1599
+ // 默认阈值为1M tokens (1000k)
1600
+ const defaultThreshold = 1000; // 单位:k
1601
+ const threshold = (_c = longContextRule === null || longContextRule === void 0 ? void 0 : longContextRule.sessionTokenThreshold) !== null && _c !== void 0 ? _c : defaultThreshold;
1602
+ // 如果session累积tokens超过阈值,则认为是long-context
1603
+ if (session.totalTokens >= threshold * 1000) {
1604
+ return true;
1605
+ }
1606
+ }
1607
+ }
1588
1608
  const maxTokens = this.extractNumericField(body, [
1589
1609
  'max_tokens',
1590
1610
  'max_output_tokens',
@@ -1694,19 +1714,15 @@ class ProxyServer {
1694
1714
  }
1695
1715
  /**
1696
1716
  * 构建 OpenAI Responses 类型的完整 URL
1697
- * - baseUrl /v{number} 结尾时,直接拼接请求路径
1698
- * - baseUrl 不带版本时,自动补 /v1 再拼接请求路径
1699
- * - 兼容请求路径本身已携带版本前缀(如 /v1/responses)场景
1717
+ * - 用户填写不含 /v1 的 baseUrl
1718
+ * - 服务端固定拼接 /v1 + 请求路径
1700
1719
  */
1701
1720
  buildOpenAIResponsesUrl(baseUrl, mappedPath) {
1702
1721
  const trimmedBase = baseUrl.trim().replace(/\/+$/, '');
1703
1722
  const normalizedPath = mappedPath.startsWith('/') || mappedPath === '' ? mappedPath : `/${mappedPath}`;
1704
- const baseHasVersionSuffix = /\/v\d+$/i.test(trimmedBase);
1705
- const pathHasVersionPrefix = /^\/v\d+(?:\/|$)/i.test(normalizedPath);
1706
- if (baseHasVersionSuffix || pathHasVersionPrefix) {
1707
- return `${trimmedBase}${normalizedPath}`;
1708
- }
1709
- return `${trimmedBase}/v1${normalizedPath}`;
1723
+ const pathWithoutVersionPrefix = normalizedPath.replace(/^\/v\d+(?=\/|$)/i, '');
1724
+ const normalizedResponsesPath = pathWithoutVersionPrefix || '/responses';
1725
+ return `${trimmedBase}/v1${normalizedResponsesPath}`;
1710
1726
  }
1711
1727
  /**
1712
1728
  * 构建 Gemini API 的完整 URL
@@ -2273,8 +2289,9 @@ class ProxyServer {
2273
2289
  // Session 索引逻辑
2274
2290
  const sessionId = this.defaultExtractSessionId(req, targetType);
2275
2291
  if (sessionId) {
2276
- const totalTokens = ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0) +
2277
- ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) || 0);
2292
+ // 正确计算当前请求的tokens:优先使用totalTokens,否则使用input+output
2293
+ const totalTokens = (usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.totalTokens) ||
2294
+ (((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.inputTokens) || 0) + ((usageForLog === null || usageForLog === void 0 ? void 0 : usageForLog.outputTokens) || 0));
2278
2295
  const sessionTitle = this.defaultExtractSessionTitle(req, sessionId);
2279
2296
  const existingSession = this.dbManager.getSession(sessionId);
2280
2297
  this.dbManager.upsertSession({
@@ -2298,7 +2315,8 @@ class ProxyServer {
2298
2315
  }
2299
2316
  // 更新规则的token使用量(只在成功请求时更新)
2300
2317
  if (usageForLog && statusCode < 400) {
2301
- const totalTokens = (usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0);
2318
+ const totalTokens = usageForLog.totalTokens ||
2319
+ ((usageForLog.inputTokens || 0) + (usageForLog.outputTokens || 0));
2302
2320
  if (totalTokens > 0) {
2303
2321
  this.dbManager.incrementRuleTokenUsage(rule.id, totalTokens);
2304
2322
  // 获取更新后的规则数据并广播
@@ -2448,7 +2466,13 @@ class ProxyServer {
2448
2466
  upstreamUrl = this.buildGeminiUrl(service.apiUrl, model, streamRequested);
2449
2467
  }
2450
2468
  else if (sourceType === 'openai') {
2451
- // OpenAI Responses 兼容模式:自动处理 baseUrl 是否包含 /v{number}
2469
+ if (/\/v1\/?$/i.test(service.apiUrl.trim())) {
2470
+ const error = 'OpenAI 数据源请填写不包含 /v1 的 base URL,例如:https://api.openai.com';
2471
+ res.status(400).json({ error });
2472
+ yield finalizeLog(400, error);
2473
+ return;
2474
+ }
2475
+ // OpenAI Responses:固定拼接为 {baseUrl}/v1/*
2452
2476
  upstreamUrl = this.buildOpenAIResponsesUrl(service.apiUrl, mappedPath);
2453
2477
  }
2454
2478
  else if (this.isChatType(sourceType) || this.isGeminiChatSource(sourceType)) {