gpteam 0.1.26 → 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 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
- 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 explicitly disables WebSocket prewarm, 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`.
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
 
@@ -21,7 +23,7 @@ The Image MCP exposes an async-first local job flow plus a legacy compatibility
21
23
  - `create_image_job`: recommended for normal use. It starts a local background image job and returns `job_id` quickly, which avoids losing the whole generation when a VPN, proxy, or client has a 60-second idle timeout.
22
24
  - `get_image_job_status`: checks whether the local job is queued, running, succeeded, failed, canceled, or expired.
23
25
  - `cancel_image_job`: cancels a queued/running local job.
24
- - `download_image_result`: returns the completed file metadata and local file path by default. Pass `include_image: true` only when the client must receive MCP image content, because large images can trigger context compaction.
26
+ - `download_image_result`: returns the completed file metadata and image content. Use `metadata_only`, `include_image`, and `include_revised_prompt` to control large result payloads.
25
27
  - `get_capabilities`: returns supported sizes, formats, quality values, async support, cancellation semantics, queue limits, and image-to-image support.
26
28
  - `generate_image`: legacy compatibility alias. It now creates the same async job and returns `job_id` immediately instead of blocking until the image is complete.
27
29
 
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(streams.map((item) => item.dnsMs).filter(Number.isFinite)),
55
- tcpMs: median(streams.map((item) => item.tcpMs).filter(Number.isFinite)),
56
- tlsMs: median(streams.map((item) => item.tlsMs).filter(Number.isFinite)),
57
- healthMs: median(successful.map((sample) => sample.health.totalMs).filter(Number.isFinite)),
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 ok = response.statusCode >= 200
161
- && response.statusCode < 300
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: response.statusCode,
168
+ status,
167
169
  ...timings,
168
170
  totalMs: performance.now() - started,
169
- error: ok ? '' : semantic.error || `stream HTTP ${response.statusCode}`
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 { describeSplit, INGRESS_NODES } from './nodes.js';
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
- `${item.node.label}(${describeSplit(item.node)})`,
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.id === preferred);
167
+ const preferredNode = nodes.find((node) => nodeMatchesID(node, preferred));
168
168
  if (preferredNode) return preferredNode;
169
169
  if (recommendedID) {
170
- printStatus(theme, '综合推荐', recommendedID);
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 `${node.label}(${describeSplit(node)})`;
227
+ return String(node?.label || node?.id || '');
227
228
  }
228
229
 
229
230
  export function assertNodeConfig(node) {
package/lib/config.js CHANGED
@@ -54,7 +54,7 @@ export function writeCodexConfig(settings) {
54
54
  `base_url = ${tomlString(settings.node.baseUrl)}`,
55
55
  'wire_api = "responses"',
56
56
  'requires_openai_auth = true',
57
- 'supports_websockets = false'
57
+ 'supports_websockets = true'
58
58
  ];
59
59
  const mcpCommand = codexImageMCPCommand();
60
60
  const managedImageMCP = [
package/lib/help.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.26';
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 / us-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 客户端命令缺失时直接安装,不再二次确认',
@@ -651,7 +651,7 @@ function normalizeIdempotencyKey(value) {
651
651
  }
652
652
 
653
653
  function shapeDownloadResult(result, input = {}) {
654
- const includeImage = input.include_image === true && !input.metadata_only;
654
+ const includeImage = !input.metadata_only && input.include_image !== false;
655
655
  const includeRevisedPrompt = input.include_revised_prompt !== false;
656
656
  const output = {
657
657
  ...result,
@@ -238,13 +238,13 @@ function downloadSchema() {
238
238
  const schema = jobIDSchema();
239
239
  schema.properties.metadata_only = {
240
240
  type: 'boolean',
241
- description: '只返回文件路径和元数据,不返回 MCP 图片内容。默认 true,避免大图进入上下文触发频繁 compact。',
242
- default: true
241
+ description: 'Return only metadata without MCP image content.',
242
+ default: false
243
243
  };
244
244
  schema.properties.include_image = {
245
245
  type: 'boolean',
246
- description: '显式返回 MCP 图片内容。大图会显著增加上下文,通常保持 false,只使用本地文件路径。',
247
- default: false
246
+ description: 'Include image content when the job succeeded.',
247
+ default: true
248
248
  };
249
249
  schema.properties.include_revised_prompt = {
250
250
  type: 'boolean',
package/lib/nodes.js CHANGED
@@ -1,40 +1,31 @@
1
1
  export const INGRESS_NODES = [
2
2
  {
3
- id: 'jp-direct',
4
- label: '直连',
5
- region: 'JP',
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-split',
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-split',
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
- },
26
- {
27
- id: 'us-split',
28
- label: '美国入口',
29
- region: 'US',
30
- split: true,
31
- baseUrl: 'https://api-us.gpteamservices.com/v1',
32
- healthUrl: 'https://api-us.gpteamservices.com/api/health'
33
22
  }
34
23
  ];
35
24
 
36
- export function describeSplit(node) {
37
- return node.split ? '分流' : '不分流';
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));
38
29
  }
39
30
 
40
31
  export function endpointRoot(baseUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {