gpteam 0.1.0

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 ADDED
@@ -0,0 +1,23 @@
1
+ # GPTeam CLI
2
+
3
+ Interactive GPTeam API client configurator.
4
+
5
+ ```bash
6
+ npx gpteam
7
+ ```
8
+
9
+ The CLI asks for an API key, detects available models, benchmarks all production ingress endpoints with real API requests, then writes the selected client configuration after backing up existing files.
10
+
11
+ Supported clients:
12
+
13
+ - Codex
14
+ - OpenCode
15
+ - Claude Code
16
+ - OpenClaw on macOS and Linux
17
+
18
+ Useful non-interactive smoke checks:
19
+
20
+ ```bash
21
+ npx gpteam --help
22
+ npx gpteam --version
23
+ ```
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../lib/cli.js';
3
+
4
+ runCli(process.argv.slice(2)).catch((error) => {
5
+ console.error(`\n配置失败:${error && error.message ? error.message : error}`);
6
+ process.exitCode = 1;
7
+ });
package/lib/bench.js ADDED
@@ -0,0 +1,162 @@
1
+ import dns from 'node:dns';
2
+ import https from 'node:https';
3
+ import { performance } from 'node:perf_hooks';
4
+ import { inspectSSEBody } from './sse.js';
5
+
6
+ export async function benchmarkNodes(nodes, options) {
7
+ const rounds = options.rounds || 3;
8
+ const results = [];
9
+ for (const node of nodes) {
10
+ const samples = [];
11
+ for (let index = 0; index < rounds; index += 1) {
12
+ samples.push(await benchmarkNode(node, options));
13
+ }
14
+ results.push(summarizeNode(node, samples));
15
+ }
16
+ return results.sort((a, b) => scoreResult(a) - scoreResult(b));
17
+ }
18
+
19
+ export async function benchmarkNode(node, options) {
20
+ const health = await measureHealth(node.healthUrl);
21
+ const stream = await measureStream(node.baseUrl, options);
22
+ return {
23
+ ok: health.ok && stream.ok,
24
+ health,
25
+ stream,
26
+ error: health.error || stream.error || ''
27
+ };
28
+ }
29
+
30
+ export function summarizeNode(node, samples) {
31
+ const successful = samples.filter((sample) => sample.ok);
32
+ const streams = successful.map((sample) => sample.stream);
33
+ return {
34
+ node,
35
+ samples,
36
+ successRate: samples.length ? successful.length / samples.length : 0,
37
+ firstEventMs: median(streams.map((item) => item.firstEventMs).filter(Number.isFinite)),
38
+ totalMs: median(streams.map((item) => item.totalMs).filter(Number.isFinite)),
39
+ dnsMs: median(streams.map((item) => item.dnsMs).filter(Number.isFinite)),
40
+ tcpMs: median(streams.map((item) => item.tcpMs).filter(Number.isFinite)),
41
+ tlsMs: median(streams.map((item) => item.tlsMs).filter(Number.isFinite)),
42
+ healthMs: median(successful.map((sample) => sample.health.totalMs).filter(Number.isFinite)),
43
+ error: samples.find((sample) => sample.error)?.error || ''
44
+ };
45
+ }
46
+
47
+ export function formatMs(value) {
48
+ if (!Number.isFinite(value)) return '-';
49
+ if (value < 1000) return `${Math.round(value)}ms`;
50
+ return `${(value / 1000).toFixed(2)}s`;
51
+ }
52
+
53
+ function scoreResult(result) {
54
+ if (!result.successRate) return Number.MAX_SAFE_INTEGER;
55
+ return (result.firstEventMs || 999999) + (result.totalMs || 999999) - result.successRate * 1000;
56
+ }
57
+
58
+ async function measureHealth(url) {
59
+ const started = performance.now();
60
+ try {
61
+ const response = await fetch(url, { headers: { 'User-Agent': 'gpteam-api-config/0.1' } });
62
+ await response.arrayBuffer();
63
+ return {
64
+ ok: response.ok,
65
+ status: response.status,
66
+ totalMs: performance.now() - started,
67
+ error: response.ok ? '' : `health HTTP ${response.status}`
68
+ };
69
+ } catch (error) {
70
+ return { ok: false, status: 0, totalMs: performance.now() - started, error: error.message };
71
+ }
72
+ }
73
+
74
+ function measureStream(baseUrl, options) {
75
+ return new Promise((resolve) => {
76
+ const url = new URL(`${baseUrl.replace(/\/$/, '')}/responses`);
77
+ const started = performance.now();
78
+ const timings = { dnsMs: NaN, tcpMs: NaN, tlsMs: NaN, firstEventMs: NaN };
79
+ const payload = JSON.stringify({
80
+ model: options.model,
81
+ stream: true,
82
+ input: options.prompt || '请只回复一句话:节点测速完成。',
83
+ max_output_tokens: options.maxOutputTokens || 648,
84
+ reasoning: options.effort ? { effort: options.effort } : undefined,
85
+ metadata: { gpteam_config_probe: '1' }
86
+ });
87
+
88
+ const request = https.request({
89
+ protocol: url.protocol,
90
+ hostname: url.hostname,
91
+ port: url.port || 443,
92
+ path: url.pathname,
93
+ method: 'POST',
94
+ timeout: options.timeoutMs || 45000,
95
+ headers: {
96
+ Authorization: `Bearer ${options.apiKey}`,
97
+ 'Content-Type': 'application/json',
98
+ Accept: 'text/event-stream',
99
+ 'Content-Length': Buffer.byteLength(payload),
100
+ 'User-Agent': 'gpteam-api-config/0.1'
101
+ },
102
+ lookup(hostname, opts, callback) {
103
+ const before = performance.now();
104
+ dns.lookup(hostname, opts, (error, address, family) => {
105
+ timings.dnsMs = performance.now() - before;
106
+ callback(error, address, family);
107
+ });
108
+ }
109
+ }, (response) => {
110
+ let body = '';
111
+ response.on('data', (chunk) => {
112
+ if (!Number.isFinite(timings.firstEventMs)) {
113
+ const text = chunk.toString('utf8');
114
+ if (text.includes('data:') || text.trim()) timings.firstEventMs = performance.now() - started;
115
+ }
116
+ body += chunk.toString('utf8');
117
+ });
118
+ response.on('end', () => {
119
+ const semantic = inspectSSEBody(body);
120
+ const ok = response.statusCode >= 200
121
+ && response.statusCode < 300
122
+ && semantic.ok
123
+ && Number.isFinite(timings.firstEventMs);
124
+ resolve({
125
+ ok,
126
+ status: response.statusCode,
127
+ ...timings,
128
+ totalMs: performance.now() - started,
129
+ error: ok ? '' : semantic.error || `stream HTTP ${response.statusCode}`
130
+ });
131
+ });
132
+ });
133
+
134
+ request.on('socket', (socket) => {
135
+ const socketStarted = performance.now();
136
+ socket.on('connect', () => {
137
+ timings.tcpMs = performance.now() - socketStarted;
138
+ });
139
+ socket.on('secureConnect', () => {
140
+ timings.tlsMs = performance.now() - socketStarted;
141
+ });
142
+ });
143
+ request.on('timeout', () => request.destroy(new Error('stream timeout')));
144
+ request.on('error', (error) => {
145
+ resolve({
146
+ ok: false,
147
+ status: 0,
148
+ ...timings,
149
+ totalMs: performance.now() - started,
150
+ error: error.message
151
+ });
152
+ });
153
+ request.end(payload);
154
+ });
155
+ }
156
+
157
+ function median(values) {
158
+ if (!values.length) return NaN;
159
+ const sorted = [...values].sort((a, b) => a - b);
160
+ const middle = Math.floor(sorted.length / 2);
161
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
162
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,190 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ import { benchmarkNodes, formatMs } from './bench.js';
4
+ import { CLIENTS, writeClientConfig } from './config.js';
5
+ import { getHelpText, PACKAGE_NAME, PACKAGE_VERSION } from './help.js';
6
+ import { fetchModels, modelByID, normalizeModels } from './models.js';
7
+ import { describeSplit, INGRESS_NODES } from './nodes.js';
8
+
9
+ export async function runCli(argv = []) {
10
+ const normalizedArgv = normalizeSubcommand(argv);
11
+ const args = parseArgs(normalizedArgv);
12
+ if (args.help || args.h) {
13
+ console.log(getHelpText());
14
+ return;
15
+ }
16
+ if (args.version || args.v) {
17
+ console.log(`${PACKAGE_NAME} ${PACKAGE_VERSION}`);
18
+ return;
19
+ }
20
+ const rl = readline.createInterface({ input, output });
21
+ try {
22
+ console.log('GPTeam API 配置助手');
23
+ console.log('会先做真实 API 测速,再写入客户端配置;旧配置会自动备份。\n');
24
+
25
+ const apiKey = args.key || args.apiKey || await askRequired(rl, '请输入 API key:');
26
+ const client = await choose(rl, '请选择客户端类型', CLIENTS, args.client);
27
+ const models = await loadModels(apiKey);
28
+ const model = await chooseModel(rl, models, args.model);
29
+ const contextLength = await askContextLength(rl, model, args.context);
30
+ const effort = await choose(rl, '请选择思考深度', model.efforts.map((id) => ({ id, label: id })), args.effort);
31
+ const maxOutputTokens = Number(args.maxOutputTokens || 648);
32
+
33
+ console.log('\n开始真实测速:GET /api/health + POST /v1/responses stream=true');
34
+ console.log(`模型:${model.id},测速输出上限:${maxOutputTokens}`);
35
+ const results = await benchmarkNodes(INGRESS_NODES, {
36
+ apiKey,
37
+ model: model.id,
38
+ effort: effort.id,
39
+ maxOutputTokens,
40
+ rounds: Number(args.rounds || 3)
41
+ });
42
+ printResults(results);
43
+
44
+ const recommended = results.find((item) => item.successRate > 0) || results[0];
45
+ const selectedNode = await chooseNode(rl, results, args.node, recommended && recommended.node.id);
46
+ const written = writeClientConfig(client.id, {
47
+ apiKey,
48
+ model: model.id,
49
+ effort: effort.id,
50
+ contextLength,
51
+ maxOutputTokens: model.maxOutputTokens,
52
+ node: selectedNode
53
+ });
54
+
55
+ console.log('\n已写入配置:');
56
+ for (const filePath of written) console.log(`- ${filePath}`);
57
+ console.log(`入口:${selectedNode.label}(${describeSplit(selectedNode)}) ${selectedNode.baseUrl}`);
58
+ } finally {
59
+ rl.close();
60
+ }
61
+ }
62
+
63
+ export function parseArgs(argv) {
64
+ const result = {};
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const item = argv[index];
67
+ if (!item.startsWith('--')) continue;
68
+ const [key, inline] = item.slice(2).split('=', 2);
69
+ if (inline !== undefined) {
70
+ result[toCamel(key)] = inline;
71
+ continue;
72
+ }
73
+ const next = argv[index + 1];
74
+ if (!next || next.startsWith('--')) {
75
+ result[toCamel(key)] = true;
76
+ continue;
77
+ }
78
+ result[toCamel(key)] = next;
79
+ index += 1;
80
+ }
81
+ return result;
82
+ }
83
+
84
+ export function printResults(results) {
85
+ const recommended = results.find((item) => item.successRate > 0);
86
+ const rows = results.map((item) => [
87
+ `${item.node.label}(${describeSplit(item.node)})`,
88
+ formatMs(item.dnsMs),
89
+ formatMs(item.tcpMs),
90
+ formatMs(item.tlsMs),
91
+ formatMs(item.firstEventMs),
92
+ formatMs(item.totalMs),
93
+ `${Math.round(item.successRate * 100)}%`,
94
+ formatMs(item.healthMs),
95
+ item === recommended ? '推荐' : '-'
96
+ ]);
97
+ const header = ['节点', 'DNS', 'TCP', 'TLS', '首包', '完成', '成功率', '健康检查', '推荐'];
98
+ const widths = header.map((name, i) => Math.max(displayWidth(name), ...rows.map((row) => displayWidth(row[i]))) + 2);
99
+ console.log('');
100
+ console.log(formatRow(header, widths));
101
+ for (const row of rows) console.log(formatRow(row, widths));
102
+ }
103
+
104
+ async function loadModels(apiKey) {
105
+ for (const node of INGRESS_NODES) {
106
+ try {
107
+ return await fetchModels(node.baseUrl, apiKey);
108
+ } catch {
109
+ // 继续尝试下一个入口,全部失败时用本地兜底模型表。
110
+ }
111
+ }
112
+ return normalizeModels({ data: [] });
113
+ }
114
+
115
+ async function chooseModel(rl, models, preferred) {
116
+ const selected = preferred ? modelByID(models, preferred) : null;
117
+ if (selected) return selected;
118
+ console.log('\n可用模型:');
119
+ models.forEach((model, index) => {
120
+ console.log(`${index + 1}. ${model.id}(上下文 ${model.contextLength},输出 ${model.maxOutputTokens})`);
121
+ });
122
+ const answer = await askRequired(rl, '请选择模型序号:');
123
+ const index = Math.max(1, Math.min(models.length, Number(answer) || 1)) - 1;
124
+ return models[index];
125
+ }
126
+
127
+ async function askContextLength(rl, model, preferred) {
128
+ const max = Number(model.contextLength || 400000);
129
+ if (preferred) return clamp(Number(preferred), 1, max);
130
+ const answer = await rl.question(`请输入上下文长度(最大 ${max},输出上限 ${model.maxOutputTokens},默认 ${max}):`);
131
+ return clamp(Number(answer || max), 1, max);
132
+ }
133
+
134
+ async function chooseNode(rl, results, preferred, recommendedID) {
135
+ const nodes = results.map((item) => item.node);
136
+ const preferredNode = nodes.find((node) => node.id === preferred);
137
+ if (preferredNode) return preferredNode;
138
+ if (recommendedID) {
139
+ console.log(`\n推荐入口:${recommendedID}`);
140
+ }
141
+ return (await choose(rl, '请选择最终写入的入口', nodes.map((node) => ({
142
+ id: node.id,
143
+ label: `${node.label}(${describeSplit(node)})`
144
+ })))).raw;
145
+ }
146
+
147
+ async function choose(rl, title, items, preferred) {
148
+ const found = items.find((item) => item.id === preferred);
149
+ if (found) return { ...found, raw: found };
150
+ console.log(`\n${title}:`);
151
+ items.forEach((item, index) => console.log(`${index + 1}. ${item.label}`));
152
+ const answer = await askRequired(rl, '请输入序号:');
153
+ const index = Math.max(1, Math.min(items.length, Number(answer) || 1)) - 1;
154
+ const item = items[index];
155
+ return { ...item, raw: item };
156
+ }
157
+
158
+ async function askRequired(rl, prompt) {
159
+ for (;;) {
160
+ const answer = (await rl.question(prompt)).trim();
161
+ if (answer) return answer;
162
+ }
163
+ }
164
+
165
+ function formatRow(cells, widths) {
166
+ return cells.map((cell, index) => padCell(cell, widths[index])).join('');
167
+ }
168
+
169
+ function padCell(value, width) {
170
+ const text = String(value);
171
+ return text + ' '.repeat(Math.max(1, width - displayWidth(text)));
172
+ }
173
+
174
+ function displayWidth(value) {
175
+ return Array.from(String(value)).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 2 : 1), 0);
176
+ }
177
+
178
+ function clamp(value, min, max) {
179
+ if (!Number.isFinite(value)) return max;
180
+ return Math.max(min, Math.min(max, Math.floor(value)));
181
+ }
182
+
183
+ function toCamel(value) {
184
+ return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
185
+ }
186
+
187
+ function normalizeSubcommand(argv) {
188
+ if (argv[0] === 'config') return argv.slice(1);
189
+ return argv;
190
+ }
package/lib/config.js ADDED
@@ -0,0 +1,172 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { endpointRoot } from './nodes.js';
5
+
6
+ export const CLIENTS = [
7
+ { id: 'codex', label: 'Codex' },
8
+ { id: 'opencode', label: 'OpenCode' },
9
+ { id: 'claude-code', label: 'Claude Code' },
10
+ { id: 'openclaw', label: 'OpenClaw(macOS / Linux)' }
11
+ ];
12
+
13
+ export function writeClientConfig(clientID, settings) {
14
+ if (clientID === 'codex') return writeCodexConfig(settings);
15
+ if (clientID === 'opencode') return writeOpenCodeConfig(settings);
16
+ if (clientID === 'claude-code') return writeClaudeCodeEnv(settings);
17
+ if (clientID === 'openclaw') return writeOpenClawConfig(settings);
18
+ throw new Error(`未知客户端:${clientID}`);
19
+ }
20
+
21
+ export function writeCodexConfig(settings) {
22
+ const dir = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
23
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ const configPath = path.join(dir, 'config.toml');
25
+ const authPath = path.join(dir, 'auth.json');
26
+ backupIfExists(configPath);
27
+ backupIfExists(authPath);
28
+
29
+ const raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
30
+ const cleaned = stripCodexManagedConfig(raw);
31
+ const managed = [
32
+ `model = ${tomlString(settings.model)}`,
33
+ `model_provider = "gpteam"`,
34
+ `model_context_window = ${Number(settings.contextLength)}`,
35
+ `model_reasoning_effort = ${tomlString(settings.effort)}`,
36
+ 'disable_response_storage = true',
37
+ '',
38
+ '[model_providers.gpteam]',
39
+ 'name = "gpteam"',
40
+ `base_url = ${tomlString(settings.node.baseUrl)}`,
41
+ 'wire_api = "responses"',
42
+ 'requires_openai_auth = true',
43
+ 'supports_websockets = false'
44
+ ].join('\n');
45
+ const next = `${managed}\n\n${cleaned.trim()}`.trim() + '\n';
46
+ fs.writeFileSync(configPath, next, { encoding: 'utf8', mode: 0o600 });
47
+ fs.writeFileSync(authPath, `${JSON.stringify({ OPENAI_API_KEY: settings.apiKey }, null, 2)}\n`, {
48
+ encoding: 'utf8',
49
+ mode: 0o600
50
+ });
51
+ return [configPath, authPath];
52
+ }
53
+
54
+ export function writeOpenCodeConfig(settings) {
55
+ const dir = path.join(os.homedir(), '.config', 'opencode');
56
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ const filePath = path.join(dir, 'opencode.json');
58
+ backupIfExists(filePath);
59
+ const config = readJSON(filePath, { $schema: 'https://opencode.ai/config.json' });
60
+ config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
61
+ config.provider.gpteam = {
62
+ npm: '@ai-sdk/openai',
63
+ options: {
64
+ apiKey: settings.apiKey,
65
+ baseURL: settings.node.baseUrl
66
+ },
67
+ models: {
68
+ [settings.model]: {
69
+ name: settings.model,
70
+ limit: {
71
+ context: Number(settings.contextLength),
72
+ output: Number(settings.maxOutputTokens)
73
+ },
74
+ variants: {
75
+ [settings.effort]: {
76
+ reasoningEffort: settings.effort,
77
+ reasoningSummary: settings.effort === 'high' || settings.effort === 'xhigh' ? 'detailed' : 'auto',
78
+ textVerbosity: 'medium'
79
+ }
80
+ }
81
+ }
82
+ }
83
+ };
84
+ writeJSON(filePath, config);
85
+ return [filePath];
86
+ }
87
+
88
+ export function writeClaudeCodeEnv(settings) {
89
+ const dir = path.join(os.homedir(), '.gpteam');
90
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
91
+ const filePath = path.join(dir, 'claude-code.env');
92
+ backupIfExists(filePath);
93
+ const root = endpointRoot(settings.node.baseUrl);
94
+ const lines = [
95
+ '# GPTeam Claude Code environment.',
96
+ `export ANTHROPIC_AUTH_TOKEN=${shellQuote(settings.apiKey)}`,
97
+ `export ANTHROPIC_BASE_URL=${shellQuote(root)}`,
98
+ `export ANTHROPIC_MODEL=${shellQuote(settings.model)}`,
99
+ `export CODEX_REASONING_EFFORT=${shellQuote(settings.effort)}`,
100
+ '',
101
+ '# 使用方式:source ~/.gpteam/claude-code.env'
102
+ ];
103
+ fs.writeFileSync(filePath, `${lines.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
104
+ return [filePath];
105
+ }
106
+
107
+ export function writeOpenClawConfig(settings) {
108
+ if (process.platform === 'win32') {
109
+ throw new Error('OpenClaw 自动配置当前只支持 macOS / Linux');
110
+ }
111
+ const dir = path.join(os.homedir(), '.openclaw');
112
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
113
+ const filePath = path.join(dir, 'openclaw.json');
114
+ backupIfExists(filePath);
115
+ const config = readJSON(filePath, {});
116
+ config.models = config.models && typeof config.models === 'object' ? config.models : {};
117
+ config.models.mode = config.models.mode || 'merge';
118
+ config.models.providers = config.models.providers && typeof config.models.providers === 'object'
119
+ ? config.models.providers
120
+ : {};
121
+ config.models.providers.gpteam = {
122
+ baseUrl: settings.node.baseUrl,
123
+ apiKey: settings.apiKey,
124
+ api: 'openai-responses',
125
+ models: [{
126
+ id: settings.model,
127
+ name: settings.model,
128
+ reasoning: true,
129
+ contextWindow: Number(settings.contextLength),
130
+ maxTokens: Number(settings.maxOutputTokens)
131
+ }]
132
+ };
133
+ writeJSON(filePath, config);
134
+ return [filePath];
135
+ }
136
+
137
+ function stripCodexManagedConfig(raw) {
138
+ return String(raw || '')
139
+ .replace(/^\s*model\s*=.*\n?/gm, '')
140
+ .replace(/^\s*model_provider\s*=.*\n?/gm, '')
141
+ .replace(/^\s*model_context_window\s*=.*\n?/gm, '')
142
+ .replace(/^\s*model_reasoning_effort\s*=.*\n?/gm, '')
143
+ .replace(/^\s*\[model_providers\.gpteam\][\s\S]*?(?=^\s*\[[^\]]+\]|$)/gm, '')
144
+ .trim();
145
+ }
146
+
147
+ function backupIfExists(filePath) {
148
+ if (!fs.existsSync(filePath)) return;
149
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
150
+ fs.copyFileSync(filePath, `${filePath}.bak-${stamp}`);
151
+ }
152
+
153
+ function readJSON(filePath, fallback) {
154
+ if (!fs.existsSync(filePath)) return fallback;
155
+ try {
156
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
157
+ } catch {
158
+ return fallback;
159
+ }
160
+ }
161
+
162
+ function writeJSON(filePath, value) {
163
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
164
+ }
165
+
166
+ function tomlString(value) {
167
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
168
+ }
169
+
170
+ function shellQuote(value) {
171
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
172
+ }
package/lib/help.js ADDED
@@ -0,0 +1,26 @@
1
+ export const PACKAGE_NAME = 'gpteam';
2
+ export const PACKAGE_VERSION = '0.1.0';
3
+
4
+ export function getHelpText() {
5
+ return [
6
+ 'GPTeam API 配置助手',
7
+ '',
8
+ '用法:',
9
+ ' npx gpteam',
10
+ ' npx gpteam config',
11
+ '',
12
+ '常用参数:',
13
+ ' --api-key <key> 预填 API key',
14
+ ' --client <id> codex / opencode / claude-code / openclaw',
15
+ ' --model <id> 预选模型,例如 gpt-5.5',
16
+ ' --context <tokens> 预设上下文长度',
17
+ ' --effort <level> 按模型支持项预选,常见为 none / low / medium / high / xhigh',
18
+ ' --node <id> jp-direct / jp-split / hk-split / us-split',
19
+ ' --rounds <n> 每个入口测速轮数,默认 3',
20
+ ' --max-output-tokens <n> 测速输出上限,默认 648',
21
+ ' --help 显示帮助',
22
+ ' --version 显示版本',
23
+ '',
24
+ '说明:测速会真实请求 GET /api/health 和流式 POST /v1/responses,旧配置写入前会自动备份。'
25
+ ].join('\n');
26
+ }
package/lib/models.js ADDED
@@ -0,0 +1,100 @@
1
+ export const FALLBACK_MODELS = {
2
+ 'gpt-5.2': {
3
+ id: 'gpt-5.2',
4
+ contextLength: 400000,
5
+ maxOutputTokens: 128000,
6
+ efforts: ['none', 'low', 'medium', 'high', 'xhigh']
7
+ },
8
+ 'gpt-5.3-codex': {
9
+ id: 'gpt-5.3-codex',
10
+ contextLength: 400000,
11
+ maxOutputTokens: 128000,
12
+ efforts: ['low', 'medium', 'high', 'xhigh']
13
+ },
14
+ 'gpt-5.3-codex-spark': {
15
+ id: 'gpt-5.3-codex-spark',
16
+ contextLength: 128000,
17
+ maxOutputTokens: 128000,
18
+ efforts: ['low', 'medium', 'high', 'xhigh']
19
+ },
20
+ 'gpt-5.4': {
21
+ id: 'gpt-5.4',
22
+ contextLength: 1050000,
23
+ maxOutputTokens: 128000,
24
+ efforts: ['low', 'medium', 'high', 'xhigh']
25
+ },
26
+ 'gpt-5.4-mini': {
27
+ id: 'gpt-5.4-mini',
28
+ contextLength: 400000,
29
+ maxOutputTokens: 128000,
30
+ efforts: ['low', 'medium', 'high', 'xhigh']
31
+ },
32
+ 'gpt-5.5': {
33
+ id: 'gpt-5.5',
34
+ contextLength: 272000,
35
+ maxOutputTokens: 128000,
36
+ efforts: ['low', 'medium', 'high', 'xhigh']
37
+ }
38
+ };
39
+
40
+ export function normalizeModels(payload) {
41
+ const items = Array.isArray(payload && payload.data) ? payload.data : [];
42
+ const result = new Map();
43
+
44
+ for (const item of items) {
45
+ const id = String(item.id || item.name || '').trim();
46
+ if (!id || !id.startsWith('gpt-')) continue;
47
+ if (id.includes('image')) continue;
48
+ const fallback = FALLBACK_MODELS[id] || {};
49
+ const thinking = item.thinking && typeof item.thinking === 'object' ? item.thinking : {};
50
+ const levels = Array.isArray(thinking.levels) ? thinking.levels : fallback.efforts;
51
+ result.set(id, {
52
+ id,
53
+ displayName: item.display_name || item.name || id,
54
+ contextLength: Number(item.context_length || item.inputTokenLimit || fallback.contextLength || 400000),
55
+ maxOutputTokens: Number(item.max_completion_tokens || item.outputTokenLimit || fallback.maxOutputTokens || 128000),
56
+ efforts: normalizeEfforts(levels)
57
+ });
58
+ }
59
+
60
+ if (!result.size) {
61
+ for (const model of Object.values(FALLBACK_MODELS)) {
62
+ result.set(model.id, {
63
+ id: model.id,
64
+ displayName: model.id,
65
+ contextLength: model.contextLength,
66
+ maxOutputTokens: model.maxOutputTokens,
67
+ efforts: model.efforts
68
+ });
69
+ }
70
+ }
71
+
72
+ return Array.from(result.values()).sort((a, b) => a.id.localeCompare(b.id));
73
+ }
74
+
75
+ export async function fetchModels(baseUrl, apiKey) {
76
+ const response = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, {
77
+ headers: {
78
+ Authorization: `Bearer ${apiKey}`,
79
+ 'User-Agent': 'gpteam-api-config/0.1'
80
+ }
81
+ });
82
+ if (!response.ok) {
83
+ throw new Error(`/v1/models 返回 HTTP ${response.status}`);
84
+ }
85
+ return normalizeModels(await response.json());
86
+ }
87
+
88
+ export function modelByID(models, id) {
89
+ return models.find((model) => model.id === id) || models[0];
90
+ }
91
+
92
+ function normalizeEfforts(levels) {
93
+ const out = [];
94
+ for (const level of levels || []) {
95
+ const normalized = String(level || '').trim().toLowerCase();
96
+ if (!normalized || out.includes(normalized)) continue;
97
+ out.push(normalized);
98
+ }
99
+ return out.length ? out : ['medium'];
100
+ }
package/lib/nodes.js ADDED
@@ -0,0 +1,42 @@
1
+ export const INGRESS_NODES = [
2
+ {
3
+ id: 'jp-direct',
4
+ label: '日本主机直连',
5
+ region: 'JP',
6
+ split: false,
7
+ baseUrl: 'https://api.gpteamservices.com/v1',
8
+ healthUrl: 'https://api.gpteamservices.com/api/health'
9
+ },
10
+ {
11
+ id: 'jp-split',
12
+ label: '日本入口',
13
+ region: 'JP',
14
+ split: true,
15
+ baseUrl: 'https://api-jp.gpteamservices.com/v1',
16
+ healthUrl: 'https://api-jp.gpteamservices.com/api/health'
17
+ },
18
+ {
19
+ id: 'hk-split',
20
+ label: '香港入口',
21
+ region: 'HK',
22
+ split: true,
23
+ baseUrl: 'https://api-hk.gpteamservices.com/v1',
24
+ healthUrl: 'https://api-hk.gpteamservices.com/api/health'
25
+ },
26
+ {
27
+ id: 'us-split',
28
+ label: '美国入口',
29
+ region: 'US',
30
+ split: true,
31
+ baseUrl: 'https://api-us.gpteamservices.com/v1',
32
+ healthUrl: 'https://api-us.gpteamservices.com/api/health'
33
+ }
34
+ ];
35
+
36
+ export function describeSplit(node) {
37
+ return node.split ? '分流' : '不分流';
38
+ }
39
+
40
+ export function endpointRoot(baseUrl) {
41
+ return String(baseUrl || '').replace(/\/v1\/?$/, '').replace(/\/$/, '');
42
+ }
package/lib/sse.js ADDED
@@ -0,0 +1,42 @@
1
+ export function inspectSSEBody(body) {
2
+ const text = String(body || '');
3
+ let sawData = false;
4
+ let sawDone = false;
5
+ let sawCompleted = false;
6
+
7
+ for (const line of text.split(/\r?\n/)) {
8
+ if (!line.startsWith('data:')) continue;
9
+ const data = line.slice(5).trim();
10
+ if (!data) continue;
11
+ sawData = true;
12
+ if (data === '[DONE]') {
13
+ sawDone = true;
14
+ continue;
15
+ }
16
+ const parsed = parseJSON(data);
17
+ if (!parsed) continue;
18
+ const type = String(parsed.type || '');
19
+ if (type === 'response.completed') sawCompleted = true;
20
+ if (isFailureSSE(parsed, type)) {
21
+ return { ok: false, error: `stream semantic failure ${type || 'error'}` };
22
+ }
23
+ }
24
+
25
+ if (!sawData) return { ok: false, error: 'stream empty response' };
26
+ return { ok: sawDone || sawCompleted || sawData, error: '' };
27
+ }
28
+
29
+ function parseJSON(value) {
30
+ try {
31
+ return JSON.parse(value);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function isFailureSSE(parsed, type) {
38
+ if (['response.failed', 'response.incomplete', 'response.canceled', 'response.cancelled', 'error'].includes(type)) {
39
+ return true;
40
+ }
41
+ return parsed && typeof parsed === 'object' && parsed.error && typeof parsed.error === 'object';
42
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "gpteam",
3
+ "version": "0.1.0",
4
+ "description": "GPTeam API interactive client configurator and ingress benchmark CLI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "gpteam": "bin/gpteam-api-config.js",
8
+ "gpteam-api-config": "bin/gpteam-api-config.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test test/*.test.mjs"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.18.0"
19
+ },
20
+ "license": "UNLICENSED",
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ }
25
+ }