gpteam 0.1.29 → 0.1.31

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
@@ -1,20 +1,20 @@
1
- # GPTeam CLI
1
+ # GPTEAM CLI
2
2
 
3
- Interactive GPTeam API client configurator.
3
+ Interactive GPTEAM API client configurator.
4
4
 
5
5
  ```bash
6
6
  npx gpteam
7
7
  ```
8
8
 
9
- The CLI asks for an API key, reads the key-scoped GPTeam capability summary, shows only the available clients and models for that key's group, benchmarks production ingress endpoints with real API requests, then backs up old files and writes the selected client configuration. Legacy servers fall back to `/v1/models` for validation only.
9
+ The CLI asks for an API key, reads the key-scoped GPTEAM capability summary, shows only the available clients and models for that key's group, benchmarks production ingress endpoints with real API requests, then backs up old files and writes the selected client configuration. Legacy servers fall back to `/v1/models` for validation only.
10
10
 
11
- When a selected client is missing, the interactive CLI shows the exact install command and asks before running it. `--install-client` skips that confirmation for scripted setup. Codex, Claude Code, OpenCode, and OpenClaw use npm-based install commands; OpenCode uses the current `opencode-ai` package. Windows is blocked for OpenClaw because GPTeam's OpenClaw config writer currently supports macOS and Linux only.
11
+ When a selected client is missing, the interactive CLI shows the exact install command and asks before running it. `--install-client` skips that confirmation for scripted setup. Codex, Claude Code, OpenCode, and OpenClaw use npm-based install commands; OpenCode uses the current `opencode-ai` package. Windows is blocked for OpenClaw because GPTEAM's OpenClaw config writer currently supports macOS and Linux only.
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 `main` and `jp`. Older scripted values `jp-direct` and `jp-split` remain compatibility aliases. The retired Hong Kong ingress is not included in the package, generated configs, help text, or release smoke tests.
16
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`.
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`.
18
18
 
