@adversity/coding-tool-x 3.1.0 → 3.1.1
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/CHANGELOG.md +15 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +30 -18
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +15 -3
- package/src/server/index.js +165 -58
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +27 -18
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
const https = require('https');
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const { URL } = require('url');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
10
|
const { probeModelAvailability } = require('./model-detector');
|
|
11
|
+
const { getEffectiveApiKey: getClaudeEffectiveApiKey } = require('./channels');
|
|
12
|
+
const { getEffectiveApiKey: getCodexEffectiveApiKey } = require('./codex-channels');
|
|
13
|
+
const { getEffectiveApiKey: getGeminiEffectiveApiKey } = require('./gemini-channels');
|
|
14
|
+
const { getEffectiveApiKey: getOpenCodeEffectiveApiKey } = require('./opencode-channels');
|
|
13
15
|
|
|
14
16
|
// 测试结果缓存
|
|
15
17
|
const testResultsCache = new Map();
|
|
16
18
|
|
|
17
|
-
// Codex 请求体模板文件路径
|
|
18
|
-
const CODEX_REQUEST_TEMPLATE_PATH = path.join(__dirname, 'codex-speed-test-template.json');
|
|
19
|
-
|
|
20
19
|
// 超时配置(毫秒)
|
|
21
20
|
const DEFAULT_TIMEOUT = 15000;
|
|
22
21
|
const MIN_TIMEOUT = 5000;
|
|
23
22
|
const MAX_TIMEOUT = 60000;
|
|
23
|
+
const CLAUDE_CODE_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,prompt-caching-2024-07-31';
|
|
24
|
+
const ROUTE_OR_METHOD_MISMATCH_STATUS = new Set([404, 405, 501]);
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 规范化超时时间
|
|
@@ -30,6 +31,203 @@ function sanitizeTimeout(timeout) {
|
|
|
30
31
|
return Math.min(Math.max(ms, MIN_TIMEOUT), MAX_TIMEOUT);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 规范化批量测速并发度(默认小并发)
|
|
36
|
+
*/
|
|
37
|
+
function sanitizeBatchConcurrency(concurrency, defaultValue = 2) {
|
|
38
|
+
const value = Number(concurrency);
|
|
39
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
40
|
+
return defaultValue;
|
|
41
|
+
}
|
|
42
|
+
return Math.min(Math.max(Math.round(value), 1), 5);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 按并发限制执行异步任务,保持结果顺序与输入一致
|
|
47
|
+
*/
|
|
48
|
+
async function runWithConcurrencyLimit(items, concurrency, taskFn) {
|
|
49
|
+
const list = Array.isArray(items) ? items : [];
|
|
50
|
+
if (list.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
const limit = sanitizeBatchConcurrency(concurrency);
|
|
53
|
+
const results = new Array(list.length);
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
|
|
56
|
+
async function worker() {
|
|
57
|
+
while (true) {
|
|
58
|
+
const currentIndex = cursor;
|
|
59
|
+
cursor += 1;
|
|
60
|
+
if (currentIndex >= list.length) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
results[currentIndex] = await taskFn(list[currentIndex], currentIndex);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const workers = [];
|
|
68
|
+
const workerCount = Math.min(limit, list.length);
|
|
69
|
+
for (let i = 0; i < workerCount; i += 1) {
|
|
70
|
+
workers.push(worker());
|
|
71
|
+
}
|
|
72
|
+
await Promise.all(workers);
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeNonEmptyString(value) {
|
|
77
|
+
if (typeof value !== 'string') return null;
|
|
78
|
+
const trimmed = value.trim();
|
|
79
|
+
return trimmed || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveExplicitModel(channel, model) {
|
|
83
|
+
return (
|
|
84
|
+
normalizeNonEmptyString(model)
|
|
85
|
+
|| normalizeNonEmptyString(channel?.model)
|
|
86
|
+
|| normalizeNonEmptyString(channel?.modelConfig?.model)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveEffectiveApiKey(channel, channelType) {
|
|
91
|
+
switch (channelType) {
|
|
92
|
+
case 'codex':
|
|
93
|
+
return getCodexEffectiveApiKey(channel);
|
|
94
|
+
case 'gemini':
|
|
95
|
+
return getGeminiEffectiveApiKey(channel);
|
|
96
|
+
case 'opencode':
|
|
97
|
+
return getOpenCodeEffectiveApiKey(channel);
|
|
98
|
+
case 'claude':
|
|
99
|
+
default:
|
|
100
|
+
return getClaudeEffectiveApiKey(channel);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mapStainlessOs() {
|
|
105
|
+
switch (process.platform) {
|
|
106
|
+
case 'darwin':
|
|
107
|
+
return 'MacOS';
|
|
108
|
+
case 'win32':
|
|
109
|
+
return 'Windows';
|
|
110
|
+
case 'linux':
|
|
111
|
+
return 'Linux';
|
|
112
|
+
default:
|
|
113
|
+
return `other::${process.platform}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mapStainlessArch() {
|
|
118
|
+
switch (process.arch) {
|
|
119
|
+
case 'x64':
|
|
120
|
+
return 'x64';
|
|
121
|
+
case 'arm64':
|
|
122
|
+
return 'arm64';
|
|
123
|
+
case 'ia32':
|
|
124
|
+
return 'x86';
|
|
125
|
+
default:
|
|
126
|
+
return `other::${process.arch}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildGeminiNativeGeneratePath(parsedUrl, model) {
|
|
131
|
+
let pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
132
|
+
const modelsIndex = pathname.indexOf('/models');
|
|
133
|
+
if (modelsIndex >= 0) {
|
|
134
|
+
pathname = pathname.slice(0, modelsIndex);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let apiBasePath;
|
|
138
|
+
if (!pathname || pathname === '/') {
|
|
139
|
+
apiBasePath = '/v1beta';
|
|
140
|
+
} else if (pathname.endsWith('/v1beta') || pathname.endsWith('/v1')) {
|
|
141
|
+
apiBasePath = pathname;
|
|
142
|
+
} else {
|
|
143
|
+
apiBasePath = `${pathname}/v1beta`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${apiBasePath}/models/${encodeURIComponent(model)}:generateContent`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildGeminiCliGeneratePath(parsedUrl) {
|
|
150
|
+
let pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
151
|
+
if (!pathname || pathname === '/') {
|
|
152
|
+
return '/v1internal:generateContent';
|
|
153
|
+
}
|
|
154
|
+
if (pathname.endsWith(':streamGenerateContent')) {
|
|
155
|
+
return pathname.replace(/:streamGenerateContent$/, ':generateContent');
|
|
156
|
+
}
|
|
157
|
+
if (pathname.endsWith(':generateContent')) {
|
|
158
|
+
return pathname;
|
|
159
|
+
}
|
|
160
|
+
if (pathname.endsWith('/v1internal')) {
|
|
161
|
+
return `${pathname}:generateContent`;
|
|
162
|
+
}
|
|
163
|
+
if (pathname.endsWith('/v1')) {
|
|
164
|
+
return '/v1internal:generateContent';
|
|
165
|
+
}
|
|
166
|
+
return `${pathname}/v1internal:generateContent`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildCodexResponsesPath(parsedUrl) {
|
|
170
|
+
let pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
171
|
+
if (!pathname || pathname === '/') {
|
|
172
|
+
return '/responses';
|
|
173
|
+
}
|
|
174
|
+
if (pathname.endsWith('/responses') || pathname.endsWith('/v1/responses')) {
|
|
175
|
+
return pathname;
|
|
176
|
+
}
|
|
177
|
+
if (pathname.endsWith('/v1')) {
|
|
178
|
+
return `${pathname}/responses`;
|
|
179
|
+
}
|
|
180
|
+
return `${pathname}/responses`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldUseGeminiCliFormat(parsedUrl) {
|
|
184
|
+
const host = String(parsedUrl.hostname || '').toLowerCase();
|
|
185
|
+
const pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
186
|
+
|
|
187
|
+
if (pathname.includes('/v1internal') || pathname.endsWith(':generateContent') || pathname.endsWith(':streamGenerateContent')) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (pathname.includes('/v1beta') || pathname.includes('/models/')) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (host.includes('cloudcode-pa.googleapis.com')) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (!pathname || pathname === '/') {
|
|
197
|
+
return !host.includes('generativelanguage.googleapis.com') && !host.includes('aiplatform.googleapis.com');
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractJsonPayloads(responseData) {
|
|
203
|
+
const payloads = [];
|
|
204
|
+
const text = typeof responseData === 'string' ? responseData : String(responseData || '');
|
|
205
|
+
if (!text.trim()) {
|
|
206
|
+
return payloads;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
payloads.push(JSON.parse(text));
|
|
211
|
+
} catch {
|
|
212
|
+
// ignore and continue parsing SSE fragments
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const lines = text.split(/\r?\n/);
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
219
|
+
const rawData = trimmed.slice(5).trim();
|
|
220
|
+
if (!rawData || rawData === '[DONE]') continue;
|
|
221
|
+
try {
|
|
222
|
+
payloads.push(JSON.parse(rawData));
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore invalid SSE fragment
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return payloads;
|
|
229
|
+
}
|
|
230
|
+
|
|
33
231
|
/**
|
|
34
232
|
* 测试单个渠道的连接速度和 API 功能
|
|
35
233
|
* @param {Object} channel - 渠道配置
|
|
@@ -74,9 +272,31 @@ async function testChannelSpeed(channel, timeout = DEFAULT_TIMEOUT, channelType
|
|
|
74
272
|
};
|
|
75
273
|
}
|
|
76
274
|
|
|
275
|
+
const effectiveApiKey = resolveEffectiveApiKey(channel, channelType);
|
|
276
|
+
if (!effectiveApiKey) {
|
|
277
|
+
return {
|
|
278
|
+
channelId: channel.id,
|
|
279
|
+
channelName: channel.name,
|
|
280
|
+
success: false,
|
|
281
|
+
networkOk: false,
|
|
282
|
+
apiOk: false,
|
|
283
|
+
error: 'API Key 未配置',
|
|
284
|
+
latency: null,
|
|
285
|
+
statusCode: null,
|
|
286
|
+
testedAt: Date.now()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
77
290
|
// 直接测试 API 功能(发送测试消息)
|
|
78
291
|
// 不再单独测试网络连通性,因为直接 GET base_url 可能返回 404
|
|
79
|
-
const apiResult = await testAPIFunctionality(
|
|
292
|
+
const apiResult = await testAPIFunctionality(
|
|
293
|
+
testUrl,
|
|
294
|
+
effectiveApiKey,
|
|
295
|
+
sanitizedTimeout,
|
|
296
|
+
channelType,
|
|
297
|
+
channel.model,
|
|
298
|
+
channel
|
|
299
|
+
);
|
|
80
300
|
|
|
81
301
|
const success = apiResult.success;
|
|
82
302
|
const networkOk = apiResult.latency !== null; // 如果有延迟数据,说明网络是通的
|
|
@@ -90,7 +310,7 @@ async function testChannelSpeed(channel, timeout = DEFAULT_TIMEOUT, channelType
|
|
|
90
310
|
apiOk: success,
|
|
91
311
|
statusCode: apiResult.statusCode || null,
|
|
92
312
|
error: success ? null : (apiResult.error || '测试失败'),
|
|
93
|
-
latency: apiResult.latency
|
|
313
|
+
latency: apiResult.latency ?? null, // 无论成功失败都保留延迟数据(保留 0ms)
|
|
94
314
|
testedAt: Date.now(),
|
|
95
315
|
testedModel: apiResult.testedModel,
|
|
96
316
|
availableModels: apiResult.availableModels,
|
|
@@ -199,220 +419,259 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
199
419
|
// Probe model availability if channel is provided
|
|
200
420
|
let modelProbe = null;
|
|
201
421
|
if (channel) {
|
|
202
|
-
|
|
203
|
-
|
|
422
|
+
const configuredSpeedTestModel = normalizeNonEmptyString(channel.speedTestModel);
|
|
423
|
+
const explicitModel = resolveExplicitModel(channel, model);
|
|
424
|
+
|
|
425
|
+
// 优先使用 speedTestModel,避免测速时额外探测
|
|
426
|
+
if (configuredSpeedTestModel) {
|
|
204
427
|
// Use the explicitly configured model for speed testing
|
|
205
428
|
modelProbe = {
|
|
206
|
-
preferredTestModel:
|
|
207
|
-
availableModels: [
|
|
208
|
-
cached: false
|
|
429
|
+
preferredTestModel: configuredSpeedTestModel,
|
|
430
|
+
availableModels: [configuredSpeedTestModel],
|
|
431
|
+
cached: false,
|
|
432
|
+
method: 'configured'
|
|
209
433
|
};
|
|
210
|
-
console.log(`[SpeedTest] Using configured speedTestModel: ${
|
|
434
|
+
console.log(`[SpeedTest] Using configured speedTestModel: ${configuredSpeedTestModel}`);
|
|
435
|
+
} else if (explicitModel) {
|
|
436
|
+
modelProbe = {
|
|
437
|
+
preferredTestModel: explicitModel,
|
|
438
|
+
availableModels: [explicitModel],
|
|
439
|
+
cached: false,
|
|
440
|
+
method: 'configured'
|
|
441
|
+
};
|
|
442
|
+
console.log(`[SpeedTest] Using explicit model: ${explicitModel}`);
|
|
211
443
|
} else {
|
|
212
444
|
// Fall back to auto-detection
|
|
213
445
|
try {
|
|
214
|
-
modelProbe = await probeModelAvailability(channel, channelType);
|
|
446
|
+
modelProbe = await probeModelAvailability(channel, channelType, { stopOnFirstAvailable: true });
|
|
215
447
|
} catch (error) {
|
|
216
448
|
console.error('[SpeedTest] Model detection failed:', error.message);
|
|
217
449
|
}
|
|
218
450
|
}
|
|
219
451
|
}
|
|
220
452
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
453
|
+
const parsedUrl = new URL(baseUrl);
|
|
454
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
455
|
+
const httpModule = isHttps ? https : http;
|
|
456
|
+
|
|
457
|
+
// 根据渠道类型确定 API 路径和请求格式
|
|
458
|
+
let testModel = null;
|
|
459
|
+
let primaryRequestConfig = null;
|
|
460
|
+
let fallbackRequestConfig = null;
|
|
461
|
+
|
|
462
|
+
// Helper to create result object with model info
|
|
463
|
+
const createResult = (result) => ({
|
|
464
|
+
...result,
|
|
465
|
+
testedModel: testModel,
|
|
466
|
+
availableModels: modelProbe?.availableModels,
|
|
467
|
+
modelDetectionMethod: modelProbe?.method || (modelProbe?.cached ? 'cached' : 'probed')
|
|
468
|
+
});
|
|
234
469
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
470
|
+
const parseErrorMessage = (responseData) => {
|
|
471
|
+
const payloads = extractJsonPayloads(responseData);
|
|
472
|
+
for (const payload of payloads) {
|
|
473
|
+
const message = payload?.error?.message || payload?.message || payload?.detail || payload?.error_description;
|
|
474
|
+
if (message) return message;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const UNEXPECTED_ERROR_PATTERNS = [
|
|
480
|
+
/unexpected/i,
|
|
481
|
+
/internal.*error/i,
|
|
482
|
+
/something.*went.*wrong/i,
|
|
483
|
+
/service.*unavailable/i,
|
|
484
|
+
/temporarily.*unavailable/i,
|
|
485
|
+
/try.*again.*later/i,
|
|
486
|
+
/server.*error/i,
|
|
487
|
+
/bad.*gateway/i,
|
|
488
|
+
/gateway.*timeout/i
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
function containsUnexpectedError(responseBody) {
|
|
492
|
+
const payloads = extractJsonPayloads(responseBody);
|
|
493
|
+
for (const payload of payloads) {
|
|
494
|
+
if (payload?.error) {
|
|
495
|
+
return { hasError: true, message: payload.error.message || payload.error };
|
|
496
|
+
}
|
|
497
|
+
const message = payload?.message || payload?.detail || payload?.error_description || '';
|
|
498
|
+
for (const pattern of UNEXPECTED_ERROR_PATTERNS) {
|
|
499
|
+
if (pattern.test(message)) {
|
|
500
|
+
return { hasError: true, message };
|
|
501
|
+
}
|
|
247
502
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
503
|
+
}
|
|
504
|
+
return { hasError: false };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (channelType === 'claude') {
|
|
508
|
+
// Anthropic Messages API - 模拟 Claude Code 请求格式
|
|
509
|
+
let apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
510
|
+
if (!apiPath.endsWith('/messages')) {
|
|
511
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/messages' : '/v1/messages');
|
|
512
|
+
}
|
|
513
|
+
apiPath += '?beta=true';
|
|
514
|
+
|
|
515
|
+
testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'claude-sonnet-4-20250514';
|
|
516
|
+
const sessionId = Math.random().toString(36).substring(2, 15);
|
|
517
|
+
primaryRequestConfig = {
|
|
518
|
+
apiPath,
|
|
519
|
+
requestBody: JSON.stringify({
|
|
257
520
|
model: testModel,
|
|
258
521
|
max_tokens: 1,
|
|
259
|
-
stream:
|
|
260
|
-
messages: [{ role: 'user', content: [{ type: 'text', text: '
|
|
522
|
+
stream: false,
|
|
523
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: 'ping' }] }],
|
|
261
524
|
system: [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }],
|
|
262
525
|
metadata: { user_id: `user_0000000000000000000000000000000000000000000000000000000000000000_account__session_${sessionId}` }
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
headers = {
|
|
526
|
+
}),
|
|
527
|
+
headers: {
|
|
266
528
|
'x-api-key': apiKey || '',
|
|
267
529
|
'Authorization': `Bearer ${apiKey || ''}`,
|
|
268
530
|
'anthropic-version': '2023-06-01',
|
|
269
|
-
'anthropic-beta':
|
|
531
|
+
'anthropic-beta': CLAUDE_CODE_BETA_HEADER,
|
|
270
532
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
271
533
|
'x-app': 'cli',
|
|
534
|
+
'x-stainless-helper-method': 'stream',
|
|
535
|
+
'x-stainless-retry-count': '0',
|
|
536
|
+
'x-stainless-runtime-version': 'v24.3.0',
|
|
537
|
+
'x-stainless-package-version': '0.74.0',
|
|
272
538
|
'x-stainless-lang': 'js',
|
|
273
539
|
'x-stainless-runtime': 'node',
|
|
540
|
+
'x-stainless-arch': mapStainlessArch(),
|
|
541
|
+
'x-stainless-os': mapStainlessOs(),
|
|
542
|
+
'x-stainless-timeout': '600',
|
|
543
|
+
'Accept': 'application/json',
|
|
544
|
+
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
545
|
+
'Connection': 'keep-alive',
|
|
274
546
|
'Content-Type': 'application/json',
|
|
275
|
-
'User-Agent': 'claude-cli/2.
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
console.error('[SpeedTest] Failed to load Codex template:', err.message);
|
|
297
|
-
// 降级使用简化版本(可能会失败)
|
|
298
|
-
testModel = modelProbe?.preferredTestModel || 'gpt-5-codex';
|
|
299
|
-
requestBody = JSON.stringify({
|
|
300
|
-
model: testModel,
|
|
301
|
-
instructions: 'You are Codex.',
|
|
302
|
-
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
|
|
303
|
-
max_output_tokens: 1,
|
|
304
|
-
stream: false,
|
|
305
|
-
store: false
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
headers = {
|
|
547
|
+
'User-Agent': 'claude-cli/2.1.44 (external, sdk-cli)'
|
|
548
|
+
},
|
|
549
|
+
isStreamingResponse: false
|
|
550
|
+
};
|
|
551
|
+
} else if (channelType === 'codex') {
|
|
552
|
+
const apiPath = buildCodexResponsesPath(parsedUrl);
|
|
553
|
+
testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gpt-5-codex';
|
|
554
|
+
const codexSessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
555
|
+
|
|
556
|
+
const baseBody = {
|
|
557
|
+
model: testModel,
|
|
558
|
+
instructions: 'You are Codex.',
|
|
559
|
+
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
|
|
560
|
+
store: false,
|
|
561
|
+
prompt_cache_key: codexSessionId
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
primaryRequestConfig = {
|
|
565
|
+
apiPath,
|
|
566
|
+
requestBody: JSON.stringify({ ...baseBody, stream: false }),
|
|
567
|
+
headers: {
|
|
309
568
|
'Authorization': `Bearer ${apiKey || ''}`,
|
|
569
|
+
'Accept': 'application/json',
|
|
570
|
+
'Connection': 'Keep-Alive',
|
|
571
|
+
'Version': '0.101.0',
|
|
572
|
+
'Session_id': codexSessionId,
|
|
573
|
+
'Originator': 'codex_cli_rs',
|
|
310
574
|
'Content-Type': 'application/json',
|
|
311
|
-
'User-Agent': 'codex_cli_rs/0.
|
|
575
|
+
'User-Agent': 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464',
|
|
312
576
|
'openai-beta': 'responses=experimental'
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
577
|
+
},
|
|
578
|
+
isStreamingResponse: false
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
fallbackRequestConfig = {
|
|
582
|
+
apiPath,
|
|
583
|
+
requestBody: JSON.stringify({ ...baseBody, stream: true }),
|
|
584
|
+
headers: {
|
|
585
|
+
...primaryRequestConfig.headers,
|
|
586
|
+
'Accept': 'text/event-stream'
|
|
587
|
+
},
|
|
588
|
+
isStreamingResponse: true
|
|
589
|
+
};
|
|
590
|
+
} else if (channelType === 'gemini') {
|
|
591
|
+
testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gemini-2.5-pro';
|
|
592
|
+
const useCliFormat = shouldUseGeminiCliFormat(parsedUrl);
|
|
593
|
+
|
|
594
|
+
const cliRequestConfig = {
|
|
595
|
+
apiPath: buildGeminiCliGeneratePath(parsedUrl),
|
|
596
|
+
requestBody: JSON.stringify({
|
|
597
|
+
project: '',
|
|
323
598
|
model: testModel,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
599
|
+
request: {
|
|
600
|
+
contents: [{ role: 'user', parts: [{ text: 'ping' }] }],
|
|
601
|
+
generationConfig: { maxOutputTokens: 1, temperature: 0 }
|
|
602
|
+
}
|
|
603
|
+
}),
|
|
604
|
+
headers: {
|
|
328
605
|
'Authorization': `Bearer ${apiKey || ''}`,
|
|
606
|
+
'x-goog-api-key': apiKey || '',
|
|
607
|
+
'Accept': 'application/json',
|
|
329
608
|
'Content-Type': 'application/json',
|
|
330
|
-
'User-Agent': '
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
609
|
+
'User-Agent': 'google-api-nodejs-client/9.15.1',
|
|
610
|
+
'X-Goog-Api-Client': 'gl-node/22.17.0',
|
|
611
|
+
'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI'
|
|
612
|
+
},
|
|
613
|
+
isStreamingResponse: false
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const nativeRequestConfig = {
|
|
617
|
+
apiPath: buildGeminiNativeGeneratePath(parsedUrl, testModel),
|
|
618
|
+
requestBody: JSON.stringify({
|
|
619
|
+
contents: [{ role: 'user', parts: [{ text: 'ping' }] }],
|
|
620
|
+
generationConfig: { maxOutputTokens: 1, temperature: 0 }
|
|
621
|
+
}),
|
|
622
|
+
headers: {
|
|
623
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
624
|
+
'x-goog-api-key': apiKey || '',
|
|
625
|
+
'Accept': 'application/json',
|
|
626
|
+
'Content-Type': 'application/json',
|
|
627
|
+
'User-Agent': 'google-genai-sdk/0.8.0'
|
|
628
|
+
},
|
|
629
|
+
isStreamingResponse: false
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
primaryRequestConfig = useCliFormat ? cliRequestConfig : nativeRequestConfig;
|
|
633
|
+
fallbackRequestConfig = useCliFormat ? nativeRequestConfig : cliRequestConfig;
|
|
634
|
+
} else {
|
|
635
|
+
let apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
636
|
+
if (!apiPath.endsWith('/chat/completions')) {
|
|
637
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
|
|
638
|
+
}
|
|
639
|
+
primaryRequestConfig = {
|
|
640
|
+
apiPath,
|
|
641
|
+
requestBody: JSON.stringify({
|
|
339
642
|
model: 'gpt-4o-mini',
|
|
340
643
|
max_tokens: 1,
|
|
341
644
|
messages: [{ role: 'user', content: 'Hi' }]
|
|
342
|
-
})
|
|
343
|
-
headers
|
|
645
|
+
}),
|
|
646
|
+
headers: {
|
|
344
647
|
'Authorization': `Bearer ${apiKey || ''}`,
|
|
345
648
|
'Content-Type': 'application/json',
|
|
346
649
|
'User-Agent': 'Coding-Tool-SpeedTest/1.0'
|
|
347
|
-
}
|
|
348
|
-
|
|
650
|
+
},
|
|
651
|
+
isStreamingResponse: false
|
|
652
|
+
};
|
|
653
|
+
}
|
|
349
654
|
|
|
655
|
+
const executeRequest = (requestConfig) => new Promise((resolve) => {
|
|
656
|
+
const startTime = Date.now();
|
|
350
657
|
const options = {
|
|
351
658
|
hostname: parsedUrl.hostname,
|
|
352
659
|
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
353
|
-
path: apiPath,
|
|
660
|
+
path: requestConfig.apiPath,
|
|
354
661
|
method: 'POST',
|
|
355
662
|
timeout,
|
|
356
|
-
headers
|
|
663
|
+
headers: requestConfig.headers
|
|
357
664
|
};
|
|
358
665
|
|
|
359
666
|
const req = httpModule.request(options, (res) => {
|
|
360
667
|
let data = '';
|
|
361
668
|
let resolved = false;
|
|
362
|
-
const isStreamingResponse = channelType === 'codex'; // Codex 使用流式响应
|
|
363
|
-
|
|
364
|
-
// 解析响应体中的错误信息
|
|
365
|
-
const parseErrorMessage = (responseData) => {
|
|
366
|
-
try {
|
|
367
|
-
const errData = JSON.parse(responseData);
|
|
368
|
-
return errData.error?.message || errData.message || errData.detail || null;
|
|
369
|
-
} catch {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const UNEXPECTED_ERROR_PATTERNS = [
|
|
375
|
-
/unexpected/i,
|
|
376
|
-
/internal.*error/i,
|
|
377
|
-
/something.*went.*wrong/i,
|
|
378
|
-
/service.*unavailable/i,
|
|
379
|
-
/temporarily.*unavailable/i,
|
|
380
|
-
/try.*again.*later/i,
|
|
381
|
-
/server.*error/i,
|
|
382
|
-
/bad.*gateway/i,
|
|
383
|
-
/gateway.*timeout/i
|
|
384
|
-
];
|
|
385
|
-
|
|
386
|
-
function containsUnexpectedError(responseBody) {
|
|
387
|
-
try {
|
|
388
|
-
const data = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody;
|
|
389
|
-
|
|
390
|
-
// Check for explicit error field
|
|
391
|
-
if (data.error) {
|
|
392
|
-
return { hasError: true, message: data.error.message || data.error };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Check message patterns
|
|
396
|
-
const message = data.message || data.detail || data.error_description || '';
|
|
397
|
-
for (const pattern of UNEXPECTED_ERROR_PATTERNS) {
|
|
398
|
-
if (pattern.test(message)) {
|
|
399
|
-
return { hasError: true, message };
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return { hasError: false };
|
|
404
|
-
} catch {
|
|
405
|
-
return { hasError: false };
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
669
|
|
|
409
670
|
res.on('data', chunk => {
|
|
410
671
|
data += chunk;
|
|
411
672
|
const chunkStr = chunk.toString();
|
|
412
673
|
|
|
413
|
-
|
|
414
|
-
if (isStreamingResponse && !resolved && res.statusCode >= 200 && res.statusCode < 300) {
|
|
415
|
-
// 检查是否收到了 response.created 或 response.in_progress 事件
|
|
674
|
+
if (requestConfig.isStreamingResponse && !resolved && res.statusCode >= 200 && res.statusCode < 300) {
|
|
416
675
|
if (chunkStr.includes('response.created') || chunkStr.includes('response.in_progress')) {
|
|
417
676
|
resolved = true;
|
|
418
677
|
const latency = Date.now() - startTime;
|
|
@@ -423,8 +682,7 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
423
682
|
error: null,
|
|
424
683
|
statusCode: res.statusCode
|
|
425
684
|
}));
|
|
426
|
-
} else if (chunkStr.includes('"detail"') || chunkStr.includes('"error"')) {
|
|
427
|
-
// 流式响应中的错误 - 使用新的错误检测函数
|
|
685
|
+
} else if (chunkStr.includes('"detail"') || chunkStr.includes('"error"') || chunkStr.includes('data:')) {
|
|
428
686
|
const errorCheck = containsUnexpectedError(chunkStr);
|
|
429
687
|
if (errorCheck.hasError) {
|
|
430
688
|
resolved = true;
|
|
@@ -442,13 +700,10 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
442
700
|
});
|
|
443
701
|
|
|
444
702
|
res.on('end', () => {
|
|
445
|
-
if (resolved) return;
|
|
703
|
+
if (resolved) return;
|
|
446
704
|
|
|
447
705
|
const latency = Date.now() - startTime;
|
|
448
|
-
|
|
449
|
-
// 严格判断:只有 2xx 且没有错误信息才算成功
|
|
450
706
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
451
|
-
// 使用新的错误检测函数
|
|
452
707
|
const errorCheck = containsUnexpectedError(data);
|
|
453
708
|
if (errorCheck.hasError) {
|
|
454
709
|
resolve(createResult({
|
|
@@ -458,7 +713,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
458
713
|
statusCode: res.statusCode
|
|
459
714
|
}));
|
|
460
715
|
} else {
|
|
461
|
-
// 真正的成功响应
|
|
462
716
|
resolve(createResult({
|
|
463
717
|
success: true,
|
|
464
718
|
latency,
|
|
@@ -481,7 +735,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
481
735
|
statusCode: res.statusCode
|
|
482
736
|
}));
|
|
483
737
|
} else if (res.statusCode === 429) {
|
|
484
|
-
// 请求过多 - 标记为失败
|
|
485
738
|
const errMsg = parseErrorMessage(data) || '请求过多,服务限流中';
|
|
486
739
|
resolve(createResult({
|
|
487
740
|
success: false,
|
|
@@ -490,7 +743,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
490
743
|
statusCode: res.statusCode
|
|
491
744
|
}));
|
|
492
745
|
} else if (res.statusCode === 503 || res.statusCode === 529) {
|
|
493
|
-
// 服务暂时不可用/过载 - 标记为失败
|
|
494
746
|
const errMsg = parseErrorMessage(data) || (res.statusCode === 503 ? '服务暂时不可用' : '服务过载');
|
|
495
747
|
resolve(createResult({
|
|
496
748
|
success: false,
|
|
@@ -506,7 +758,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
506
758
|
statusCode: res.statusCode
|
|
507
759
|
}));
|
|
508
760
|
} else if (res.statusCode === 400) {
|
|
509
|
-
// 请求参数错误
|
|
510
761
|
const errMsg = parseErrorMessage(data) || '请求参数错误';
|
|
511
762
|
resolve(createResult({
|
|
512
763
|
success: false,
|
|
@@ -515,7 +766,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
515
766
|
statusCode: res.statusCode
|
|
516
767
|
}));
|
|
517
768
|
} else if (res.statusCode >= 500) {
|
|
518
|
-
// 5xx 服务器错误
|
|
519
769
|
const errMsg = parseErrorMessage(data) || `服务器错误 (${res.statusCode})`;
|
|
520
770
|
resolve(createResult({
|
|
521
771
|
success: false,
|
|
@@ -524,7 +774,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
524
774
|
statusCode: res.statusCode
|
|
525
775
|
}));
|
|
526
776
|
} else {
|
|
527
|
-
// 其他错误
|
|
528
777
|
const errMsg = parseErrorMessage(data) || `HTTP ${res.statusCode}`;
|
|
529
778
|
resolve(createResult({
|
|
530
779
|
success: false,
|
|
@@ -553,9 +802,29 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
553
802
|
}));
|
|
554
803
|
});
|
|
555
804
|
|
|
556
|
-
req.write(requestBody);
|
|
805
|
+
req.write(requestConfig.requestBody);
|
|
557
806
|
req.end();
|
|
558
807
|
});
|
|
808
|
+
|
|
809
|
+
const primaryResult = await executeRequest(primaryRequestConfig);
|
|
810
|
+
if (primaryResult.success || !fallbackRequestConfig) {
|
|
811
|
+
return primaryResult;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (channelType === 'gemini' && ROUTE_OR_METHOD_MISMATCH_STATUS.has(primaryResult.statusCode)) {
|
|
815
|
+
return executeRequest(fallbackRequestConfig);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (channelType === 'codex') {
|
|
819
|
+
const codexError = String(primaryResult.error || '').toLowerCase();
|
|
820
|
+
const shouldRetryWithStreaming = ROUTE_OR_METHOD_MISMATCH_STATUS.has(primaryResult.statusCode)
|
|
821
|
+
|| (primaryResult.statusCode === 400 && (codexError.includes('stream') || codexError.includes('event-stream') || codexError.includes('sse')));
|
|
822
|
+
if (shouldRetryWithStreaming) {
|
|
823
|
+
return executeRequest(fallbackRequestConfig);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return primaryResult;
|
|
559
828
|
}
|
|
560
829
|
|
|
561
830
|
/**
|
|
@@ -565,16 +834,22 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
565
834
|
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
566
835
|
* @returns {Promise<Array>} 测试结果列表
|
|
567
836
|
*/
|
|
568
|
-
async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude') {
|
|
569
|
-
const results = await
|
|
570
|
-
channels
|
|
837
|
+
async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude', concurrency = 2) {
|
|
838
|
+
const results = await runWithConcurrencyLimit(
|
|
839
|
+
channels,
|
|
840
|
+
concurrency,
|
|
841
|
+
channel => testChannelSpeed(channel, timeout, channelType)
|
|
571
842
|
);
|
|
572
843
|
|
|
573
844
|
// 按延迟排序(成功的在前,按延迟升序)
|
|
574
845
|
results.sort((a, b) => {
|
|
575
846
|
if (a.success && !b.success) return -1;
|
|
576
847
|
if (!a.success && b.success) return 1;
|
|
577
|
-
if (a.success && b.success)
|
|
848
|
+
if (a.success && b.success) {
|
|
849
|
+
const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
|
|
850
|
+
const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
|
|
851
|
+
return aLatency - bLatency;
|
|
852
|
+
}
|
|
578
853
|
return 0;
|
|
579
854
|
});
|
|
580
855
|
|
|
@@ -608,7 +883,8 @@ function clearCache() {
|
|
|
608
883
|
* @returns {string} 等级:excellent/good/fair/poor
|
|
609
884
|
*/
|
|
610
885
|
function getLatencyLevel(latency) {
|
|
611
|
-
if (
|
|
886
|
+
if (latency === null || latency === undefined) return 'unknown';
|
|
887
|
+
if (!Number.isFinite(Number(latency))) return 'unknown';
|
|
612
888
|
if (latency < 300) return 'excellent'; // < 300ms 优秀
|
|
613
889
|
if (latency < 500) return 'good'; // < 500ms 良好
|
|
614
890
|
if (latency < 800) return 'fair'; // < 800ms 一般
|
|
@@ -620,5 +896,7 @@ module.exports = {
|
|
|
620
896
|
testMultipleChannels,
|
|
621
897
|
getCachedResult,
|
|
622
898
|
clearCache,
|
|
623
|
-
getLatencyLevel
|
|
899
|
+
getLatencyLevel,
|
|
900
|
+
sanitizeBatchConcurrency,
|
|
901
|
+
runWithConcurrencyLimit
|
|
624
902
|
};
|