gpteam 0.1.4 → 0.1.5
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 +1 -1
- package/lib/bench.js +47 -9
- package/lib/cli.js +8 -2
- package/lib/help.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ npx gpteam
|
|
|
8
8
|
|
|
9
9
|
The CLI asks for an API key, validates it with `/v1/models`, detects available models, benchmarks all production ingress endpoints with real API requests, then backs up old files and writes the selected client configuration. The next steps are blocked until the key validation succeeds. Ingress endpoints are benchmarked in parallel, while rounds for the same endpoint remain sequential to keep real API request pressure bounded. Failed benchmark rows show their error reason in the result table.
|
|
10
10
|
|
|
11
|
-
Recommendation order is deterministic: success rate first, then
|
|
11
|
+
Recommendation order is deterministic: success rate first, then an experience score. The score weighs first SSE event time, total completion time, p90 completion tail latency, and health-check time, so a node with one very slow probe is not recommended just because its median result looks good. Model prompts use "configurable context" wording because the value is written to the client-side Codex `model_context_window`; it must not be confused with a public marketing total-window label.
|
|
12
12
|
|
|
13
13
|
Codex config writing follows the same safety pattern as cc-switch: keep top-level fields separate from provider tables, preserve unrelated sections such as MCP servers, and stop before writing if the generated TOML would contain duplicate keys.
|
|
14
14
|
|
package/lib/bench.js
CHANGED
|
@@ -3,6 +3,16 @@ import https from 'node:https';
|
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { inspectSSEBody } from './sse.js';
|
|
5
5
|
|
|
6
|
+
const MISSING_LATENCY_MS = 999999;
|
|
7
|
+
const SCORE_WEIGHTS = {
|
|
8
|
+
firstEvent: 0.55,
|
|
9
|
+
total: 0.30,
|
|
10
|
+
health: 0.05,
|
|
11
|
+
tailFirstEventPenalty: 0.25,
|
|
12
|
+
tailTotalPenalty: 0.20,
|
|
13
|
+
failurePenalty: 4
|
|
14
|
+
};
|
|
15
|
+
|
|
6
16
|
export async function benchmarkNodes(nodes, options) {
|
|
7
17
|
const rounds = options.rounds || 3;
|
|
8
18
|
const runBenchmark = options.benchmarkNode || benchmarkNode;
|
|
@@ -30,18 +40,24 @@ export async function benchmarkNode(node, options) {
|
|
|
30
40
|
export function summarizeNode(node, samples) {
|
|
31
41
|
const successful = samples.filter((sample) => sample.ok);
|
|
32
42
|
const streams = successful.map((sample) => sample.stream);
|
|
33
|
-
|
|
43
|
+
const firstEvents = streams.map((item) => item.firstEventMs).filter(Number.isFinite);
|
|
44
|
+
const totals = streams.map((item) => item.totalMs).filter(Number.isFinite);
|
|
45
|
+
const summary = {
|
|
34
46
|
node,
|
|
35
47
|
samples,
|
|
36
48
|
successRate: samples.length ? successful.length / samples.length : 0,
|
|
37
|
-
firstEventMs: median(
|
|
38
|
-
totalMs: median(
|
|
49
|
+
firstEventMs: median(firstEvents),
|
|
50
|
+
totalMs: median(totals),
|
|
51
|
+
tailFirstEventMs: percentile(firstEvents, 0.9),
|
|
52
|
+
tailTotalMs: percentile(totals, 0.9),
|
|
39
53
|
dnsMs: median(streams.map((item) => item.dnsMs).filter(Number.isFinite)),
|
|
40
54
|
tcpMs: median(streams.map((item) => item.tcpMs).filter(Number.isFinite)),
|
|
41
55
|
tlsMs: median(streams.map((item) => item.tlsMs).filter(Number.isFinite)),
|
|
42
56
|
healthMs: median(successful.map((sample) => sample.health.totalMs).filter(Number.isFinite)),
|
|
43
57
|
error: samples.find((sample) => sample.error)?.error || ''
|
|
44
58
|
};
|
|
59
|
+
summary.experienceScore = scoreResult(summary);
|
|
60
|
+
return summary;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
export function formatMs(value) {
|
|
@@ -53,14 +69,29 @@ export function formatMs(value) {
|
|
|
53
69
|
function compareResults(a, b) {
|
|
54
70
|
const successDelta = b.successRate - a.successRate;
|
|
55
71
|
if (successDelta !== 0) return successDelta;
|
|
56
|
-
|
|
72
|
+
const scoreDelta = a.experienceScore - b.experienceScore;
|
|
73
|
+
if (scoreDelta !== 0) return scoreDelta;
|
|
74
|
+
return String(a.node.id || a.node.label).localeCompare(String(b.node.id || b.node.label));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function scoreResult(result) {
|
|
78
|
+
if (!result.successRate) return Number.POSITIVE_INFINITY;
|
|
79
|
+
const firstEventMs = finiteOrFallback(result.firstEventMs);
|
|
80
|
+
const totalMs = finiteOrFallback(result.totalMs);
|
|
81
|
+
const healthMs = finiteOrFallback(result.healthMs);
|
|
82
|
+
const tailFirstEventMs = finiteOrFallback(result.tailFirstEventMs, firstEventMs);
|
|
83
|
+
const tailTotalMs = finiteOrFallback(result.tailTotalMs, totalMs);
|
|
84
|
+
const typicalLatency = firstEventMs * SCORE_WEIGHTS.firstEvent
|
|
85
|
+
+ totalMs * SCORE_WEIGHTS.total
|
|
86
|
+
+ healthMs * SCORE_WEIGHTS.health;
|
|
87
|
+
const tailPenalty = Math.max(0, tailFirstEventMs - firstEventMs) * SCORE_WEIGHTS.tailFirstEventPenalty
|
|
88
|
+
+ Math.max(0, tailTotalMs - totalMs) * SCORE_WEIGHTS.tailTotalPenalty;
|
|
89
|
+
const failurePenalty = 1 + Math.max(0, 1 - result.successRate) * SCORE_WEIGHTS.failurePenalty;
|
|
90
|
+
return (typicalLatency + tailPenalty) * failurePenalty;
|
|
57
91
|
}
|
|
58
92
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
const totalMs = Number.isFinite(result.totalMs) ? result.totalMs : 999999;
|
|
62
|
-
const healthMs = Number.isFinite(result.healthMs) ? result.healthMs : 999999;
|
|
63
|
-
return firstEventMs * 0.6 + totalMs * 0.35 + healthMs * 0.05;
|
|
93
|
+
function finiteOrFallback(value, fallback = MISSING_LATENCY_MS) {
|
|
94
|
+
return Number.isFinite(value) ? value : fallback;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
async function measureHealth(url) {
|
|
@@ -168,3 +199,10 @@ function median(values) {
|
|
|
168
199
|
const middle = Math.floor(sorted.length / 2);
|
|
169
200
|
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
170
201
|
}
|
|
202
|
+
|
|
203
|
+
function percentile(values, ratio) {
|
|
204
|
+
if (!values.length) return NaN;
|
|
205
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
206
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
|
|
207
|
+
return sorted[index];
|
|
208
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function runCli(argv = []) {
|
|
|
35
35
|
|
|
36
36
|
console.log('\n开始真实测速:GET /api/health + POST /v1/responses stream=true');
|
|
37
37
|
console.log('测速会按入口并行执行,每个入口内部仍按轮次顺序执行,避免同时打出过多真实请求。');
|
|
38
|
-
console.log('
|
|
38
|
+
console.log('推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好。');
|
|
39
39
|
console.log(`模型:${model.id},测速输出上限:${maxOutputTokens}`);
|
|
40
40
|
const results = await benchmarkNodes(INGRESS_NODES, {
|
|
41
41
|
apiKey,
|
|
@@ -97,12 +97,14 @@ export function printResults(results) {
|
|
|
97
97
|
formatMs(item.tlsMs),
|
|
98
98
|
formatMs(item.firstEventMs),
|
|
99
99
|
formatMs(item.totalMs),
|
|
100
|
+
formatMs(item.tailTotalMs),
|
|
100
101
|
`${Math.round(item.successRate * 100)}%`,
|
|
102
|
+
formatScore(item.experienceScore),
|
|
101
103
|
formatMs(item.healthMs),
|
|
102
104
|
item === recommended ? '推荐' : '-',
|
|
103
105
|
item.error || '-'
|
|
104
106
|
]);
|
|
105
|
-
const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '成功率', '健康检查', '推荐', '错误'];
|
|
107
|
+
const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '尾延迟', '成功率', '体验分', '健康检查', '推荐', '错误'];
|
|
106
108
|
const widths = header.map((name, i) => Math.max(displayWidth(name), ...rows.map((row) => displayWidth(row[i]))) + 2);
|
|
107
109
|
console.log('');
|
|
108
110
|
console.log(formatRow(header, widths));
|
|
@@ -173,6 +175,10 @@ function displayWidth(value) {
|
|
|
173
175
|
return Array.from(String(value)).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 2 : 1), 0);
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
function formatScore(value) {
|
|
179
|
+
return Number.isFinite(value) ? String(Math.round(value)) : '-';
|
|
180
|
+
}
|
|
181
|
+
|
|
176
182
|
function clamp(value, min, max) {
|
|
177
183
|
if (!Number.isFinite(value)) return max;
|
|
178
184
|
return Math.max(min, Math.min(max, Math.floor(value)));
|
package/lib/help.js
CHANGED