19
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`.
20
20
 
@@ -29,9 +29,9 @@ The Image MCP exposes an async-first local job flow plus a legacy compatibility
29
29
 
30
30
  Image MCP results are returned as stable JSON text and MCP `structuredContent`. Successful results include final file path, model, action, size, format, quality, byte size, SHA-256, MIME type, image dimensions, duration, retry count, `job_id`, `trace_id`, and optional `idempotency_key`. Error results use stable `error.code`, `error.message`, `error.retryable`, `error.stage`, `error.upstream_status`, and `error.trace_id` fields while keeping compatibility fields such as `category` and `http_status`.
31
31
 
32
- The MCP supports normal text-to-image generation and image-to-image/edit inputs. Pass `images` as data URLs, HTTPS URLs, or local file paths. For easier tool calling, `image`, `image_path`, `image_paths`, `input_image`, and `input_images` are accepted as aliases. Pass `mask` or `mask_path` the same way for masked edits. `input_fidelity` is accepted for compatibility but is not forwarded to the current GPTeam Image 2 bridge because upstream Codex image edits reject it. The MCP requests GPTeam image endpoints with `stream=true` and reads the final image event, so long image jobs get early stream bytes instead of sitting idle until the final JSON body. File writes create missing directories, avoid overwriting existing files by adding `-v2`, `-v3`, etc., and validate PNG/JPEG/WebP before returning success. `overwrite: true` is available for explicit replacement. `return_revised_prompt` controls whether the upstream revised prompt is included in the result.
32
+ The MCP supports normal text-to-image generation and image-to-image/edit inputs. Pass `images` as data URLs, HTTPS URLs, or local file paths. For easier tool calling, `image`, `image_path`, `image_paths`, `input_image`, and `input_images` are accepted as aliases. Pass `mask` or `mask_path` the same way for masked edits. `input_fidelity` is accepted for compatibility but is not forwarded to the current GPTEAM Image 2 bridge because upstream Codex image edits reject it. The MCP requests GPTEAM image endpoints with `stream=true` and reads the final image event, so long image jobs get early stream bytes instead of sitting idle until the final JSON body. File writes create missing directories, avoid overwriting existing files by adding `-v2`, `-v3`, etc., and validate PNG/JPEG/WebP before returning success. `overwrite: true` is available for explicit replacement. `return_revised_prompt` controls whether the upstream revised prompt is included in the result.
33
33
 
34
- Claude Code is written to `~/.claude/settings.json` under the `env` section, using the GPTeam `/anthropic` base URL. OpenClaw writes `models.providers.gpteam` and also selects `gpteam/<model>` under `agents.defaults.model`, so the chosen model is active without an extra manual step.
34
+ Claude Code is written to `~/.claude/settings.json` under the `env` section, using the GPTEAM `/anthropic` base URL. OpenClaw writes `models.providers.gpteam` and also selects `gpteam/<model>` under `agents.defaults.model`, so the chosen model is active without an extra manual step.
35
35
 
36
36
  Supported clients:
37
37
 
package/lib/bench.js CHANGED
@@ -118,14 +118,7 @@ function measureStream(baseUrl, options) {
118
118
  const url = new URL(`${baseUrl.replace(/\/$/, '')}/responses`);
119
119
  const started = performance.now();
120
120
  const timings = { dnsMs: NaN, tcpMs: NaN, tlsMs: NaN, firstEventMs: NaN };
121
- const payload = JSON.stringify({
122
- model: options.model,
123
- stream: true,
124
- input: options.prompt || '请只回复一句话:节点测速完成。',
125
- max_output_tokens: options.maxOutputTokens || 648,
126
- reasoning: options.effort ? { effort: options.effort } : undefined,
127
- metadata: { gpteam_config_probe: '1' }
128
- });
121
+ const payload = JSON.stringify(buildResponsesProbePayload(options));
129
122
 
130
123
  const request = https.request({
131
124
  protocol: url.protocol,
@@ -197,6 +190,21 @@ function measureStream(baseUrl, options) {
197
190
  });
198
191
  }
199
192
 
193
+ export function buildResponsesProbePayload(options = {}) {
194
+ const prompt = options.prompt || '请只回复一句话:节点测速完成。';
195
+ return {
196
+ model: options.model,
197
+ stream: true,
198
+ instructions: 'You are a helpful coding assistant.',
199
+ input: [{
200
+ role: 'user',
201
+ content: [{ type: 'input_text', text: prompt }]
202
+ }],
203
+ reasoning: options.effort ? { effort: options.effort } : undefined,
204
+ metadata: { gpteam_config_probe: '1' }
205
+ };
206
+ }
207
+
200
208
  export function formatStreamProbeError(status, headers, body, semanticError) {
201
209
  const statusCode = Number(status || 0);
202
210
  const contentType = String(headers?.['content-type'] || headers?.['Content-Type'] || '').trim();
package/lib/cli.js CHANGED
@@ -4,7 +4,7 @@ import { benchmarkNodes, formatMs } from './bench.js';
4
4
  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
- import { modelByID, validateApiKey } from './models.js';
7
+ import { DEFAULT_OPENAI_MODEL_ID, validateApiKey } from './models.js';
8
8
  import { INGRESS_NODES, nodeMatchesID, nodesFromCapabilities } from './nodes.js';
9
9
  import { createTheme, stripAnsi } from './terminal.js';
10
10
 
@@ -30,24 +30,26 @@ export async function runCli(argv = []) {
30
30
  const ingressNodes = nodesFromCapabilities(validation);
31
31
  printStatus(theme, '通过', formatValidationSummary(validation));
32
32
 
33
- printStep(theme, 2, 5, '选择客户端和模型');
34
- const clientChoices = filterClientsForCapabilities(CLIENTS, validation.clients);
33
+ printStep(theme, 2, 5, '选择客户端');
34
+ const platform = validation.group?.platform || '';
35
+ const allowedClients = Array.isArray(validation.clients) && validation.clients.length
36
+ ? validation.clients
37
+ : clientIDsForPlatform(platform);
38
+ const clientChoices = filterClientsForCapabilities(CLIENTS, allowedClients);
35
39
  const client = await choose(rl, '请选择客户端类型', clientChoices, args.client, theme);
36
40
  await ensureSelectedClientInstalled(client.id, args, rl, theme);
37
41
  const models = validation.models;
38
- const model = await chooseModel(rl, models, args.model, theme);
39
- const contextLength = await askContextLength(rl, model, args.context);
40
- const effort = await choose(rl, '请选择思考深度', model.efforts.map((id) => ({ id, label: id })), args.effort, theme);
41
- const maxOutputTokens = Number(args.maxOutputTokens || 648);
42
+ const model = selectDefaultModel(models, validation.defaultModel || '', platform);
43
+ const contextLength = Number(model.contextLength || 400000);
44
+ const maxOutputTokens = Number(model.maxOutputTokens || 648);
42
45
 
43
46
  printStep(theme, 3, 5, '真实请求测速', 'GET /api/health + POST /v1/responses stream=true');
44
47
  printHint(theme, '入口之间并行测速,单入口多轮顺序执行,避免同时打出过多真实请求。');
45
48
  printHint(theme, '综合推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好;完成最快会单独标出。');
46
- printHint(theme, `模型:${model.id},测速输出上限:${maxOutputTokens}`);
49
+ printHint(theme, `测速模型:${model.id}`);
47
50
  const results = await benchmarkNodes(ingressNodes, {
48
51
  apiKey,
49
52
  model: model.id,
50
- effort: effort.id,
51
53
  maxOutputTokens,
52
54
  rounds: Number(args.rounds || 3)
53
55
  });
@@ -62,9 +64,10 @@ export async function runCli(argv = []) {
62
64
  const written = writeClientConfig(client.id, {
63
65
  apiKey,
64
66
  model: model.id,
65
- effort: effort.id,
66
67
  contextLength,
67
68
  maxOutputTokens: model.maxOutputTokens,
69
+ models,
70
+ platform,
68
71
  node: selectedNode,
69
72
  imageMCP: validation.imageMCP
70
73
  });
@@ -75,7 +78,7 @@ export async function runCli(argv = []) {
75
78
  console.log(`入口:${formatNodeLabel(selectedNode)}`);
76
79
  console.log(`地址:${selectedNode.baseUrl}`);
77
80
  if (['codex', 'opencode', 'claude-code'].includes(client.id) && validation.imageMCP?.enabled === true) {
78
- printHint(theme, '已写入 GPTeam Image MCP。客户端对话里需要生图或图生图时优先调用 create_image_job,再用 get_image_job_status 和 download_image_result 取结果,get_capabilities 可查看支持能力,MCP 使用专用环境变量读取 API key。');
81
+ printHint(theme, '已写入 GPTEAM Image MCP。客户端对话里需要生图或图生图时优先调用 create_image_job,再用 get_image_job_status 和 download_image_result 取结果,get_capabilities 可查看支持能力,MCP 使用专用环境变量读取 API key。');
79
82
  }
80
83
  } finally {
81
84
  rl.close();
@@ -146,23 +149,26 @@ export function resultMarker(item, recommended, fastestTotal) {
146
149
  return labels.length ? labels.join('/') : '-';
147
150
  }
148
151
 
149
- async function chooseModel(rl, models, preferred, theme) {
150
- const selected = preferred ? modelByID(models, preferred) : null;
151
- if (selected) return selected;
152
- printSubTitle(theme, '可用模型');
153
- models.forEach((model, index) => {
154
- console.log(` ${theme.info(String(index + 1).padStart(2, ' '))} ${formatModelLabel(model)}`);
155
- });
156
- const answer = await askRequired(rl, '请选择模型序号:');
157
- const index = Math.max(1, Math.min(models.length, Number(answer) || 1)) - 1;
158
- return models[index];
152
+ export function selectDefaultModel(models, preferred, platform) {
153
+ const available = Array.isArray(models) ? models.filter((model) => model && model.id) : [];
154
+ if (!available.length) {
155
+ throw new Error('没有可配置模型,已停止写入客户端配置');
156
+ }
157
+ const current = normalizePlatform(platform);
158
+ const preferredIDs = defaultModelCandidates(current, preferred);
159
+ for (const id of preferredIDs) {
160
+ const match = available.find((model) => model.id === id);
161
+ if (match) return match;
162
+ }
163
+ return available[0];
159
164
  }
160
165
 
161
- async function askContextLength(rl, model, preferred) {
162
- const max = Number(model.contextLength || 400000);
163
- if (preferred) return clamp(Number(preferred), 1, max);
164
- const answer = await rl.question(`请输入可配置上下文长度(最大 ${max},输出上限 ${model.maxOutputTokens},默认 ${max},回车即选择默认):`);
165
- return clamp(Number(answer || max), 1, max);
166
+ function defaultModelCandidates(platform, preferred) {
167
+ const ids = [];
168
+ if (platform === 'gemini') ids.push('gemini-3.1-pro-preview');
169
+ if (platform === 'openai') ids.push(DEFAULT_OPENAI_MODEL_ID);
170
+ ids.push(preferred);
171
+ return [...new Set(ids.map((id) => String(id || '').trim()).filter(Boolean))];
166
172
  }
167
173
 
168
174
  export async function chooseNode(rl, results, preferred, recommendedID, theme = createTheme()) {
@@ -215,24 +221,37 @@ function formatScore(value) {
215
221
  return Number.isFinite(value) ? String(Math.round(value)) : '-';
216
222
  }
217
223
 
218
- function clamp(value, min, max) {
219
- if (!Number.isFinite(value)) return max;
220
- return Math.max(min, Math.min(max, Math.floor(value)));
221
- }
222
-
223
224
  export function formatModelLabel(model) {
224
- const context = Number(model.contextLength || 0);
225
- const outputTokens = Number(model.maxOutputTokens || 0);
226
- return `${model.id}(可配置上下文 ${context},输出上限 ${outputTokens})`;
225
+ return String(model?.id || '');
227
226
  }
228
227
 
229
228
  export function filterClientsForCapabilities(clients, allowedIDs) {
230
229
  if (!Array.isArray(allowedIDs) || !allowedIDs.length) return clients;
231
- const allowed = new Set(allowedIDs.map((id) => String(id)));
230
+ const allowed = new Set(allowedIDs.flatMap((id) => {
231
+ const value = String(id);
232
+ return value === 'codex' ? ['codex', 'codex-ws'] : [value];
233
+ }));
232
234
  const filtered = clients.filter((client) => allowed.has(client.id));
233
235
  return filtered.length ? filtered : clients;
234
236
  }
235
237
 
238
+ export function clientIDsForPlatform(platform) {
239
+ const current = normalizePlatform(platform);
240
+ if (current === 'anthropic') return ['claude-code', 'opencode'];
241
+ if (current === 'gemini') return ['gemini', 'opencode'];
242
+ if (current === 'ark' || current === 'openai-compatible') return ['opencode'];
243
+ return ['codex', 'codex-ws', 'claude-code', 'opencode'];
244
+ }
245
+
246
+ function normalizePlatform(value) {
247
+ const key = String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '');
248
+ if (!key) return 'openai';
249
+ if (key.includes('anthropic') || key.includes('claude')) return 'anthropic';
250
+ if (key.includes('gemini') || key.includes('google')) return 'gemini';
251
+ if (key.includes('ark') || key.includes('volc') || key.includes('doubao') || key.includes('openaicompatible')) return 'ark';
252
+ return 'openai';
253
+ }
254
+
236
255
  function formatValidationSummary(validation) {
237
256
  const group = validation.group && validation.group.name
238
257
  ? `,分组:${validation.group.name}`
@@ -251,7 +270,7 @@ export function assertNodeConfig(node) {
251
270
  }
252
271
 
253
272
  function printBanner(theme) {
254
- console.log(theme.brand('GPTeam API 配置助手'));
273
+ console.log(theme.brand('GPTEAM API 配置助手'));
255
274
  console.log(theme.muted('真实请求测速,自动写入客户端配置,旧配置会先备份。'));
256
275
  }
257
276
 
@@ -8,6 +8,13 @@ export const CLIENT_INSTALL_SPECS = {
8
8
  installCommand: ['npm', ['i', '-g', '@openai/codex@latest']],
9
9
  versionHint: 'codex --version'
10
10
  },
11
+ 'codex-ws': {
12
+ id: 'codex-ws',
13
+ label: 'Codex (WebSocket)',
14
+ command: 'codex',
15
+ installCommand: ['npm', ['i', '-g', '@openai/codex@latest']],
16
+ versionHint: 'codex --version'
17
+ },
11
18
  opencode: {
12
19
  id: 'opencode',
13
20
  label: 'OpenCode',
@@ -22,6 +29,13 @@ export const CLIENT_INSTALL_SPECS = {
22
29
  installCommand: ['npm', ['i', '-g', '@anthropic-ai/claude-code@latest']],
23
30
  versionHint: 'claude --version'
24
31
  },
32
+ gemini: {
33
+ id: 'gemini',
34
+ label: 'Gemini CLI',
35
+ command: 'gemini',
36
+ installCommand: ['npm', ['i', '-g', '@google/gemini-cli@latest']],
37
+ versionHint: 'gemini --version'
38
+ },
25
39
  openclaw: {
26
40
  id: 'openclaw',
27
41
  label: 'OpenClaw',
package/lib/config.js CHANGED
@@ -2,7 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import JSON5 from 'json5';
5
- import { endpointRoot } from './nodes.js';
5
+ import { DEFAULT_OPENAI_SMALL_MODEL_ID } from './models.js';
6
+ import { endpointAPIBase, endpointRoot } from './nodes.js';
6
7
 
7
8
  const PROVIDER_ID = 'gpteam';
8
9
  const IMAGE_MCP_ID = 'gpteam_image';
@@ -18,15 +19,19 @@ const IMAGE_MCP_ENABLED_TOOLS = [
18
19
 
19
20
  export const CLIENTS = [
20
21
  { id: 'codex', label: 'Codex' },
22
+ { id: 'codex-ws', label: 'Codex (WebSocket)' },
21
23
  { id: 'opencode', label: 'OpenCode' },
22
24
  { id: 'claude-code', label: 'Claude Code' },
25
+ { id: 'gemini', label: 'Gemini CLI' },
23
26
  { id: 'openclaw', label: 'OpenClaw(macOS / Linux)' }
24
27
  ];
25
28
 
26
29
  export function writeClientConfig(clientID, settings) {
27
30
  if (clientID === 'codex') return writeCodexConfig(settings);
31
+ if (clientID === 'codex-ws') return writeCodexConfig({ ...settings, websocket: true });
28
32
  if (clientID === 'opencode') return writeOpenCodeConfig(settings);
29
33
  if (clientID === 'claude-code') return writeClaudeCodeConfig(settings);
34
+ if (clientID === 'gemini') return writeGeminiCLIConfig(settings);
30
35
  if (clientID === 'openclaw') return writeOpenClawConfig(settings);
31
36
  throw new Error(`未知客户端:${clientID}`);
32
37
  }
@@ -44,8 +49,6 @@ export function writeCodexConfig(settings) {
44
49
  const managedRoot = [
45
50
  `model = ${tomlString(settings.model)}`,
46
51
  `model_provider = "gpteam"`,
47
- `model_context_window = ${Number(settings.contextLength)}`,
48
- `model_reasoning_effort = ${tomlString(settings.effort)}`,
49
52
  'disable_response_storage = true'
50
53
  ];
51
54
  const managedProvider = [
@@ -56,7 +59,11 @@ export function writeCodexConfig(settings) {
56
59
  'requires_openai_auth = true',
57
60
  'supports_websockets = true'
58
61
  ];
62
+ const websocketFeature = settings.websocket === true
63
+ ? ['[features]', 'responses_websockets_v2 = true']
64
+ : [];
59
65
  const mcpCommand = codexImageMCPCommand();
66
+ const mcpEnv = imageMCPEnv(settings);
60
67
  const managedImageMCP = [
61
68
  `[mcp_servers.${IMAGE_MCP_ID}]`,
62
69
  `command = ${tomlString(mcpCommand.command)}`,
@@ -67,14 +74,16 @@ export function writeCodexConfig(settings) {
67
74
  'default_tools_approval_mode = "prompt"',
68
75
  '',
69
76
  `[mcp_servers.${IMAGE_MCP_ID}.env]`,
70
- `GPTEAM_API_KEY = ${tomlString(settings.apiKey)}`,
71
- `GPTEAM_BASE_URL = ${tomlString(settings.node.baseUrl)}`,
72
- `GPTEAM_CODEX_HOME = ${tomlString(dir)}`
77
+ `GPTEAM_API_KEY = ${tomlString(mcpEnv.GPTEAM_API_KEY)}`,
78
+ `GPTEAM_BASE_URL = ${tomlString(mcpEnv.GPTEAM_BASE_URL)}`,
79
+ `GPTEAM_CODEX_HOME = ${tomlString(dir)}`,
80
+ ...imageMCPConcurrencyEnvLines(mcpEnv)
73
81
  ];
74
82
  const next = joinTomlSections([
75
83
  managedRoot.join('\n'),
76
84
  rootLines.join('\n'),
77
85
  rest.join('\n'),
86
+ websocketFeature.join('\n'),
78
87
  imageMCPEnabled(settings) ? managedImageMCP.join('\n') : '',
79
88
  managedProvider.join('\n')
80
89
  ]);
@@ -91,31 +100,24 @@ export function writeOpenCodeConfig(settings) {
91
100
  backupIfExists(filePath);
92
101
  const config = readJSON(filePath, { $schema: 'https://opencode.ai/config.json' });
93
102
  config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
103
+ const providerProfile = openCodeProviderProfile(settings);
94
104
  config.provider[PROVIDER_ID] = {
95
- npm: '@ai-sdk/openai',
96
- name: 'GPTeam',
105
+ npm: providerProfile.npm,
106
+ name: 'GPTEAM',
97
107
  options: {
98
108
  apiKey: settings.apiKey,
99
- baseURL: settings.node.baseUrl,
109
+ baseURL: providerProfile.baseURL,
100
110
  setCacheKey: true
101
111
  },
102
- models: {
103
- [settings.model]: {
104
- name: settings.model,
105
- limit: {
106
- context: Number(settings.contextLength),
107
- output: Number(settings.maxOutputTokens)
108
- },
109
- variants: {
110
- [settings.effort]: {
111
- reasoningEffort: settings.effort,
112
- reasoningSummary: 'auto',
113
- textVerbosity: 'medium'
114
- }
115
- }
116
- }
117
- }
112
+ models: openCodeModels(settings)
118
113
  };
114
+ const modelEntries = normalizeSettingsModels(settings);
115
+ const defaultModel = resolveOpenCodeDefaultModel(settings, modelEntries);
116
+ if (defaultModel) {
117
+ const smallModel = resolveOpenCodeSmallModel(settings, modelEntries, defaultModel);
118
+ config.model = `${PROVIDER_ID}/${defaultModel}`;
119
+ config.small_model = `${PROVIDER_ID}/${smallModel}`;
120
+ }
119
121
  config.mcp = config.mcp && typeof config.mcp === 'object' ? config.mcp : {};
120
122
  if (imageMCPEnabled(settings)) {
121
123
  config.mcp[IMAGE_MCP_ID] = openCodeImageMCPConfig(settings);
@@ -141,9 +143,9 @@ export function writeClaudeCodeConfig(settings) {
141
143
  ANTHROPIC_MODEL: settings.model,
142
144
  ANTHROPIC_DEFAULT_HAIKU_MODEL: settings.model,
143
145
  ANTHROPIC_DEFAULT_SONNET_MODEL: settings.model,
144
- ANTHROPIC_DEFAULT_OPUS_MODEL: settings.model,
145
- ANTHROPIC_CUSTOM_HEADERS: `X-Codex-Reasoning-Effort: ${settings.effort}`
146
+ ANTHROPIC_DEFAULT_OPUS_MODEL: settings.model
146
147
  });
148
+ delete config.env.ANTHROPIC_CUSTOM_HEADERS;
147
149
  writeJSON(filePath, config);
148
150
  const mcpConfig = readJSON(mcpPath, {});
149
151
  mcpConfig.mcpServers = mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object'
@@ -160,6 +162,20 @@ export function writeClaudeCodeConfig(settings) {
160
162
 
161
163
  export const writeClaudeCodeEnv = writeClaudeCodeConfig;
162
164
 
165
+ export function writeGeminiCLIConfig(settings) {
166
+ const dir = path.join(homeDir(), '.gpteam');
167
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
168
+ const filePath = path.join(dir, 'gemini-cli.env');
169
+ backupIfExists(filePath);
170
+ const lines = [
171
+ `export GOOGLE_GEMINI_BASE_URL=${shellString(geminiBaseUrl(settings.node.baseUrl))}`,
172
+ `export GEMINI_API_KEY=${shellString(settings.apiKey)}`,
173
+ `export GEMINI_MODEL=${shellString(settings.model)}`
174
+ ];
175
+ writeTextAtomic(filePath, `${lines.join('\n')}\n`, 0o600);
176
+ return [filePath];
177
+ }
178
+
163
179
  export function writeOpenClawConfig(settings) {
164
180
  if (process.platform === 'win32') {
165
181
  throw new Error('OpenClaw 自动配置当前只支持 macOS / Linux');
@@ -223,6 +239,185 @@ function claudeBaseUrl(baseUrl) {
223
239
  return `${root}/anthropic`;
224
240
  }
225
241
 
242
+ function geminiBaseUrl(baseUrl) {
243
+ const root = endpointRoot(baseUrl);
244
+ if (/\/v1beta\/?$/.test(root)) return root.replace(/\/$/, '');
245
+ return `${root}/v1beta`;
246
+ }
247
+
248
+ function openCodeProviderProfile(settings) {
249
+ const platform = normalizePlatform(settings?.platform || settings?.group?.platform);
250
+ const baseUrl = settings?.node?.baseUrl || '';
251
+ if (platform === 'anthropic') {
252
+ return { npm: '@ai-sdk/anthropic', baseURL: claudeBaseUrl(baseUrl) };
253
+ }
254
+ if (platform === 'gemini') {
255
+ return { npm: '@ai-sdk/google', baseURL: geminiBaseUrl(baseUrl) };
256
+ }
257
+ if (platform === 'ark' || platform === 'openai-compatible') {
258
+ return { npm: '@ai-sdk/openai-compatible', baseURL: endpointAPIBase(baseUrl) };
259
+ }
260
+ return { npm: '@ai-sdk/openai', baseURL: endpointAPIBase(baseUrl) };
261
+ }
262
+
263
+ function resolveOpenCodeDefaultModel(settings, entries = normalizeSettingsModels(settings)) {
264
+ return String(settings.model || entries[0]?.id || '').trim();
265
+ }
266
+
267
+ function resolveOpenCodeSmallModel(settings, entries, defaultModel) {
268
+ const platform = normalizePlatform(settings?.platform || settings?.group?.platform);
269
+ if (platform === 'openai') {
270
+ const smallModel = findOpenCodeModelID(entries, DEFAULT_OPENAI_SMALL_MODEL_ID);
271
+ if (smallModel) return smallModel;
272
+ }
273
+ return defaultModel;
274
+ }
275
+
276
+ function findOpenCodeModelID(entries, id) {
277
+ const target = String(id || '').trim();
278
+ if (!target) return '';
279
+ const match = (entries || []).find((model) => String(model?.id || model?.name || '').trim() === target);
280
+ return match ? target : '';
281
+ }
282
+
283
+ function openCodeModels(settings) {
284
+ const entries = normalizeSettingsModels(settings);
285
+ const out = {};
286
+ for (const model of entries) {
287
+ const id = String(model.id || model.name || '').trim();
288
+ if (!id || out[id]) continue;
289
+ const limit = {};
290
+ const context = Number(model.contextLength || model.context_length || model.inputTokenLimit || settings.contextLength);
291
+ const output = Number(model.maxOutputTokens || model.max_completion_tokens || model.outputTokenLimit || settings.maxOutputTokens);
292
+ if (Number.isFinite(context) && context > 0) limit.context = Math.floor(context);
293
+ if (Number.isFinite(output) && output > 0) limit.output = Math.floor(output);
294
+ const item = {
295
+ id,
296
+ name: String(model.displayName || model.display_name || id),
297
+ tool_call: true,
298
+ modalities: { input: ['text'], output: ['text'] },
299
+ ...(Object.keys(limit).length >= 2 ? { limit } : {})
300
+ };
301
+ if (openCodeModelSupportsReasoning(model, settings)) {
302
+ item.reasoning = true;
303
+ const options = openCodeModelOptions(model, settings);
304
+ if (Object.keys(options).length > 0) item.options = options;
305
+ const variants = openCodeModelVariants(model, settings);
306
+ if (Object.keys(variants).length > 0) item.variants = variants;
307
+ }
308
+ out[id] = item;
309
+ }
310
+ return out;
311
+ }
312
+
313
+ function openCodeModelOptions(model, settings) {
314
+ const platform = normalizePlatform(settings?.platform || settings?.group?.platform);
315
+ if (platform === 'anthropic') {
316
+ return { thinking: { type: 'enabled', budgetTokens: openCodeThinkingBudget(model) } };
317
+ }
318
+ if (platform === 'openai') {
319
+ return { reasoningEffort: 'high' };
320
+ }
321
+ if (platform === 'gemini') {
322
+ return openCodeGeminiThinkingOptions(model, 'high');
323
+ }
324
+ return {};
325
+ }
326
+
327
+ function openCodeModelVariants(model, settings) {
328
+ const platform = normalizePlatform(settings?.platform || settings?.group?.platform);
329
+ if (platform === 'openai') return openCodeOpenAIVariants(model);
330
+ if (platform === 'anthropic') return openCodeAnthropicVariants(model);
331
+ if (platform === 'gemini') return openCodeGeminiVariants(model);
332
+ return {};
333
+ }
334
+
335
+ function openCodeOpenAIVariants(model) {
336
+ return Object.fromEntries(openCodeThinkingLevels(model, ['low', 'medium', 'high', 'xhigh'])
337
+ .map((level) => [level, { reasoningEffort: level }]));
338
+ }
339
+
340
+ function openCodeAnthropicVariants(model) {
341
+ const budgets = { low: 8000, medium: 16000, high: 32000, xhigh: 64000 };
342
+ return Object.fromEntries(openCodeThinkingLevels(model, ['low', 'medium', 'high', 'xhigh'])
343
+ .map((level) => [level, { thinking: { type: 'enabled', budgetTokens: budgets[level] || 32000 } }]));
344
+ }
345
+
346
+ function openCodeGeminiVariants(model) {
347
+ return Object.fromEntries(openCodeThinkingLevels(model, ['low', 'medium', 'high'])
348
+ .map((level) => [level, openCodeGeminiThinkingOptions(model, level)]));
349
+ }
350
+
351
+ function openCodeThinkingLevels(model, fallback) {
352
+ const levels = Array.isArray(model?.thinking?.levels) ? model.thinking.levels : fallback;
353
+ const out = [];
354
+ for (const level of levels) {
355
+ const normalized = String(level || '').trim().toLowerCase();
356
+ if (normalized && !out.includes(normalized)) out.push(normalized);
357
+ }
358
+ return out;
359
+ }
360
+
361
+ function openCodeGeminiThinkingOptions(model, level) {
362
+ const id = String(model?.id || model?.name || '').toLowerCase();
363
+ if (/gemini-3/.test(id)) return { thinkingConfig: { thinkingLevel: level } };
364
+ const budgets = { low: 1024, medium: 8192, high: 24576 };
365
+ return { thinkingConfig: { thinkingBudget: budgets[level] || 24576 } };
366
+ }
367
+
368
+ function openCodeThinkingBudget(model) {
369
+ const values = [
370
+ model?.thinking?.budgetTokens,
371
+ model?.thinking?.budget_tokens,
372
+ model?.thinkingBudgetTokens
373
+ ];
374
+ for (const value of values) {
375
+ const parsed = Number(value);
376
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
377
+ }
378
+ return 32000;
379
+ }
380
+
381
+ function openCodeModelSupportsReasoning(model, settings) {
382
+ if (model && model.thinking && typeof model.thinking === 'object') return true;
383
+ const id = String(model?.id || model?.name || '').toLowerCase();
384
+ const platform = normalizePlatform(settings?.platform || settings?.group?.platform);
385
+ if (/thinking|reasoning/.test(id)) return true;
386
+ if (platform === 'openai' && /^(gpt-|codex-)/.test(id)) return true;
387
+ if (platform === 'anthropic' && /^claude-/.test(id)) return true;
388
+ if (platform === 'gemini' && /^gemini-(3|2\.5)/.test(id)) return true;
389
+ return false;
390
+ }
391
+
392
+ function normalizeSettingsModels(settings) {
393
+ const models = Array.isArray(settings?.models) ? settings.models : [];
394
+ const entries = models.length ? models : [{
395
+ id: settings.model,
396
+ contextLength: settings.contextLength,
397
+ maxOutputTokens: settings.maxOutputTokens
398
+ }];
399
+ return entries.filter((model) => model && String(model.id || model.name || '').trim());
400
+ }
401
+
402
+ function normalizePlatform(value) {
403
+ const key = String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '');
404
+ if (!key) return 'openai';
405
+ if (key.includes('anthropic') || key.includes('claude')) return 'anthropic';
406
+ if (key.includes('gemini') || key.includes('google')) return 'gemini';
407
+ if (
408
+ key.includes('ark') ||
409
+ key.includes('volc') ||
410
+ key.includes('doubao') ||
411
+ key.includes('bytedance') ||
412
+ key.includes('openaicompatible')
413
+ ) return 'ark';
414
+ return 'openai';
415
+ }
416
+
417
+ function shellString(value) {
418
+ return `"${String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
419
+ }
420
+
226
421
  function codexImageMCPCommand() {
227
422
  if (process.platform === 'win32') {
228
423
  return {
@@ -246,10 +441,34 @@ function imageMCPServer(settings) {
246
441
  }
247
442
 
248
443
  function imageMCPEnv(settings) {
249
- return {
444
+ const env = {
250
445
  GPTEAM_API_KEY: String(settings.apiKey || ''),
251
446
  GPTEAM_BASE_URL: String(settings.node && settings.node.baseUrl ? settings.node.baseUrl : '')
252
447
  };
448
+ const maxConcurrent = positiveIntString(settings?.imageMCP?.max_concurrent_jobs);
449
+ if (maxConcurrent) env.GPTEAM_IMAGE_MAX_CONCURRENT = maxConcurrent;
450
+ const maxQueue = positiveIntString(settings?.imageMCP?.max_queued_jobs, true);
451
+ if (maxQueue) env.GPTEAM_IMAGE_MAX_QUEUE = maxQueue;
452
+ return env;
453
+ }
454
+
455
+ function imageMCPConcurrencyEnvLines(env) {
456
+ const lines = [];
457
+ if (env.GPTEAM_IMAGE_MAX_CONCURRENT) {
458
+ lines.push(`GPTEAM_IMAGE_MAX_CONCURRENT = ${tomlString(env.GPTEAM_IMAGE_MAX_CONCURRENT)}`);
459
+ }
460
+ if (env.GPTEAM_IMAGE_MAX_QUEUE) {
461
+ lines.push(`GPTEAM_IMAGE_MAX_QUEUE = ${tomlString(env.GPTEAM_IMAGE_MAX_QUEUE)}`);
462
+ }
463
+ return lines;
464
+ }
465
+
466
+ function positiveIntString(value, allowZero = false) {
467
+ const parsed = Number(value);
468
+ if (!Number.isFinite(parsed)) return '';
469
+ const rounded = Math.floor(parsed);
470
+ if (allowZero ? rounded < 0 : rounded <= 0) return '';
471
+ return String(rounded);
253
472
  }
254
473
 
255
474
  function openCodeImageMCPConfig(settings) {
package/lib/help.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.29';
2
+ export const PACKAGE_VERSION = '0.1.31';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
6
- 'GPTeam API 配置助手',
6
+ 'GPTEAM API 配置助手',
7
7
  '',
8
8
  '用法:',
9
9
  ' npx gpteam',
@@ -11,17 +11,13 @@ export function getHelpText() {
11
11
  '',
12
12
  '常用参数:',
13
13
  ' --api-key <key> 预填 API key',
14
- ' --client <id> codex / opencode / claude-code / openclaw',
15
- ' --model <id> 预选模型,例如 gpt-5.5',
16
- ' --context <tokens> 预设可配置上下文长度',
17
- ' --effort <level> 按模型支持项预选,常见为 none / low / medium / high / xhigh',
14
+ ' --client <id> codex / codex-ws / claude-code / opencode / gemini',
18
15
  ' --node <id> main / jp(兼容旧参数 jp-direct / jp-split)',
19
16
  ' --rounds <n> 每个入口测速轮数,默认 3',
20
- ' --max-output-tokens <n> 测速输出上限,默认 648',
21
17
  ' --install-client 客户端命令缺失时直接安装,不再二次确认',
22
18
  ' --help 显示帮助',
23
19
  ' --version 显示版本',
24
20
  '',
25
- '说明:输入 key 后按分组能力展示可用客户端和模型,再测速并写入配置;旧配置会先备份。Codex、OpenCode、Claude Code 会按权限写入 GPTeam Image MCP。'
21
+ '说明:输入 key 后自动识别分组能力,只展示可用客户端,自动选择模型测速并写入配置;旧配置会先备份。Codex、OpenCode、Claude Code 会按权限写入 GPTEAM Image MCP。'
26
22
  ].join('\n');
27
23
  }
@@ -2,7 +2,7 @@ import { formatNetworkError } from '../errors.js';
2
2
 
3
3
  export class ImageMCPError extends Error {
4
4
  constructor(message, options = {}) {
5
- super(String(message || 'GPTeam image MCP error'));
5
+ super(String(message || 'GPTEAM image MCP error'));
6
6
  this.name = 'ImageMCPError';
7
7
  this.code = String(options.code || 'image_mcp_error');
8
8
  this.category = String(options.category || 'unknown');
@@ -77,7 +77,7 @@ export function normalizeImageFormat(value) {
77
77
  function decodeBase64Image(value) {
78
78
  const text = String(value || '').trim();
79
79
  if (!text) {
80
- throw new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
80
+ throw new ImageMCPError('GPTEAM 图片接口没有返回 b64_json 图片数据。', {
81
81
  code: 'image_data_missing',
82
82
  category: 'response_invalid',
83
83
  stage: 'local',
@@ -86,7 +86,7 @@ function decodeBase64Image(value) {
86
86
  }
87
87
  const bytes = Buffer.from(text, 'base64');
88
88
  if (bytes.length === 0) {
89
- throw new ImageMCPError('GPTeam 图片接口返回的图片数据为空。', {
89
+ throw new ImageMCPError('GPTEAM 图片接口返回的图片数据为空。', {
90
90
  code: 'image_data_empty',
91
91
  category: 'response_invalid',
92
92
  stage: 'local',
@@ -68,15 +68,15 @@ export function buildImageGenerationPayload(input = {}, options = {}) {
68
68
  return payload;
69
69
  }
70
70
 
71
- export function loadGPTeamCredentials(options = {}) {
71
+ export function loadGPTEAMCredentials(options = {}) {
72
72
  const env = options.env || process.env;
73
73
  const codexHome = resolveCodexHome(env, options.home || os.homedir());
74
74
  const readFile = options.readFile || ((filePath) => fs.readFileSync(filePath, 'utf8'));
75
75
  const configText = safeRead(path.join(codexHome, 'config.toml'), readFile);
76
- const configuredBaseUrl = parseGPTeamBaseUrl(configText);
76
+ const configuredBaseUrl = parseGPTEAMBaseUrl(configText);
77
77
  const apiKey = firstNonEmpty(env.GPTEAM_API_KEY);
78
78
  if (!apiKey) {
79
- throw new ImageMCPError('没有找到 GPTeam API key。请在 MCP 配置 env 中设置 GPTEAM_API_KEY,或先运行 npx gpteam 完成本地配置。', {
79
+ throw new ImageMCPError('没有找到 GPTEAM API key。请在 MCP 配置 env 中设置 GPTEAM_API_KEY,或先运行 npx gpteam 完成本地配置。', {
80
80
  code: 'api_key_missing',
81
81
  category: 'configuration',
82
82
  stage: 'configuration',
@@ -87,16 +87,16 @@ export function loadGPTeamCredentials(options = {}) {
87
87
  return { apiKey, baseUrl, codexHome };
88
88
  }
89
89
 
90
- export function parseGPTeamBaseUrl(configText) {
91
- let inGPTeamProvider = false;
90
+ export function parseGPTEAMBaseUrl(configText) {
91
+ let inGPTEAMProvider = false;
92
92
  for (const rawLine of String(configText || '').split(/\r?\n/)) {
93
93
  const line = rawLine.trim();
94
94
  const table = line.match(/^\[([^\]]+)\]$/);
95
95
  if (table) {
96
- inGPTeamProvider = table[1] === 'model_providers.gpteam';
96
+ inGPTEAMProvider = table[1] === 'model_providers.gpteam';
97
97
  continue;
98
98
  }
99
- if (!inGPTeamProvider) continue;
99
+ if (!inGPTEAMProvider) continue;
100
100
  const match = line.match(/^base_url\s*=\s*"((?:\\"|[^"])*)"/);
101
101
  if (match) return unescapeTomlString(match[1]);
102
102
  }
@@ -113,7 +113,7 @@ export function normalizeBaseUrl(value) {
113
113
  export async function generateImage(input = {}, options = {}) {
114
114
  const startedAt = now(options);
115
115
  validateImageInput(input, { requirePrompt: true });
116
- const credentials = loadGPTeamCredentials(options);
116
+ const credentials = loadGPTEAMCredentials(options);
117
117
  const payload = buildImageGenerationPayload(input, options);
118
118
  const fetchImpl = options.fetch || globalThis.fetch;
119
119
  if (typeof fetchImpl !== 'function') {
@@ -459,7 +459,7 @@ function imageDataURLToB64(value) {
459
459
  }
460
460
 
461
461
  function missingImageDataError() {
462
- return new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
462
+ return new ImageMCPError('GPTEAM 图片接口没有返回 b64_json 图片数据。', {
463
463
  code: 'image_data_missing',
464
464
  category: 'response_invalid',
465
465
  stage: 'local',
@@ -469,7 +469,7 @@ function missingImageDataError() {
469
469
 
470
470
  function imageErrorFromSSEPayload(payload) {
471
471
  const error = payload && payload.error;
472
- const message = typeof error === 'string' ? error : String(error && error.message || 'GPTeam 图片流返回错误。');
472
+ const message = typeof error === 'string' ? error : String(error && error.message || 'GPTEAM 图片流返回错误。');
473
473
  const code = typeof error === 'object' && error ? String(error.code || payload.code || 'upstream_stream_error') : String(payload.code || 'upstream_stream_error');
474
474
  return new ImageMCPError(message, {
475
475
  code,
@@ -94,7 +94,7 @@ const imageInputProperties = {
94
94
  },
95
95
  input_fidelity: {
96
96
  type: 'string',
97
- description: '兼容字段。当前 GPTeam Image 2 桥接会忽略该字段,因为上游 Codex 图片工具会拒绝 edits 中的该参数。',
97
+ description: '兼容字段。当前 GPTEAM Image 2 桥接会忽略该字段,因为上游 Codex 图片工具会拒绝 edits 中的该参数。',
98
98
  enum: ['low', 'high']
99
99
  },
100
100
  background: {
@@ -126,7 +126,7 @@ const imageInputProperties = {
126
126
  const tools = [
127
127
  {
128
128
  name: 'create_image_job',
129
- description: `推荐常规使用。创建本地后台 GPTeam Image 2 任务并立即返回 job_id。${imageToolPromptingInstruction}`,
129
+ description: `推荐常规使用。创建本地后台 GPTEAM Image 2 任务并立即返回 job_id。${imageToolPromptingInstruction}`,
130
130
  inputSchema: {
131
131
  type: 'object',
132
132
  properties: imageInputProperties,
@@ -136,12 +136,12 @@ const tools = [
136
136
  },
137
137
  {
138
138
  name: 'get_image_job_status',
139
- description: '查询本地 GPTeam Image 2 图片任务状态。',
139
+ description: '查询本地 GPTEAM Image 2 图片任务状态。',
140
140
  inputSchema: jobIDSchema()
141
141
  },
142
142
  {
143
143
  name: 'cancel_image_job',
144
- description: '取消仍在 queued 或 running 的本地 GPTeam Image 2 图片任务。取消是 best-effort,上游已开始生成时不保证同步取消。',
144
+ description: '取消仍在 queued 或 running 的本地 GPTEAM Image 2 图片任务。取消是 best-effort,上游已开始生成时不保证同步取消。',
145
145
  inputSchema: jobIDSchema()
146
146
  },
147
147
  {
@@ -151,7 +151,7 @@ const tools = [
151
151
  },
152
152
  {
153
153
  name: 'get_capabilities',
154
- description: '返回 GPTeam Image MCP 能力,包括支持尺寸、格式、质量、异步任务、取消语义、队列上限和参数约束。',
154
+ description: '返回 GPTEAM Image MCP 能力,包括支持尺寸、格式、质量、异步任务、取消语义、队列上限和参数约束。',
155
155
  inputSchema: {
156
156
  type: 'object',
157
157
  properties: {},
package/lib/models.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import { formatNetworkError } from './errors.js';
2
2
  import { nodeAPIBaseUrl } from './nodes.js';
3
3
 
4
+ export const DEFAULT_OPENAI_MODEL_ID = 'gpt-5.5';
5
+ export const DEFAULT_OPENAI_SMALL_MODEL_ID = 'gpt-5.4-mini';
6
+
4
7
  export const FALLBACK_MODELS = {
5
- 'gpt-5.2': {
6
- id: 'gpt-5.2',
7
- contextLength: 400000,
8
- maxOutputTokens: 128000,
9
- efforts: ['none', 'low', 'medium', 'high', 'xhigh']
10
- },
11
8
  'gpt-5.3-codex': {
12
9
  id: 'gpt-5.3-codex',
13
10
  contextLength: 400000,
@@ -62,6 +59,7 @@ export function normalizeCapabilities(payload) {
62
59
  user: normalizePlainObject(payload?.user),
63
60
  group: normalizePlainObject(payload?.group),
64
61
  protocols: normalizePlainObject(payload?.protocols),
62
+ defaultModel: String(payload?.default_model || payload?.defaultModel || models?.default_model || models?.defaultModel || '').trim(),
65
63
  baseUrls: firstArray(payload?.base_urls, payload?.baseURLs),
66
64
  clients: firstArray(payload?.clients),
67
65
  models: normalizeModelItems(chatItems, { fallbackWhenEmpty: false }),
@@ -114,12 +112,27 @@ function isConfigurableTextModel(id, item, options = {}) {
114
112
  if (!id) return false;
115
113
  if (!options.includeImageModels && id.toLowerCase().includes('image')) return false;
116
114
  const owner = String(item?.owned_by || item?.provider || item?.type || '').trim().toLowerCase();
117
- const knownTextPrefix = /^(gpt-|claude-|gemini-)/.test(id);
115
+ const knownTextPrefix = /^(gpt-|claude-|gemini-|doubao-|deepseek-|kimi-|qwen|glm-|moonshot-|yi-|baichuan-|step-|minimax-)/.test(id);
118
116
  if (!owner) return knownTextPrefix || Object.prototype.hasOwnProperty.call(FALLBACK_MODELS, id);
119
117
  return [
120
118
  'openai',
121
119
  'codex',
122
120
  'openai-compatible',
121
+ 'ark',
122
+ 'volcengine',
123
+ 'volc',
124
+ 'doubao',
125
+ 'bytedance',
126
+ 'deepseek',
127
+ 'kimi',
128
+ 'moonshot',
129
+ 'qwen',
130
+ 'aliyun',
131
+ 'zhipu',
132
+ 'glm',
133
+ 'baichuan',
134
+ 'stepfun',
135
+ 'minimax',
123
136
  'anthropic',
124
137
  'claude',
125
138
  'gemini',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.29",
4
- "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
3
+ "version": "0.1.31",
4
+ "description": "GPTEAM API interactive client configurator and ingress benchmark CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gpteam": "bin/gpteam-api-config.js",