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 +2 -2
- package/dist/server/database.js +91 -10
- package/dist/server/proxy-server.js +171 -27
- 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) {
|
|
@@ -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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
1125
|
-
// 对于 Claude Code
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
//
|
|
1139
|
-
res.status(500).json({ error:
|
|
1282
|
+
// 对于 Codex,返回 JSON 格式的错误响应
|
|
1283
|
+
res.status(500).json({ error: errorMessage });
|
|
1140
1284
|
}
|
|
1141
1285
|
}
|
|
1142
1286
|
});
|