gpteam 0.1.3 → 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 CHANGED
@@ -8,6 +8,8 @@ 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 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
+
11
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.
12
14
 
13
15
  Supported clients:
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;
@@ -13,7 +23,7 @@ export async function benchmarkNodes(nodes, options) {
13
23
  }
14
24
  return summarizeNode(node, samples);
15
25
  }));
16
- return results.sort((a, b) => scoreResult(a) - scoreResult(b));
26
+ return results.sort(compareResults);
17
27
  }
18
28
 
19
29
  export async function benchmarkNode(node, options) {
@@ -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
- return {
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(streams.map((item) => item.firstEventMs).filter(Number.isFinite)),
38
- totalMs: median(streams.map((item) => item.totalMs).filter(Number.isFinite)),
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) {
@@ -50,9 +66,32 @@ export function formatMs(value) {
50
66
  return `${(value / 1000).toFixed(2)}s`;
51
67
  }
52
68
 
69
+ function compareResults(a, b) {
70
+ const successDelta = b.successRate - a.successRate;
71
+ if (successDelta !== 0) return successDelta;
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
+
53
77
  function scoreResult(result) {
54
- if (!result.successRate) return Number.MAX_SAFE_INTEGER;
55
- return (result.firstEventMs || 999999) + (result.totalMs || 999999) - result.successRate * 1000;
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;
91
+ }
92
+
93
+ function finiteOrFallback(value, fallback = MISSING_LATENCY_MS) {
94
+ return Number.isFinite(value) ? value : fallback;
56
95
  }
57
96
 
58
97
  async function measureHealth(url) {
@@ -160,3 +199,10 @@ function median(values) {
160
199
  const middle = Math.floor(sorted.length / 2);
161
200
  return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
162
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,6 +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
39
  console.log(`模型:${model.id},测速输出上限:${maxOutputTokens}`);
39
40
  const results = await benchmarkNodes(INGRESS_NODES, {
40
41
  apiKey,
@@ -96,12 +97,14 @@ export function printResults(results) {
96
97
  formatMs(item.tlsMs),
97
98
  formatMs(item.firstEventMs),
98
99
  formatMs(item.totalMs),
100
+ formatMs(item.tailTotalMs),
99
101
  `${Math.round(item.successRate * 100)}%`,
102
+ formatScore(item.experienceScore),
100
103
  formatMs(item.healthMs),
101
104
  item === recommended ? '推荐' : '-',
102
105
  item.error || '-'
103
106
  ]);
104
- const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '成功率', '健康检查', '推荐', '错误'];
107
+ const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '尾延迟', '成功率', '体验分', '健康检查', '推荐', '错误'];
105
108
  const widths = header.map((name, i) => Math.max(displayWidth(name), ...rows.map((row) => displayWidth(row[i]))) + 2);
106
109
  console.log('');
107
110
  console.log(formatRow(header, widths));
@@ -123,7 +126,7 @@ async function chooseModel(rl, models, preferred) {
123
126
  async function askContextLength(rl, model, preferred) {
124
127
  const max = Number(model.contextLength || 400000);
125
128
  if (preferred) return clamp(Number(preferred), 1, max);
126
- const answer = await rl.question(`请输入上下文窗口(最大 ${max},输出上限 ${model.maxOutputTokens},默认 ${max},回车即选择默认):`);
129
+ const answer = await rl.question(`请输入可配置上下文长度(最大 ${max},输出上限 ${model.maxOutputTokens},默认 ${max},回车即选择默认):`);
127
130
  return clamp(Number(answer || max), 1, max);
128
131
  }
129
132
 
@@ -172,6 +175,10 @@ function displayWidth(value) {
172
175
  return Array.from(String(value)).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 2 : 1), 0);
173
176
  }
174
177
 
178
+ function formatScore(value) {
179
+ return Number.isFinite(value) ? String(Math.round(value)) : '-';
180
+ }
181
+
175
182
  function clamp(value, min, max) {
176
183
  if (!Number.isFinite(value)) return max;
177
184
  return Math.max(min, Math.min(max, Math.floor(value)));
@@ -180,7 +187,7 @@ function clamp(value, min, max) {
180
187
  export function formatModelLabel(model) {
181
188
  const context = Number(model.contextLength || 0);
182
189
  const outputTokens = Number(model.maxOutputTokens || 0);
183
- return `${model.id}(上下文窗口 ${context},输出上限 ${outputTokens})`;
190
+ return `${model.id}(可配置上下文 ${context},输出上限 ${outputTokens})`;
184
191
  }
185
192
 
186
193
  export function formatNodeLabel(node) {
package/lib/help.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.3';
2
+ export const PACKAGE_VERSION = '0.1.5';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
@@ -13,7 +13,7 @@ export function getHelpText() {
13
13
  ' --api-key <key> 预填 API key',
14
14
  ' --client <id> codex / opencode / claude-code / openclaw',
15
15
  ' --model <id> 预选模型,例如 gpt-5.5',
16
- ' --context <tokens> 预设上下文窗口',
16
+ ' --context <tokens> 预设可配置上下文长度',
17
17
  ' --effort <level> 按模型支持项预选,常见为 none / low / medium / high / xhigh',
18
18
  ' --node <id> jp-direct / jp-split / hk-split / us-split',
19
19
  ' --rounds <n> 每个入口测速轮数,默认 3',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {