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 +7 -7
- package/lib/bench.js +16 -8
- package/lib/cli.js +55 -36
- package/lib/client-install.js +14 -0
- package/lib/config.js +247 -28
- package/lib/help.js +4 -8
- package/lib/image-mcp/errors.js +1 -1
- package/lib/image-mcp/files.js +2 -2
- package/lib/image-mcp/image.js +10 -10
- package/lib/image-mcp/server.js +5 -5
- package/lib/models.js +20 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GPTEAM CLI
|
|
2
2
|
|
|
3
|
-
Interactive
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 =
|
|
39
|
-
const contextLength =
|
|
40
|
-
const
|
|
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,
|
|
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, '已写入
|
|
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
|
-
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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.
|
|
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('
|
|
273
|
+
console.log(theme.brand('GPTEAM API 配置助手'));
|
|
255
274
|
console.log(theme.muted('真实请求测速,自动写入客户端配置,旧配置会先备份。'));
|
|
256
275
|
}
|
|
257
276
|
|
package/lib/client-install.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
71
|
-
`GPTEAM_BASE_URL = ${tomlString(
|
|
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:
|
|
96
|
-
name: '
|
|
105
|
+
npm: providerProfile.npm,
|
|
106
|
+
name: 'GPTEAM',
|
|
97
107
|
options: {
|
|
98
108
|
apiKey: settings.apiKey,
|
|
99
|
-
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
|
-
|
|
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.
|
|
2
|
+
export const PACKAGE_VERSION = '0.1.31';
|
|
3
3
|
|
|
4
4
|
export function getHelpText() {
|
|
5
5
|
return [
|
|
6
|
-
'
|
|
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 /
|
|
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
|
|
21
|
+
'说明:输入 key 后自动识别分组能力,只展示可用客户端,自动选择模型测速并写入配置;旧配置会先备份。Codex、OpenCode、Claude Code 会按权限写入 GPTEAM Image MCP。'
|
|
26
22
|
].join('\n');
|
|
27
23
|
}
|
package/lib/image-mcp/errors.js
CHANGED
|
@@ -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 || '
|
|
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');
|
package/lib/image-mcp/files.js
CHANGED
|
@@ -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('
|
|
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('
|
|
89
|
+
throw new ImageMCPError('GPTEAM 图片接口返回的图片数据为空。', {
|
|
90
90
|
code: 'image_data_empty',
|
|
91
91
|
category: 'response_invalid',
|
|
92
92
|
stage: 'local',
|
package/lib/image-mcp/image.js
CHANGED
|
@@ -68,15 +68,15 @@ export function buildImageGenerationPayload(input = {}, options = {}) {
|
|
|
68
68
|
return payload;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export function
|
|
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 =
|
|
76
|
+
const configuredBaseUrl = parseGPTEAMBaseUrl(configText);
|
|
77
77
|
const apiKey = firstNonEmpty(env.GPTEAM_API_KEY);
|
|
78
78
|
if (!apiKey) {
|
|
79
|
-
throw new ImageMCPError('没有找到
|
|
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
|
|
91
|
-
let
|
|
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
|
-
|
|
96
|
+
inGPTEAMProvider = table[1] === 'model_providers.gpteam';
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
if (!
|
|
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 =
|
|
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('
|
|
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 || '
|
|
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,
|
package/lib/image-mcp/server.js
CHANGED
|
@@ -94,7 +94,7 @@ const imageInputProperties = {
|
|
|
94
94
|
},
|
|
95
95
|
input_fidelity: {
|
|
96
96
|
type: 'string',
|
|
97
|
-
description: '兼容字段。当前
|
|
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: `推荐常规使用。创建本地后台
|
|
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: '查询本地
|
|
139
|
+
description: '查询本地 GPTEAM Image 2 图片任务状态。',
|
|
140
140
|
inputSchema: jobIDSchema()
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
name: 'cancel_image_job',
|
|
144
|
-
description: '取消仍在 queued 或 running 的本地
|
|
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: '返回
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|