gpteam 0.1.27 → 0.1.28
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 +3 -1
- package/lib/bench.js +44 -8
- package/lib/cli.js +6 -5
- package/lib/help.js +2 -2
- package/lib/nodes.js +11 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ When a selected client is missing, the interactive CLI shows the exact install c
|
|
|
12
12
|
|
|
13
13
|
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. The result table also marks the fastest full completion separately from the balanced recommendation, because deep or long-output model runs may care more about full completion time than first-token responsiveness. 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.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Production endpoints are shown as the main, Japan, and Hong Kong ingress domains. They are all GPTeam production API entry points backed by the current new backend stack; the CLI no longer describes them as split-routing or secondary-worker nodes. The current node ids are `main`, `jp`, and `hk`. Older scripted values `jp-direct`, `jp-split`, and `hk-split` remain accepted as compatibility aliases.
|
|
16
|
+
|
|
17
|
+
Client config writing follows the same safety pattern as cc-switch: keep Codex top-level fields separate from provider tables, preserve unrelated sections such as MCP servers, merge OpenCode/OpenClaw providers additively, and stop before writing when an existing JSON/JSON5 config cannot be parsed. GPTeam keeps three proxy-specific extensions on top of that baseline: Codex enables WebSocket capability for the GPTeam provider, Codex/OpenCode/Claude Code write the `gpteam_image` MCP server so Image 2 can be called from chat, and Claude Code writes the reasoning-effort header through `ANTHROPIC_CUSTOM_HEADERS`.
|
|
16
18
|
|
|
17
19
|
The Image MCP config uses cc-switch-style per-client env blocks. Codex writes `[mcp_servers.gpteam_image.env]`, OpenCode writes `mcp.gpteam_image.environment`, and Claude Code writes `mcpServers.gpteam_image.env` in `~/.claude.json`. The MCP receives `GPTEAM_API_KEY` and `GPTEAM_BASE_URL` from that MCP config, so it does not depend on Codex `auth.json` or inherited `OPENAI_API_KEY`.
|
|
18
20
|
|
package/lib/bench.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function benchmarkNode(node, options) {
|
|
|
41
41
|
export function summarizeNode(node, samples) {
|
|
42
42
|
const successful = samples.filter((sample) => sample.ok);
|
|
43
43
|
const streams = successful.map((sample) => sample.stream);
|
|
44
|
+
const allStreams = samples.map((sample) => sample.stream).filter(Boolean);
|
|
44
45
|
const firstEvents = streams.map((item) => item.firstEventMs).filter(Number.isFinite);
|
|
45
46
|
const totals = streams.map((item) => item.totalMs).filter(Number.isFinite);
|
|
46
47
|
const summary = {
|
|
@@ -51,10 +52,10 @@ export function summarizeNode(node, samples) {
|
|
|
51
52
|
totalMs: median(totals),
|
|
52
53
|
tailFirstEventMs: percentile(firstEvents, 0.9),
|
|
53
54
|
tailTotalMs: percentile(totals, 0.9),
|
|
54
|
-
dnsMs: median(
|
|
55
|
-
tcpMs: median(
|
|
56
|
-
tlsMs: median(
|
|
57
|
-
healthMs: median(
|
|
55
|
+
dnsMs: median(allStreams.map((item) => item.dnsMs).filter(Number.isFinite)),
|
|
56
|
+
tcpMs: median(allStreams.map((item) => item.tcpMs).filter(Number.isFinite)),
|
|
57
|
+
tlsMs: median(allStreams.map((item) => item.tlsMs).filter(Number.isFinite)),
|
|
58
|
+
healthMs: median(samples.map((sample) => sample.health?.totalMs).filter(Number.isFinite)),
|
|
58
59
|
error: samples.find((sample) => sample.error)?.error || ''
|
|
59
60
|
};
|
|
60
61
|
summary.experienceScore = scoreResult(summary);
|
|
@@ -157,16 +158,17 @@ function measureStream(baseUrl, options) {
|
|
|
157
158
|
});
|
|
158
159
|
response.on('end', () => {
|
|
159
160
|
const semantic = inspectSSEBody(body);
|
|
160
|
-
const
|
|
161
|
-
|
|
161
|
+
const status = response.statusCode || 0;
|
|
162
|
+
const ok = status >= 200
|
|
163
|
+
&& status < 300
|
|
162
164
|
&& semantic.ok
|
|
163
165
|
&& Number.isFinite(timings.firstEventMs);
|
|
164
166
|
resolve({
|
|
165
167
|
ok,
|
|
166
|
-
status
|
|
168
|
+
status,
|
|
167
169
|
...timings,
|
|
168
170
|
totalMs: performance.now() - started,
|
|
169
|
-
error: ok ? '' :
|
|
171
|
+
error: ok ? '' : formatStreamProbeError(status, response.headers, body, semantic.error)
|
|
170
172
|
});
|
|
171
173
|
});
|
|
172
174
|
});
|
|
@@ -194,6 +196,40 @@ function measureStream(baseUrl, options) {
|
|
|
194
196
|
});
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
export function formatStreamProbeError(status, headers, body, semanticError) {
|
|
200
|
+
const statusCode = Number(status || 0);
|
|
201
|
+
const contentType = String(headers?.['content-type'] || headers?.['Content-Type'] || '').trim();
|
|
202
|
+
const detail = responseBodyDetail(body);
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
205
|
+
parts.push(`stream HTTP ${statusCode}`);
|
|
206
|
+
} else if (semanticError === 'stream empty response' && detail) {
|
|
207
|
+
parts.push('stream non-SSE response');
|
|
208
|
+
} else {
|
|
209
|
+
parts.push(semanticError || `stream HTTP ${statusCode}`);
|
|
210
|
+
}
|
|
211
|
+
if (contentType) parts.push(`content-type ${contentType}`);
|
|
212
|
+
if (detail) parts.push(detail);
|
|
213
|
+
return parts.join(';');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function responseBodyDetail(body) {
|
|
217
|
+
const text = String(body || '').replace(/\s+/g, ' ').trim();
|
|
218
|
+
if (!text) return '';
|
|
219
|
+
const parsed = parseJSON(text);
|
|
220
|
+
const message = parsed?.error?.message || parsed?.message || parsed?.error;
|
|
221
|
+
if (message) return `body ${String(message).slice(0, 180)}`;
|
|
222
|
+
return `body ${text.slice(0, 180)}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseJSON(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(value);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
197
233
|
function median(values) {
|
|
198
234
|
if (!values.length) return NaN;
|
|
199
235
|
const sorted = [...values].sort((a, b) => a - b);
|
package/lib/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ensureClientInstalled, formatInstallCommand } from './client-install.js
|
|
|
5
5
|
import { CLIENTS, writeClientConfig } from './config.js';
|
|
6
6
|
import { getHelpText, PACKAGE_NAME, PACKAGE_VERSION } from './help.js';
|
|
7
7
|
import { modelByID, validateApiKey } from './models.js';
|
|
8
|
-
import {
|
|
8
|
+
import { INGRESS_NODES, nodeMatchesID } from './nodes.js';
|
|
9
9
|
import { createTheme, stripAnsi } from './terminal.js';
|
|
10
10
|
|
|
11
11
|
export async function runCli(argv = []) {
|
|
@@ -104,7 +104,7 @@ export function printResults(results, theme = createTheme()) {
|
|
|
104
104
|
const recommended = results.find((item) => item.successRate > 0);
|
|
105
105
|
const fastestTotal = findFastestTotalResult(results);
|
|
106
106
|
const rows = results.map((item) => [
|
|
107
|
-
|
|
107
|
+
formatNodeLabel(item.node),
|
|
108
108
|
formatMs(item.dnsMs),
|
|
109
109
|
formatMs(item.tcpMs),
|
|
110
110
|
formatMs(item.tlsMs),
|
|
@@ -164,10 +164,11 @@ async function askContextLength(rl, model, preferred) {
|
|
|
164
164
|
|
|
165
165
|
export async function chooseNode(rl, results, preferred, recommendedID, theme = createTheme()) {
|
|
166
166
|
const nodes = results.map((item) => item.node);
|
|
167
|
-
const preferredNode = nodes.find((node) => node
|
|
167
|
+
const preferredNode = nodes.find((node) => nodeMatchesID(node, preferred));
|
|
168
168
|
if (preferredNode) return preferredNode;
|
|
169
169
|
if (recommendedID) {
|
|
170
|
-
|
|
170
|
+
const recommendedNode = nodes.find((node) => nodeMatchesID(node, recommendedID));
|
|
171
|
+
printStatus(theme, '综合推荐', recommendedNode ? formatNodeLabel(recommendedNode) : recommendedID);
|
|
171
172
|
}
|
|
172
173
|
return (await choose(rl, '请选择最终写入的入口', nodes.map((node) => ({
|
|
173
174
|
id: node.id,
|
|
@@ -223,7 +224,7 @@ export function formatModelLabel(model) {
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
export function formatNodeLabel(node) {
|
|
226
|
-
return
|
|
227
|
+
return String(node?.label || node?.id || '');
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
export function assertNodeConfig(node) {
|
package/lib/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const PACKAGE_NAME = 'gpteam';
|
|
2
|
-
export const PACKAGE_VERSION = '0.1.
|
|
2
|
+
export const PACKAGE_VERSION = '0.1.28';
|
|
3
3
|
|
|
4
4
|
export function getHelpText() {
|
|
5
5
|
return [
|
|
@@ -15,7 +15,7 @@ export function getHelpText() {
|
|
|
15
15
|
' --model <id> 预选模型,例如 gpt-5.5',
|
|
16
16
|
' --context <tokens> 预设可配置上下文长度',
|
|
17
17
|
' --effort <level> 按模型支持项预选,常见为 none / low / medium / high / xhigh',
|
|
18
|
-
' --node <id> jp-direct / jp-split / hk-split',
|
|
18
|
+
' --node <id> main / jp / hk(兼容旧参数 jp-direct / jp-split / hk-split)',
|
|
19
19
|
' --rounds <n> 每个入口测速轮数,默认 3',
|
|
20
20
|
' --max-output-tokens <n> 测速输出上限,默认 648',
|
|
21
21
|
' --install-client 客户端命令缺失时直接安装,不再二次确认',
|
package/lib/nodes.js
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
export const INGRESS_NODES = [
|
|
2
2
|
{
|
|
3
|
-
id: '
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
split: false,
|
|
3
|
+
id: 'main',
|
|
4
|
+
aliases: ['jp-direct'],
|
|
5
|
+
label: '主入口',
|
|
7
6
|
baseUrl: 'https://api.gpteamservices.com/v1',
|
|
8
7
|
healthUrl: 'https://api.gpteamservices.com/api/health'
|
|
9
8
|
},
|
|
10
9
|
{
|
|
11
|
-
id: 'jp
|
|
10
|
+
id: 'jp',
|
|
11
|
+
aliases: ['jp-split'],
|
|
12
12
|
label: '日本入口',
|
|
13
|
-
region: 'JP',
|
|
14
|
-
split: true,
|
|
15
13
|
baseUrl: 'https://api-jp.gpteamservices.com/v1',
|
|
16
14
|
healthUrl: 'https://api-jp.gpteamservices.com/api/health'
|
|
17
15
|
},
|
|
18
16
|
{
|
|
19
|
-
id: 'hk
|
|
17
|
+
id: 'hk',
|
|
18
|
+
aliases: ['hk-split'],
|
|
20
19
|
label: '香港入口',
|
|
21
|
-
region: 'HK',
|
|
22
|
-
split: true,
|
|
23
20
|
baseUrl: 'https://api-hk.gpteamservices.com/v1',
|
|
24
21
|
healthUrl: 'https://api-hk.gpteamservices.com/api/health'
|
|
25
22
|
}
|
|
26
23
|
];
|
|
27
24
|
|
|
28
|
-
export function
|
|
29
|
-
|
|
25
|
+
export function nodeMatchesID(node, id) {
|
|
26
|
+
const value = String(id || '').trim();
|
|
27
|
+
if (!value) return false;
|
|
28
|
+
return node.id === value || (Array.isArray(node.aliases) && node.aliases.includes(value));
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
export function endpointRoot(baseUrl) {
|