gpteam 0.1.28 → 0.1.30
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 +8 -8
- package/bin/gpteam-image-mcp.js +0 -0
- package/lib/bench.js +3 -1
- package/lib/cli.js +77 -41
- package/lib/client-install.js +14 -0
- package/lib/config.js +262 -31
- package/lib/help.js +5 -9
- package/lib/image-mcp/errors.js +1 -1
- package/lib/image-mcp/files.js +2 -2
- package/lib/image-mcp/image.js +12 -12
- package/lib/image-mcp/server.js +9 -9
- package/lib/models.js +137 -17
- package/lib/nodes.js +35 -9
- 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,
|
|
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
|
-
Production endpoints are
|
|
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/bin/gpteam-image-mcp.js
CHANGED
|
File without changes
|
package/lib/bench.js
CHANGED
|
@@ -2,6 +2,7 @@ import dns from 'node:dns';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { formatNetworkError } from './errors.js';
|
|
5
|
+
import { nodeAPIBaseUrl } from './nodes.js';
|
|
5
6
|
import { inspectSSEBody } from './sse.js';
|
|
6
7
|
|
|
7
8
|
const MISSING_LATENCY_MS = 999999;
|
|
@@ -29,7 +30,7 @@ export async function benchmarkNodes(nodes, options) {
|
|
|
29
30
|
|
|
30
31
|
export async function benchmarkNode(node, options) {
|
|
31
32
|
const health = await measureHealth(node.healthUrl);
|
|
32
|
-
const stream = await measureStream(node
|
|
33
|
+
const stream = await measureStream(nodeAPIBaseUrl(node), options);
|
|
33
34
|
return {
|
|
34
35
|
ok: health.ok && stream.ok,
|
|
35
36
|
health,
|
|
@@ -120,6 +121,7 @@ function measureStream(baseUrl, options) {
|
|
|
120
121
|
const payload = JSON.stringify({
|
|
121
122
|
model: options.model,
|
|
122
123
|
stream: true,
|
|
124
|
+
instructions: 'You are a helpful coding assistant.',
|
|
123
125
|
input: options.prompt || '请只回复一句话:节点测速完成。',
|
|
124
126
|
max_output_tokens: options.maxOutputTokens || 648,
|
|
125
127
|
reasoning: options.effort ? { effort: options.effort } : undefined,
|
package/lib/cli.js
CHANGED
|
@@ -4,8 +4,8 @@ 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 {
|
|
8
|
-
import { INGRESS_NODES, nodeMatchesID } from './nodes.js';
|
|
7
|
+
import { DEFAULT_OPENAI_MODEL_ID, validateApiKey } from './models.js';
|
|
8
|
+
import { INGRESS_NODES, nodeMatchesID, nodesFromCapabilities } from './nodes.js';
|
|
9
9
|
import { createTheme, stripAnsi } from './terminal.js';
|
|
10
10
|
|
|
11
11
|
export async function runCli(argv = []) {
|
|
@@ -25,27 +25,31 @@ export async function runCli(argv = []) {
|
|
|
25
25
|
printBanner(theme);
|
|
26
26
|
|
|
27
27
|
const apiKey = args.key || args.apiKey || await askRequired(rl, '请输入 API key:');
|
|
28
|
-
printStep(theme, 1, 5, '校验 API key', '
|
|
28
|
+
printStep(theme, 1, 5, '校验 API key', '读取 Key 分组能力,校验通过后才继续。');
|
|
29
29
|
const validation = await validateApiKey(INGRESS_NODES, apiKey);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const ingressNodes = nodesFromCapabilities(validation);
|
|
31
|
+
printStatus(theme, '通过', formatValidationSummary(validation));
|
|
32
|
+
|
|
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);
|
|
39
|
+
const client = await choose(rl, '请选择客户端类型', clientChoices, args.client, theme);
|
|
34
40
|
await ensureSelectedClientInstalled(client.id, args, rl, theme);
|
|
35
41
|
const models = validation.models;
|
|
36
|
-
const model =
|
|
37
|
-
const contextLength =
|
|
38
|
-
const
|
|
39
|
-
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);
|
|
40
45
|
|
|
41
46
|
printStep(theme, 3, 5, '真实请求测速', 'GET /api/health + POST /v1/responses stream=true');
|
|
42
47
|
printHint(theme, '入口之间并行测速,单入口多轮顺序执行,避免同时打出过多真实请求。');
|
|
43
48
|
printHint(theme, '综合推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好;完成最快会单独标出。');
|
|
44
|
-
printHint(theme,
|
|
45
|
-
const results = await benchmarkNodes(
|
|
49
|
+
printHint(theme, `测速模型:${model.id}`);
|
|
50
|
+
const results = await benchmarkNodes(ingressNodes, {
|
|
46
51
|
apiKey,
|
|
47
52
|
model: model.id,
|
|
48
|
-
effort: effort.id,
|
|
49
53
|
maxOutputTokens,
|
|
50
54
|
rounds: Number(args.rounds || 3)
|
|
51
55
|
});
|
|
@@ -60,10 +64,12 @@ export async function runCli(argv = []) {
|
|
|
60
64
|
const written = writeClientConfig(client.id, {
|
|
61
65
|
apiKey,
|
|
62
66
|
model: model.id,
|
|
63
|
-
effort: effort.id,
|
|
64
67
|
contextLength,
|
|
65
68
|
maxOutputTokens: model.maxOutputTokens,
|
|
66
|
-
|
|
69
|
+
models,
|
|
70
|
+
platform,
|
|
71
|
+
node: selectedNode,
|
|
72
|
+
imageMCP: validation.imageMCP
|
|
67
73
|
});
|
|
68
74
|
|
|
69
75
|
console.log('');
|
|
@@ -71,8 +77,8 @@ export async function runCli(argv = []) {
|
|
|
71
77
|
for (const filePath of written) console.log(`- ${filePath}`);
|
|
72
78
|
console.log(`入口:${formatNodeLabel(selectedNode)}`);
|
|
73
79
|
console.log(`地址:${selectedNode.baseUrl}`);
|
|
74
|
-
if (['codex', 'opencode', 'claude-code'].includes(client.id)) {
|
|
75
|
-
printHint(theme, '已写入
|
|
80
|
+
if (['codex', 'opencode', 'claude-code'].includes(client.id) && validation.imageMCP?.enabled === true) {
|
|
81
|
+
printHint(theme, '已写入 GPTEAM Image MCP。客户端对话里需要生图或图生图时优先调用 create_image_job,再用 get_image_job_status 和 download_image_result 取结果,get_capabilities 可查看支持能力,MCP 使用专用环境变量读取 API key。');
|
|
76
82
|
}
|
|
77
83
|
} finally {
|
|
78
84
|
rl.close();
|
|
@@ -143,23 +149,26 @@ export function resultMarker(item, recommended, fastestTotal) {
|
|
|
143
149
|
return labels.length ? labels.join('/') : '-';
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
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];
|
|
156
164
|
}
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
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))];
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
export async function chooseNode(rl, results, preferred, recommendedID, theme = createTheme()) {
|
|
@@ -212,15 +221,42 @@ function formatScore(value) {
|
|
|
212
221
|
return Number.isFinite(value) ? String(Math.round(value)) : '-';
|
|
213
222
|
}
|
|
214
223
|
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
224
|
+
export function formatModelLabel(model) {
|
|
225
|
+
return String(model?.id || '');
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
export function
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
228
|
+
export function filterClientsForCapabilities(clients, allowedIDs) {
|
|
229
|
+
if (!Array.isArray(allowedIDs) || !allowedIDs.length) return clients;
|
|
230
|
+
const allowed = new Set(allowedIDs.flatMap((id) => {
|
|
231
|
+
const value = String(id);
|
|
232
|
+
return value === 'codex' ? ['codex', 'codex-ws'] : [value];
|
|
233
|
+
}));
|
|
234
|
+
const filtered = clients.filter((client) => allowed.has(client.id));
|
|
235
|
+
return filtered.length ? filtered : clients;
|
|
236
|
+
}
|
|
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
|
+
|
|
255
|
+
function formatValidationSummary(validation) {
|
|
256
|
+
const group = validation.group && validation.group.name
|
|
257
|
+
? `,分组:${validation.group.name}`
|
|
258
|
+
: '';
|
|
259
|
+
return `API key 可用,校验入口:${validation.node.label}${group}`;
|
|
224
260
|
}
|
|
225
261
|
|
|
226
262
|
export function formatNodeLabel(node) {
|
|
@@ -234,7 +270,7 @@ export function assertNodeConfig(node) {
|
|
|
234
270
|
}
|
|
235
271
|
|
|
236
272
|
function printBanner(theme) {
|
|
237
|
-
console.log(theme.brand('
|
|
273
|
+
console.log(theme.brand('GPTEAM API 配置助手'));
|
|
238
274
|
console.log(theme.muted('真实请求测速,自动写入客户端配置,旧配置会先备份。'));
|
|
239
275
|
}
|
|
240
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,15 +74,17 @@ 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'),
|
|
78
|
-
|
|
86
|
+
websocketFeature.join('\n'),
|
|
87
|
+
imageMCPEnabled(settings) ? managedImageMCP.join('\n') : '',
|
|
79
88
|
managedProvider.join('\n')
|
|
80
89
|
]);
|
|
81
90
|
assertNoDuplicateTomlKeys(next);
|
|
@@ -91,33 +100,30 @@ 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)) {
|
|
123
|
+
config.mcp[IMAGE_MCP_ID] = openCodeImageMCPConfig(settings);
|
|
124
|
+
} else {
|
|
125
|
+
delete config.mcp[IMAGE_MCP_ID];
|
|
126
|
+
}
|
|
121
127
|
writeJSON(filePath, config);
|
|
122
128
|
return [filePath];
|
|
123
129
|
}
|
|
@@ -137,21 +143,39 @@ export function writeClaudeCodeConfig(settings) {
|
|
|
137
143
|
ANTHROPIC_MODEL: settings.model,
|
|
138
144
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: settings.model,
|
|
139
145
|
ANTHROPIC_DEFAULT_SONNET_MODEL: settings.model,
|
|
140
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: settings.model
|
|
141
|
-
ANTHROPIC_CUSTOM_HEADERS: `X-Codex-Reasoning-Effort: ${settings.effort}`
|
|
146
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: settings.model
|
|
142
147
|
});
|
|
148
|
+
delete config.env.ANTHROPIC_CUSTOM_HEADERS;
|
|
143
149
|
writeJSON(filePath, config);
|
|
144
150
|
const mcpConfig = readJSON(mcpPath, {});
|
|
145
151
|
mcpConfig.mcpServers = mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object'
|
|
146
152
|
? mcpConfig.mcpServers
|
|
147
153
|
: {};
|
|
148
|
-
|
|
154
|
+
if (imageMCPEnabled(settings)) {
|
|
155
|
+
mcpConfig.mcpServers[IMAGE_MCP_ID] = imageMCPServer(settings);
|
|
156
|
+
} else {
|
|
157
|
+
delete mcpConfig.mcpServers[IMAGE_MCP_ID];
|
|
158
|
+
}
|
|
149
159
|
writeJSON(mcpPath, mcpConfig);
|
|
150
160
|
return [filePath, mcpPath];
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
export const writeClaudeCodeEnv = writeClaudeCodeConfig;
|
|
154
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
|
+
|
|
155
179
|
export function writeOpenClawConfig(settings) {
|
|
156
180
|
if (process.platform === 'win32') {
|
|
157
181
|
throw new Error('OpenClaw 自动配置当前只支持 macOS / Linux');
|
|
@@ -215,6 +239,185 @@ function claudeBaseUrl(baseUrl) {
|
|
|
215
239
|
return `${root}/anthropic`;
|
|
216
240
|
}
|
|
217
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
|
+
|
|
218
421
|
function codexImageMCPCommand() {
|
|
219
422
|
if (process.platform === 'win32') {
|
|
220
423
|
return {
|
|
@@ -238,10 +441,34 @@ function imageMCPServer(settings) {
|
|
|
238
441
|
}
|
|
239
442
|
|
|
240
443
|
function imageMCPEnv(settings) {
|
|
241
|
-
|
|
444
|
+
const env = {
|
|
242
445
|
GPTEAM_API_KEY: String(settings.apiKey || ''),
|
|
243
446
|
GPTEAM_BASE_URL: String(settings.node && settings.node.baseUrl ? settings.node.baseUrl : '')
|
|
244
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);
|
|
245
472
|
}
|
|
246
473
|
|
|
247
474
|
function openCodeImageMCPConfig(settings) {
|
|
@@ -254,6 +481,10 @@ function openCodeImageMCPConfig(settings) {
|
|
|
254
481
|
};
|
|
255
482
|
}
|
|
256
483
|
|
|
484
|
+
function imageMCPEnabled(settings) {
|
|
485
|
+
return settings?.imageMCP?.enabled === true;
|
|
486
|
+
}
|
|
487
|
+
|
|
257
488
|
function stripCodexManagedConfig(raw) {
|
|
258
489
|
const rootLines = [];
|
|
259
490
|
const rest = [];
|
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.30';
|
|
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
|
-
' --
|
|
16
|
-
' --context <tokens> 预设可配置上下文长度',
|
|
17
|
-
' --effort <level> 按模型支持项预选,常见为 none / low / medium / high / xhigh',
|
|
18
|
-
' --node <id> main / jp / hk(兼容旧参数 jp-direct / jp-split / hk-split)',
|
|
14
|
+
' --client <id> codex / codex-ws / claude-code / opencode / gemini',
|
|
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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from './errors.js';
|
|
11
11
|
import { DEFAULT_IMAGE_FORMAT, localImageToDataURL, normalizeImageFormat, writeImageOutput } from './files.js';
|
|
12
12
|
|
|
13
|
-
export const DEFAULT_BASE_URL = 'https://api.gpteamservices.com
|
|
13
|
+
export const DEFAULT_BASE_URL = 'https://api.gpteamservices.com';
|
|
14
14
|
export const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
|
|
15
15
|
export { DEFAULT_IMAGE_FORMAT };
|
|
16
16
|
|
|
@@ -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,
|
|
@@ -651,7 +651,7 @@ function normalizeIdempotencyKey(value) {
|
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
function shapeDownloadResult(result, input = {}) {
|
|
654
|
-
const includeImage =
|
|
654
|
+
const includeImage = input.include_image === true && !input.metadata_only;
|
|
655
655
|
const includeRevisedPrompt = input.include_revised_prompt !== false;
|
|
656
656
|
const output = {
|
|
657
657
|
...result,
|
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: {},
|
|
@@ -238,13 +238,13 @@ function downloadSchema() {
|
|
|
238
238
|
const schema = jobIDSchema();
|
|
239
239
|
schema.properties.metadata_only = {
|
|
240
240
|
type: 'boolean',
|
|
241
|
-
description: '
|
|
242
|
-
default:
|
|
241
|
+
description: '只返回文件路径和元数据,不返回 MCP 图片内容。默认 true,避免大图进入上下文触发频繁 compact。',
|
|
242
|
+
default: true
|
|
243
243
|
};
|
|
244
244
|
schema.properties.include_image = {
|
|
245
245
|
type: 'boolean',
|
|
246
|
-
description: '
|
|
247
|
-
default:
|
|
246
|
+
description: '显式返回 MCP 图片内容。大图会显著增加上下文,通常保持 false,只使用本地文件路径。',
|
|
247
|
+
default: false
|
|
248
248
|
};
|
|
249
249
|
schema.properties.include_revised_prompt = {
|
|
250
250
|
type: 'boolean',
|
package/lib/models.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { formatNetworkError } from './errors.js';
|
|
2
|
+
import { nodeAPIBaseUrl } from './nodes.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_OPENAI_MODEL_ID = 'gpt-5.5';
|
|
5
|
+
export const DEFAULT_OPENAI_SMALL_MODEL_ID = 'gpt-5.4-mini';
|
|
2
6
|
|
|
3
7
|
export const FALLBACK_MODELS = {
|
|
4
|
-
'gpt-5.2': {
|
|
5
|
-
id: 'gpt-5.2',
|
|
6
|
-
contextLength: 400000,
|
|
7
|
-
maxOutputTokens: 128000,
|
|
8
|
-
efforts: ['none', 'low', 'medium', 'high', 'xhigh']
|
|
9
|
-
},
|
|
10
8
|
'gpt-5.3-codex': {
|
|
11
9
|
id: 'gpt-5.3-codex',
|
|
12
10
|
contextLength: 400000,
|
|
@@ -39,26 +37,63 @@ export const FALLBACK_MODELS = {
|
|
|
39
37
|
}
|
|
40
38
|
};
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
class CapabilityEndpointUnavailableError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'CapabilityEndpointUnavailableError';
|
|
44
|
+
this.capabilityEndpointUnavailable = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeModels(payload, options = {}) {
|
|
43
49
|
const items = Array.isArray(payload && payload.data) ? payload.data : [];
|
|
50
|
+
return normalizeModelItems(items, { fallbackWhenEmpty: options.fallbackWhenEmpty !== false });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeCapabilities(payload) {
|
|
54
|
+
const models = payload && typeof payload === 'object' ? payload.models : {};
|
|
55
|
+
const chatItems = firstArray(models?.chat, payload?.chat_models, payload?.models_chat);
|
|
56
|
+
const imageItems = firstArray(models?.image, payload?.image_models, payload?.models_image);
|
|
57
|
+
return {
|
|
58
|
+
apiKey: normalizePlainObject(payload?.api_key),
|
|
59
|
+
user: normalizePlainObject(payload?.user),
|
|
60
|
+
group: normalizePlainObject(payload?.group),
|
|
61
|
+
protocols: normalizePlainObject(payload?.protocols),
|
|
62
|
+
defaultModel: String(payload?.default_model || payload?.defaultModel || models?.default_model || models?.defaultModel || '').trim(),
|
|
63
|
+
baseUrls: firstArray(payload?.base_urls, payload?.baseURLs),
|
|
64
|
+
clients: firstArray(payload?.clients),
|
|
65
|
+
models: normalizeModelItems(chatItems, { fallbackWhenEmpty: false }),
|
|
66
|
+
imageModels: normalizeModelItems(imageItems, {
|
|
67
|
+
fallbackWhenEmpty: false,
|
|
68
|
+
includeImageModels: true
|
|
69
|
+
}),
|
|
70
|
+
imageMCP: normalizePlainObject(payload?.image_mcp)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeModelItems(items, options = {}) {
|
|
44
75
|
const result = new Map();
|
|
45
76
|
|
|
46
77
|
for (const item of items) {
|
|
47
78
|
const id = String(item.id || item.name || '').trim();
|
|
48
|
-
if (!
|
|
79
|
+
if (!isConfigurableTextModel(id, item, options)) continue;
|
|
49
80
|
const fallback = FALLBACK_MODELS[id] || {};
|
|
50
81
|
const thinking = item.thinking && typeof item.thinking === 'object' ? item.thinking : {};
|
|
51
82
|
const levels = Array.isArray(thinking.levels) ? thinking.levels : fallback.efforts;
|
|
83
|
+
const defaultEffort = String(thinking.default || '').trim().toLowerCase();
|
|
84
|
+
const efforts = normalizeEfforts(levels);
|
|
52
85
|
result.set(id, {
|
|
53
86
|
id,
|
|
54
87
|
displayName: item.display_name || item.name || id,
|
|
55
88
|
contextLength: Number(item.context_length || item.inputTokenLimit || fallback.contextLength || 400000),
|
|
56
89
|
maxOutputTokens: Number(item.max_completion_tokens || item.outputTokenLimit || fallback.maxOutputTokens || 128000),
|
|
57
|
-
efforts:
|
|
90
|
+
efforts: defaultEffort && efforts.includes(defaultEffort)
|
|
91
|
+
? [defaultEffort, ...efforts.filter((effort) => effort !== defaultEffort)]
|
|
92
|
+
: efforts
|
|
58
93
|
});
|
|
59
94
|
}
|
|
60
95
|
|
|
61
|
-
if (!result.size) {
|
|
96
|
+
if (!result.size && options.fallbackWhenEmpty !== false) {
|
|
62
97
|
for (const model of Object.values(FALLBACK_MODELS)) {
|
|
63
98
|
result.set(model.id, {
|
|
64
99
|
id: model.id,
|
|
@@ -73,11 +108,60 @@ export function normalizeModels(payload) {
|
|
|
73
108
|
return Array.from(result.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
74
109
|
}
|
|
75
110
|
|
|
76
|
-
function
|
|
77
|
-
if (!id
|
|
111
|
+
function isConfigurableTextModel(id, item, options = {}) {
|
|
112
|
+
if (!id) return false;
|
|
113
|
+
if (!options.includeImageModels && id.toLowerCase().includes('image')) return false;
|
|
78
114
|
const owner = String(item?.owned_by || item?.provider || item?.type || '').trim().toLowerCase();
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
const knownTextPrefix = /^(gpt-|claude-|gemini-|doubao-|deepseek-|kimi-|qwen|glm-|moonshot-|yi-|baichuan-|step-|minimax-)/.test(id);
|
|
116
|
+
if (!owner) return knownTextPrefix || Object.prototype.hasOwnProperty.call(FALLBACK_MODELS, id);
|
|
117
|
+
return [
|
|
118
|
+
'openai',
|
|
119
|
+
'codex',
|
|
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',
|
|
136
|
+
'anthropic',
|
|
137
|
+
'claude',
|
|
138
|
+
'gemini',
|
|
139
|
+
'google',
|
|
140
|
+
'antigravity'
|
|
141
|
+
].includes(owner);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function fetchCapabilities(baseUrl, apiKey, options = {}) {
|
|
145
|
+
let response;
|
|
146
|
+
try {
|
|
147
|
+
response = await fetch(`${baseUrl.replace(/\/$/, '')}/gpteam/config-capabilities`, {
|
|
148
|
+
signal: makeTimeoutSignal(options.timeoutMs || 15000),
|
|
149
|
+
headers: {
|
|
150
|
+
Authorization: `Bearer ${apiKey}`,
|
|
151
|
+
'User-Agent': 'gpteam-api-config/0.1'
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`/v1/gpteam/config-capabilities 请求失败:${formatNetworkError(error)}`);
|
|
156
|
+
}
|
|
157
|
+
if (response.status === 404 || response.status === 405) {
|
|
158
|
+
throw new CapabilityEndpointUnavailableError('/v1/gpteam/config-capabilities 未部署');
|
|
159
|
+
}
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const detail = await readResponseError(response);
|
|
162
|
+
throw new Error(`/v1/gpteam/config-capabilities 返回 HTTP ${response.status}${detail ? `:${detail}` : ''}`);
|
|
163
|
+
}
|
|
164
|
+
return normalizeCapabilities(await response.json());
|
|
81
165
|
}
|
|
82
166
|
|
|
83
167
|
export async function fetchModels(baseUrl, apiKey, options = {}) {
|
|
@@ -108,13 +192,38 @@ export async function validateApiKey(nodes, apiKey) {
|
|
|
108
192
|
|
|
109
193
|
const results = await Promise.all(candidates.map(async (node) => {
|
|
110
194
|
try {
|
|
111
|
-
|
|
195
|
+
const baseUrl = nodeAPIBaseUrl(node);
|
|
196
|
+
try {
|
|
197
|
+
const capabilities = await fetchCapabilities(baseUrl, apiKey);
|
|
198
|
+
if (!capabilities.models.length) {
|
|
199
|
+
throw new Error('/v1/gpteam/config-capabilities 没有返回可配置模型');
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
node,
|
|
204
|
+
...capabilities,
|
|
205
|
+
capabilities
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (!error || error.capabilityEndpointUnavailable !== true) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
node,
|
|
215
|
+
models: await fetchModels(baseUrl, apiKey),
|
|
216
|
+
imageMCP: { enabled: false }
|
|
217
|
+
};
|
|
112
218
|
} catch (error) {
|
|
113
219
|
return { ok: false, node, error };
|
|
114
220
|
}
|
|
115
221
|
}));
|
|
116
|
-
const success = results.find((item) => item.ok);
|
|
117
|
-
if (success)
|
|
222
|
+
const success = results.find((item) => item.ok && item.capabilities) || results.find((item) => item.ok);
|
|
223
|
+
if (success) {
|
|
224
|
+
const { ok, ...result } = success;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
118
227
|
|
|
119
228
|
const detail = results
|
|
120
229
|
.map((item) => `${item.node.label || item.node.id || item.node.baseUrl}: ${item.error && item.error.message ? item.error.message : item.error}`)
|
|
@@ -126,6 +235,17 @@ export function modelByID(models, id) {
|
|
|
126
235
|
return models.find((model) => model.id === id) || models[0];
|
|
127
236
|
}
|
|
128
237
|
|
|
238
|
+
function firstArray(...values) {
|
|
239
|
+
for (const value of values) {
|
|
240
|
+
if (Array.isArray(value)) return value;
|
|
241
|
+
}
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizePlainObject(value) {
|
|
246
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
247
|
+
}
|
|
248
|
+
|
|
129
249
|
function normalizeEfforts(levels) {
|
|
130
250
|
const out = [];
|
|
131
251
|
for (const level of levels || []) {
|
package/lib/nodes.js
CHANGED
|
@@ -3,22 +3,15 @@ export const INGRESS_NODES = [
|
|
|
3
3
|
id: 'main',
|
|
4
4
|
aliases: ['jp-direct'],
|
|
5
5
|
label: '主入口',
|
|
6
|
-
baseUrl: 'https://api.gpteamservices.com
|
|
6
|
+
baseUrl: 'https://api.gpteamservices.com',
|
|
7
7
|
healthUrl: 'https://api.gpteamservices.com/api/health'
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
id: 'jp',
|
|
11
11
|
aliases: ['jp-split'],
|
|
12
12
|
label: '日本入口',
|
|
13
|
-
baseUrl: 'https://api-jp.gpteamservices.com
|
|
13
|
+
baseUrl: 'https://api-jp.gpteamservices.com',
|
|
14
14
|
healthUrl: 'https://api-jp.gpteamservices.com/api/health'
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
id: 'hk',
|
|
18
|
-
aliases: ['hk-split'],
|
|
19
|
-
label: '香港入口',
|
|
20
|
-
baseUrl: 'https://api-hk.gpteamservices.com/v1',
|
|
21
|
-
healthUrl: 'https://api-hk.gpteamservices.com/api/health'
|
|
22
15
|
}
|
|
23
16
|
];
|
|
24
17
|
|
|
@@ -31,3 +24,36 @@ export function nodeMatchesID(node, id) {
|
|
|
31
24
|
export function endpointRoot(baseUrl) {
|
|
32
25
|
return String(baseUrl || '').replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
|
33
26
|
}
|
|
27
|
+
|
|
28
|
+
export function endpointAPIBase(baseUrl) {
|
|
29
|
+
const root = endpointRoot(baseUrl);
|
|
30
|
+
return root ? `${root}/v1` : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function nodeAPIBaseUrl(node) {
|
|
34
|
+
if (!node) return '';
|
|
35
|
+
return endpointAPIBase(node.apiBaseUrl || node.baseUrl);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function nodesFromCapabilities(capabilities, fallbackNodes = INGRESS_NODES) {
|
|
39
|
+
const entries = Array.isArray(capabilities?.baseUrls) ? capabilities.baseUrls : [];
|
|
40
|
+
const nodes = entries
|
|
41
|
+
.map((entry) => capabilityBaseURLToNode(entry))
|
|
42
|
+
.filter((node) => node && node.baseUrl);
|
|
43
|
+
return nodes.length ? nodes : fallbackNodes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function capabilityBaseURLToNode(entry) {
|
|
47
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
48
|
+
const id = String(entry.id || '').trim();
|
|
49
|
+
const baseUrl = endpointRoot(entry.url || entry.base_url || entry.baseUrl);
|
|
50
|
+
if (!id || !baseUrl) return null;
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
aliases: Array.isArray(entry.aliases) ? entry.aliases.map((item) => String(item)) : [],
|
|
54
|
+
label: String(entry.label || id),
|
|
55
|
+
baseUrl,
|
|
56
|
+
healthUrl: String(entry.health_url || entry.healthUrl || `${baseUrl}/api/health`),
|
|
57
|
+
recommended: Boolean(entry.recommended)
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gpteam",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.30",
|
|
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",
|