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 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 a latency score using first SSE event, total completion time, and health-check time. 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.
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
- 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) {
@@ -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
- return scoreLatency(a) - scoreLatency(b);
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 scoreLatency(result) {
60
- const firstEventMs = Number.isFinite(result.firstEventMs) ? result.firstEventMs : 999999;
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
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.4';
2
+ export const PACKAGE_VERSION = '0.1.5';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {