aicodeswitch 1.7.1 → 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 +2 -2
- package/dist/server/database.js +91 -10
- package/dist/server/proxy-server.js +85 -2
- package/dist/ui/assets/index-D6QkIxk8.js +391 -0
- package/dist/ui/assets/index-g31C4Oez.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-Bfc2kKan.css +0 -1
- package/dist/ui/assets/index-DGODOljs.js +0 -391
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Codex的配置覆盖逻辑一模一样。
|
|
|
97
97
|
|
|
98
98
|
**有什么用?**
|
|
99
99
|
|
|
100
|
-
aicodeswitch内部,会根据“源类型”来转换数据。例如,你的供应商API服务接口是OpenAI Chat
|
|
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
|
|
package/dist/server/database.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
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) {
|
|
@@ -457,7 +457,12 @@ class ProxyServer {
|
|
|
457
457
|
*/
|
|
458
458
|
getServiceById(serviceId) {
|
|
459
459
|
const allServices = this.dbManager.getAPIServices();
|
|
460
|
-
|
|
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* () {
|
|
@@ -768,6 +773,77 @@ class ProxyServer {
|
|
|
768
773
|
isOpenAIChatSource(sourceType) {
|
|
769
774
|
return sourceType === 'openai-chat' || sourceType === 'deepseek-chat';
|
|
770
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
|
+
}
|
|
771
847
|
applyModelOverride(body, rule) {
|
|
772
848
|
// 如果 targetModel 为空或不存在,保留原始 model(透传)
|
|
773
849
|
if (!rule.targetModel)
|
|
@@ -990,6 +1066,8 @@ class ProxyServer {
|
|
|
990
1066
|
return;
|
|
991
1067
|
}
|
|
992
1068
|
}
|
|
1069
|
+
// 应用 max_output_tokens 限制
|
|
1070
|
+
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
993
1071
|
const streamRequested = this.isStreamRequested(req, requestBody);
|
|
994
1072
|
// Build the full URL by appending the request path to the service API URL
|
|
995
1073
|
let pathToAppend = req.path;
|
|
@@ -1016,9 +1094,14 @@ class ProxyServer {
|
|
|
1016
1094
|
config.data = requestBody;
|
|
1017
1095
|
}
|
|
1018
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);
|
|
1019
1100
|
upstreamRequestForLog = {
|
|
1020
1101
|
url: `${service.apiUrl}${mappedPath}`,
|
|
1021
|
-
model:
|
|
1102
|
+
model: actualModel,
|
|
1103
|
+
maxTokens: actualMaxTokens,
|
|
1104
|
+
maxTokensField: maxTokensFieldName,
|
|
1022
1105
|
};
|
|
1023
1106
|
const response = yield (0, axios_1.default)(config);
|
|
1024
1107
|
const responseHeaders = response.headers || {};
|