gpteam 0.1.8 → 0.1.11
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 +9 -3
- package/lib/cli.js +51 -4
- package/lib/client-install.js +100 -0
- package/lib/config.js +66 -29
- package/lib/help.js +3 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -6,11 +6,15 @@ Interactive GPTeam API client configurator.
|
|
|
6
6
|
npx gpteam
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
The CLI asks for an API key, validates it with `/v1/models`, detects available models, benchmarks all production ingress endpoints with real API requests, then backs up old files and writes the selected client configuration. The next steps are blocked until the key validation succeeds.
|
|
9
|
+
The CLI asks for an API key, validates it with `/v1/models`, checks whether the selected client command is installed, detects available models, benchmarks all production ingress endpoints with real API requests, then backs up old files and writes the selected client configuration. The next steps are blocked until the key validation succeeds. Client detection runs before model selection and benchmarking, so a missing local client does not waste API probes.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
+
|
|
15
|
+
Client config writing follows the same safety pattern as cc-switch: keep Codex top-level fields separate from provider tables, preserve unrelated sections such as MCP servers, merge OpenCode/OpenClaw providers additively, and stop before writing when an existing JSON/JSON5 config cannot be parsed. GPTeam keeps two proxy-specific extensions on top of that baseline: Codex explicitly disables WebSocket prewarm, and Claude Code writes the reasoning-effort header through `ANTHROPIC_CUSTOM_HEADERS`.
|
|
16
|
+
|
|
17
|
+
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.
|
|
14
18
|
|
|
15
19
|
Supported clients:
|
|
16
20
|
|
|
@@ -25,3 +29,5 @@ Useful non-interactive smoke checks:
|
|
|
25
29
|
npx gpteam --help
|
|
26
30
|
npx gpteam --version
|
|
27
31
|
```
|
|
32
|
+
|
|
33
|
+
`--install-client` is for scripted setup after you also provide the usual non-interactive inputs such as `--api-key`, `--client`, `--model`, and `--node`.
|
package/lib/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
3
|
import { benchmarkNodes, formatMs } from './bench.js';
|
|
4
|
+
import { ensureClientInstalled, formatInstallCommand } from './client-install.js';
|
|
4
5
|
import { CLIENTS, writeClientConfig } from './config.js';
|
|
5
6
|
import { getHelpText, PACKAGE_NAME, PACKAGE_VERSION } from './help.js';
|
|
6
7
|
import { modelByID, validateApiKey } from './models.js';
|
|
@@ -30,6 +31,7 @@ export async function runCli(argv = []) {
|
|
|
30
31
|
|
|
31
32
|
printStep(theme, 2, 5, '选择客户端和模型');
|
|
32
33
|
const client = await choose(rl, '请选择客户端类型', CLIENTS, args.client, theme);
|
|
34
|
+
await ensureSelectedClientInstalled(client.id, args, rl, theme);
|
|
33
35
|
const models = validation.models;
|
|
34
36
|
const model = await chooseModel(rl, models, args.model, theme);
|
|
35
37
|
const contextLength = await askContextLength(rl, model, args.context);
|
|
@@ -38,7 +40,7 @@ export async function runCli(argv = []) {
|
|
|
38
40
|
|
|
39
41
|
printStep(theme, 3, 5, '真实请求测速', 'GET /api/health + POST /v1/responses stream=true');
|
|
40
42
|
printHint(theme, '入口之间并行测速,单入口多轮顺序执行,避免同时打出过多真实请求。');
|
|
41
|
-
printHint(theme, '
|
|
43
|
+
printHint(theme, '综合推荐规则:成功率优先,其次按首包、完成、尾延迟和健康检查计算体验分,分数越低越好;完成最快会单独标出。');
|
|
42
44
|
printHint(theme, `模型:${model.id},测速输出上限:${maxOutputTokens}`);
|
|
43
45
|
const results = await benchmarkNodes(INGRESS_NODES, {
|
|
44
46
|
apiKey,
|
|
@@ -97,6 +99,7 @@ export function parseArgs(argv) {
|
|
|
97
99
|
|
|
98
100
|
export function printResults(results, theme = createTheme()) {
|
|
99
101
|
const recommended = results.find((item) => item.successRate > 0);
|
|
102
|
+
const fastestTotal = findFastestTotalResult(results);
|
|
100
103
|
const rows = results.map((item) => [
|
|
101
104
|
`${item.node.label}(${describeSplit(item.node)})`,
|
|
102
105
|
formatMs(item.dnsMs),
|
|
@@ -108,10 +111,10 @@ export function printResults(results, theme = createTheme()) {
|
|
|
108
111
|
`${Math.round(item.successRate * 100)}%`,
|
|
109
112
|
formatScore(item.experienceScore),
|
|
110
113
|
formatMs(item.healthMs),
|
|
111
|
-
item
|
|
114
|
+
resultMarker(item, recommended, fastestTotal),
|
|
112
115
|
item.error || '-'
|
|
113
116
|
]);
|
|
114
|
-
const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '尾延迟', '成功率', '体验分', '健康检查', '
|
|
117
|
+
const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '尾延迟', '成功率', '体验分', '健康检查', '标记', '错误'];
|
|
115
118
|
const widths = header.map((name, i) => Math.max(displayWidth(name), ...rows.map((row) => displayWidth(row[i]))) + 2);
|
|
116
119
|
console.log('');
|
|
117
120
|
console.log(theme.title(formatRow(header, widths)));
|
|
@@ -119,6 +122,22 @@ export function printResults(results, theme = createTheme()) {
|
|
|
119
122
|
const line = formatRow(row, widths);
|
|
120
123
|
console.log(row[10] === '-' ? line : theme.ok(line));
|
|
121
124
|
}
|
|
125
|
+
if (recommended && fastestTotal && recommended !== fastestTotal) {
|
|
126
|
+
printHint(theme, `综合推荐偏首包和稳定性;如果更看重整段完成速度,可选 ${formatNodeLabel(fastestTotal.node)}。`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function findFastestTotalResult(results) {
|
|
131
|
+
return results
|
|
132
|
+
.filter((item) => item.successRate > 0 && Number.isFinite(item.totalMs))
|
|
133
|
+
.sort((a, b) => a.totalMs - b.totalMs)[0] || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resultMarker(item, recommended, fastestTotal) {
|
|
137
|
+
const labels = [];
|
|
138
|
+
if (item === recommended) labels.push('综合推荐');
|
|
139
|
+
if (item === fastestTotal) labels.push('完成最快');
|
|
140
|
+
return labels.length ? labels.join('/') : '-';
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
async function chooseModel(rl, models, preferred, theme) {
|
|
@@ -145,7 +164,7 @@ export async function chooseNode(rl, results, preferred, recommendedID, theme =
|
|
|
145
164
|
const preferredNode = nodes.find((node) => node.id === preferred);
|
|
146
165
|
if (preferredNode) return preferredNode;
|
|
147
166
|
if (recommendedID) {
|
|
148
|
-
printStatus(theme, '
|
|
167
|
+
printStatus(theme, '综合推荐', recommendedID);
|
|
149
168
|
}
|
|
150
169
|
return (await choose(rl, '请选择最终写入的入口', nodes.map((node) => ({
|
|
151
170
|
id: node.id,
|
|
@@ -234,6 +253,34 @@ function printStatus(theme, label, detail) {
|
|
|
234
253
|
console.log(`${theme.ok(`[${label}]`)} ${detail}`);
|
|
235
254
|
}
|
|
236
255
|
|
|
256
|
+
async function ensureSelectedClientInstalled(clientID, args, rl, theme) {
|
|
257
|
+
const result = await ensureClientInstalled(clientID, {
|
|
258
|
+
autoInstall: cliFlagEnabled(args.installClient) || cliFlagEnabled(args.yes) || cliFlagEnabled(args.y),
|
|
259
|
+
confirmInstall: shouldPromptForInstall() ? (spec) => confirmClientInstall(rl, spec, theme) : undefined
|
|
260
|
+
});
|
|
261
|
+
if (result.status === 'installed') {
|
|
262
|
+
printStatus(theme, '已检测', `${result.spec.label} 命令可用:${result.spec.command}`);
|
|
263
|
+
} else {
|
|
264
|
+
printStatus(theme, '已安装', `${result.spec.label} 安装完成。`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function confirmClientInstall(rl, spec, theme) {
|
|
269
|
+
printStatus(theme, '未检测到', `${spec.label} 命令 ${spec.command}`);
|
|
270
|
+
printHint(theme, `将执行安装命令:${formatInstallCommand(spec)}`);
|
|
271
|
+
printHint(theme, `安装后会继续配置,可用 ${spec.versionHint} 自检版本。`);
|
|
272
|
+
const answer = await rl.question('现在安装吗?输入 y 继续,其他输入停止:');
|
|
273
|
+
return ['y', 'yes'].includes(answer.trim().toLowerCase());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function shouldPromptForInstall() {
|
|
277
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function cliFlagEnabled(value) {
|
|
281
|
+
return value === true || ['1', 'true', 'yes', 'y'].includes(String(value || '').toLowerCase());
|
|
282
|
+
}
|
|
283
|
+
|
|
237
284
|
function toCamel(value) {
|
|
238
285
|
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
239
286
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const CLIENT_INSTALL_SPECS = {
|
|
4
|
+
codex: {
|
|
5
|
+
id: 'codex',
|
|
6
|
+
label: 'Codex',
|
|
7
|
+
command: 'codex',
|
|
8
|
+
installCommand: ['npm', ['i', '-g', '@openai/codex@latest']],
|
|
9
|
+
versionHint: 'codex --version'
|
|
10
|
+
},
|
|
11
|
+
opencode: {
|
|
12
|
+
id: 'opencode',
|
|
13
|
+
label: 'OpenCode',
|
|
14
|
+
command: 'opencode',
|
|
15
|
+
installCommand: ['npm', ['i', '-g', 'opencode-ai@latest']],
|
|
16
|
+
versionHint: 'opencode --version'
|
|
17
|
+
},
|
|
18
|
+
'claude-code': {
|
|
19
|
+
id: 'claude-code',
|
|
20
|
+
label: 'Claude Code',
|
|
21
|
+
command: 'claude',
|
|
22
|
+
installCommand: ['npm', ['i', '-g', '@anthropic-ai/claude-code@latest']],
|
|
23
|
+
versionHint: 'claude --version'
|
|
24
|
+
},
|
|
25
|
+
openclaw: {
|
|
26
|
+
id: 'openclaw',
|
|
27
|
+
label: 'OpenClaw',
|
|
28
|
+
command: 'openclaw',
|
|
29
|
+
installCommand: ['npm', ['i', '-g', 'openclaw@latest']],
|
|
30
|
+
versionHint: 'openclaw --version',
|
|
31
|
+
windowsUnsupported: true
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function resolveClientInstallSpec(clientID) {
|
|
36
|
+
const spec = CLIENT_INSTALL_SPECS[clientID];
|
|
37
|
+
if (!spec) throw new Error(`未知客户端:${clientID}`);
|
|
38
|
+
return spec;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function ensureClientInstalled(clientID, options = {}) {
|
|
42
|
+
const spec = resolveClientInstallSpec(clientID);
|
|
43
|
+
const platform = options.platform || process.platform;
|
|
44
|
+
if (spec.windowsUnsupported && platform === 'win32') {
|
|
45
|
+
throw new Error(`Windows 暂不支持自动安装或自动配置 ${spec.label}。请在 macOS / Linux 环境运行配置。`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const commandExists = options.commandExists || defaultCommandExists;
|
|
49
|
+
if (await commandExists(spec.command)) return { status: 'installed', spec };
|
|
50
|
+
|
|
51
|
+
if (!await shouldInstallMissingClient(spec, options)) {
|
|
52
|
+
throw new Error(`未安装 ${spec.label},已停止配置。安装后重新运行 npx gpteam。`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await runInstallCommand(spec, options.runCommand || defaultRunCommand);
|
|
56
|
+
if (!await commandExists(spec.command)) {
|
|
57
|
+
throw new Error(`安装完成但仍未检测到 ${spec.label} 命令 ${spec.command}。请检查 npm 全局 bin 是否在 PATH 中,然后运行 ${spec.versionHint}。`);
|
|
58
|
+
}
|
|
59
|
+
return { status: 'installed-after-run', spec };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatInstallCommand(spec) {
|
|
63
|
+
return [spec.installCommand[0], ...spec.installCommand[1]].join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function defaultCommandExists(command) {
|
|
67
|
+
const checker = process.platform === 'win32' ? 'where' : 'sh';
|
|
68
|
+
const args = process.platform === 'win32' ? [command] : ['-c', `command -v ${shellQuote(command)}`];
|
|
69
|
+
const result = await defaultRunCommand(checker, args, { stdio: 'ignore' });
|
|
70
|
+
return result.status === 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function shouldInstallMissingClient(spec, options) {
|
|
74
|
+
if (options.autoInstall) return true;
|
|
75
|
+
if (!options.confirmInstall) return false;
|
|
76
|
+
return options.confirmInstall(spec);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runInstallCommand(spec, runCommand) {
|
|
80
|
+
const [command, args] = spec.installCommand;
|
|
81
|
+
const result = await runCommand(command, args, { stdio: 'inherit' });
|
|
82
|
+
if (result.status !== 0) {
|
|
83
|
+
throw new Error(`${spec.label} 安装失败,命令:${formatInstallCommand(spec)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function defaultRunCommand(command, args, options = {}) {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const child = spawn(command, args, {
|
|
90
|
+
stdio: options.stdio || 'ignore',
|
|
91
|
+
shell: options.shell || false
|
|
92
|
+
});
|
|
93
|
+
child.on('error', () => resolve({ status: 127 }));
|
|
94
|
+
child.on('close', (status) => resolve({ status: status ?? 1 }));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shellQuote(value) {
|
|
99
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
100
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import JSON5 from 'json5';
|
|
4
5
|
import { endpointRoot } from './nodes.js';
|
|
5
6
|
|
|
7
|
+
const PROVIDER_ID = 'gpteam';
|
|
8
|
+
|
|
6
9
|
export const CLIENTS = [
|
|
7
10
|
{ id: 'codex', label: 'Codex' },
|
|
8
11
|
{ id: 'opencode', label: 'OpenCode' },
|
|
@@ -13,13 +16,13 @@ export const CLIENTS = [
|
|
|
13
16
|
export function writeClientConfig(clientID, settings) {
|
|
14
17
|
if (clientID === 'codex') return writeCodexConfig(settings);
|
|
15
18
|
if (clientID === 'opencode') return writeOpenCodeConfig(settings);
|
|
16
|
-
if (clientID === 'claude-code') return
|
|
19
|
+
if (clientID === 'claude-code') return writeClaudeCodeConfig(settings);
|
|
17
20
|
if (clientID === 'openclaw') return writeOpenClawConfig(settings);
|
|
18
21
|
throw new Error(`未知客户端:${clientID}`);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function writeCodexConfig(settings) {
|
|
22
|
-
const dir = process.env.CODEX_HOME || path.join(
|
|
25
|
+
const dir = process.env.CODEX_HOME || path.join(homeDir(), '.codex');
|
|
23
26
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
27
|
const configPath = path.join(dir, 'config.toml');
|
|
25
28
|
const authPath = path.join(dir, 'auth.json');
|
|
@@ -56,17 +59,19 @@ export function writeCodexConfig(settings) {
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
export function writeOpenCodeConfig(settings) {
|
|
59
|
-
const dir = path.join(
|
|
62
|
+
const dir = path.join(homeDir(), '.config', 'opencode');
|
|
60
63
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
64
|
const filePath = path.join(dir, 'opencode.json');
|
|
62
65
|
backupIfExists(filePath);
|
|
63
66
|
const config = readJSON(filePath, { $schema: 'https://opencode.ai/config.json' });
|
|
64
67
|
config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
|
|
65
|
-
config.provider
|
|
68
|
+
config.provider[PROVIDER_ID] = {
|
|
66
69
|
npm: '@ai-sdk/openai',
|
|
70
|
+
name: 'GPTeam',
|
|
67
71
|
options: {
|
|
68
72
|
apiKey: settings.apiKey,
|
|
69
|
-
baseURL: settings.node.baseUrl
|
|
73
|
+
baseURL: settings.node.baseUrl,
|
|
74
|
+
setCacheKey: true
|
|
70
75
|
},
|
|
71
76
|
models: {
|
|
72
77
|
[settings.model]: {
|
|
@@ -78,7 +83,7 @@ export function writeOpenCodeConfig(settings) {
|
|
|
78
83
|
variants: {
|
|
79
84
|
[settings.effort]: {
|
|
80
85
|
reasoningEffort: settings.effort,
|
|
81
|
-
reasoningSummary:
|
|
86
|
+
reasoningSummary: 'auto',
|
|
82
87
|
textVerbosity: 'medium'
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -89,30 +94,33 @@ export function writeOpenCodeConfig(settings) {
|
|
|
89
94
|
return [filePath];
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
export function
|
|
93
|
-
const dir = path.join(
|
|
97
|
+
export function writeClaudeCodeConfig(settings) {
|
|
98
|
+
const dir = path.join(homeDir(), '.claude');
|
|
94
99
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
95
|
-
const filePath =
|
|
100
|
+
const filePath = claudeSettingsPath(dir);
|
|
96
101
|
backupIfExists(filePath);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
const config = readJSON(filePath, {});
|
|
103
|
+
config.env = config.env && typeof config.env === 'object' ? config.env : {};
|
|
104
|
+
Object.assign(config.env, {
|
|
105
|
+
ANTHROPIC_AUTH_TOKEN: settings.apiKey,
|
|
106
|
+
ANTHROPIC_BASE_URL: claudeBaseUrl(settings.node.baseUrl),
|
|
107
|
+
ANTHROPIC_MODEL: settings.model,
|
|
108
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: settings.model,
|
|
109
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: settings.model,
|
|
110
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: settings.model,
|
|
111
|
+
ANTHROPIC_CUSTOM_HEADERS: `X-Codex-Reasoning-Effort: ${settings.effort}`
|
|
112
|
+
});
|
|
113
|
+
writeJSON(filePath, config);
|
|
108
114
|
return [filePath];
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
export const writeClaudeCodeEnv = writeClaudeCodeConfig;
|
|
118
|
+
|
|
111
119
|
export function writeOpenClawConfig(settings) {
|
|
112
120
|
if (process.platform === 'win32') {
|
|
113
121
|
throw new Error('OpenClaw 自动配置当前只支持 macOS / Linux');
|
|
114
122
|
}
|
|
115
|
-
const dir = path.join(
|
|
123
|
+
const dir = path.join(homeDir(), '.openclaw');
|
|
116
124
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
117
125
|
const filePath = path.join(dir, 'openclaw.json');
|
|
118
126
|
backupIfExists(filePath);
|
|
@@ -122,7 +130,7 @@ export function writeOpenClawConfig(settings) {
|
|
|
122
130
|
config.models.providers = config.models.providers && typeof config.models.providers === 'object'
|
|
123
131
|
? config.models.providers
|
|
124
132
|
: {};
|
|
125
|
-
config.models.providers
|
|
133
|
+
config.models.providers[PROVIDER_ID] = {
|
|
126
134
|
baseUrl: settings.node.baseUrl,
|
|
127
135
|
apiKey: settings.apiKey,
|
|
128
136
|
api: 'openai-responses',
|
|
@@ -134,10 +142,43 @@ export function writeOpenClawConfig(settings) {
|
|
|
134
142
|
maxTokens: Number(settings.maxOutputTokens)
|
|
135
143
|
}]
|
|
136
144
|
};
|
|
145
|
+
config.agents = config.agents && typeof config.agents === 'object' ? config.agents : {};
|
|
146
|
+
config.agents.defaults = config.agents.defaults && typeof config.agents.defaults === 'object'
|
|
147
|
+
? config.agents.defaults
|
|
148
|
+
: {};
|
|
149
|
+
const qualifiedModel = `${PROVIDER_ID}/${settings.model}`;
|
|
150
|
+
config.agents.defaults.model = {
|
|
151
|
+
primary: qualifiedModel,
|
|
152
|
+
fallbacks: []
|
|
153
|
+
};
|
|
154
|
+
config.agents.defaults.models = config.agents.defaults.models && typeof config.agents.defaults.models === 'object'
|
|
155
|
+
? config.agents.defaults.models
|
|
156
|
+
: {};
|
|
157
|
+
config.agents.defaults.models[qualifiedModel] = {
|
|
158
|
+
alias: settings.model
|
|
159
|
+
};
|
|
137
160
|
writeJSON(filePath, config);
|
|
138
161
|
return [filePath];
|
|
139
162
|
}
|
|
140
163
|
|
|
164
|
+
function homeDir() {
|
|
165
|
+
return process.env.GPTEAM_CONFIG_TEST_HOME || os.homedir();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function claudeSettingsPath(dir) {
|
|
169
|
+
const settings = path.join(dir, 'settings.json');
|
|
170
|
+
if (fs.existsSync(settings)) return settings;
|
|
171
|
+
const legacy = path.join(dir, 'claude.json');
|
|
172
|
+
if (fs.existsSync(legacy)) return legacy;
|
|
173
|
+
return settings;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function claudeBaseUrl(baseUrl) {
|
|
177
|
+
const root = endpointRoot(baseUrl);
|
|
178
|
+
if (/\/anthropic\/?$/.test(root)) return root.replace(/\/$/, '');
|
|
179
|
+
return `${root}/anthropic`;
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
function stripCodexManagedConfig(raw) {
|
|
142
183
|
const rootLines = [];
|
|
143
184
|
const rest = [];
|
|
@@ -236,9 +277,9 @@ function writeTextAtomic(filePath, text, mode) {
|
|
|
236
277
|
function readJSON(filePath, fallback) {
|
|
237
278
|
if (!fs.existsSync(filePath)) return fallback;
|
|
238
279
|
try {
|
|
239
|
-
return
|
|
240
|
-
} catch {
|
|
241
|
-
|
|
280
|
+
return JSON5.parse(fs.readFileSync(filePath, 'utf8'));
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(`无法解析配置文件 ${filePath}:${error.message}。为避免覆盖旧配置,已停止写入`);
|
|
242
283
|
}
|
|
243
284
|
}
|
|
244
285
|
|
|
@@ -249,7 +290,3 @@ function writeJSON(filePath, value) {
|
|
|
249
290
|
function tomlString(value) {
|
|
250
291
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
251
292
|
}
|
|
252
|
-
|
|
253
|
-
function shellQuote(value) {
|
|
254
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
255
|
-
}
|
package/lib/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const PACKAGE_NAME = 'gpteam';
|
|
2
|
-
export const PACKAGE_VERSION = '0.1.
|
|
2
|
+
export const PACKAGE_VERSION = '0.1.11';
|
|
3
3
|
|
|
4
4
|
export function getHelpText() {
|
|
5
5
|
return [
|
|
@@ -18,9 +18,10 @@ export function getHelpText() {
|
|
|
18
18
|
' --node <id> jp-direct / jp-split / hk-split / us-split',
|
|
19
19
|
' --rounds <n> 每个入口测速轮数,默认 3',
|
|
20
20
|
' --max-output-tokens <n> 测速输出上限,默认 648',
|
|
21
|
+
' --install-client 客户端命令缺失时直接安装,不再二次确认',
|
|
21
22
|
' --help 显示帮助',
|
|
22
23
|
' --version 显示版本',
|
|
23
24
|
'',
|
|
24
|
-
'说明:输入 key 后会先请求 /v1/models
|
|
25
|
+
'说明:输入 key 后会先请求 /v1/models 校验,通过后才继续。选好客户端后会检测本机命令是否可用,缺失时提示安装;Windows 暂不支持自动配置 OpenClaw。测速会请求 GET /api/health 和流式 POST /v1/responses。入口之间并行测速,写新配置前会先备份旧配置。'
|
|
25
26
|
].join('\n');
|
|
26
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gpteam",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "node --test test/*.test.mjs"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"json5": "^2.2.3"
|
|
19
|
+
},
|
|
17
20
|
"engines": {
|
|
18
21
|
"node": ">=18.18.0"
|
|
19
22
|
},
|