gpteam 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,9 @@ When a selected client is missing, the interactive CLI shows the exact install c
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
- 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`.
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 three proxy-specific extensions on top of that baseline: Codex explicitly disables WebSocket prewarm, Codex writes 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`.
16
+
17
+ For Codex, the Image MCP config uses the cc-switch-style MCP env block. On macOS/Linux it writes `command = "npx"` with `args = ["-y", "-p", "gpteam", "gpteam-image-mcp"]`; on Windows it writes a `cmd /c npx -y -p gpteam gpteam-image-mcp` wrapper. The MCP receives `GPTEAM_API_KEY` and `GPTEAM_BASE_URL` from `[mcp_servers.gpteam_image.env]`, so it does not depend on Codex `auth.json`.
16
18
 
17
19
  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.
18
20
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runServer } from '../lib/image-mcp/server.js';
3
+
4
+ runServer().catch((error) => {
5
+ console.error(error && error.message ? error.message : error);
6
+ process.exitCode = 1;
7
+ });
package/lib/cli.js CHANGED
@@ -71,6 +71,9 @@ export async function runCli(argv = []) {
71
71
  for (const filePath of written) console.log(`- ${filePath}`);
72
72
  console.log(`入口:${formatNodeLabel(selectedNode)}`);
73
73
  console.log(`地址:${selectedNode.baseUrl}`);
74
+ if (client.id === 'codex') {
75
+ printHint(theme, '已写入 GPTeam Image MCP。Codex 对话里需要生图时可调用 generate_image,MCP 使用专用环境变量读取 API key。');
76
+ }
74
77
  } finally {
75
78
  rl.close();
76
79
  }
package/lib/config.js CHANGED
@@ -5,6 +5,9 @@ import JSON5 from 'json5';
5
5
  import { endpointRoot } from './nodes.js';
6
6
 
7
7
  const PROVIDER_ID = 'gpteam';
8
+ const IMAGE_MCP_ID = 'gpteam_image';
9
+ const IMAGE_MCP_PACKAGE = 'gpteam';
10
+ const IMAGE_MCP_BIN = 'gpteam-image-mcp';
8
11
 
9
12
  export const CLIENTS = [
10
13
  { id: 'codex', label: 'Codex' },
@@ -46,10 +49,26 @@ export function writeCodexConfig(settings) {
46
49
  'requires_openai_auth = true',
47
50
  'supports_websockets = false'
48
51
  ];
52
+ const mcpCommand = codexImageMCPCommand();
53
+ const managedImageMCP = [
54
+ `[mcp_servers.${IMAGE_MCP_ID}]`,
55
+ `command = ${tomlString(mcpCommand.command)}`,
56
+ `args = [${mcpCommand.args.map((arg) => tomlString(arg)).join(', ')}]`,
57
+ 'startup_timeout_sec = 20',
58
+ 'tool_timeout_sec = 300',
59
+ 'enabled_tools = ["generate_image"]',
60
+ 'default_tools_approval_mode = "prompt"',
61
+ '',
62
+ `[mcp_servers.${IMAGE_MCP_ID}.env]`,
63
+ `GPTEAM_API_KEY = ${tomlString(settings.apiKey)}`,
64
+ `GPTEAM_BASE_URL = ${tomlString(settings.node.baseUrl)}`,
65
+ `GPTEAM_CODEX_HOME = ${tomlString(dir)}`
66
+ ];
49
67
  const next = joinTomlSections([
50
68
  managedRoot.join('\n'),
51
69
  rootLines.join('\n'),
52
70
  rest.join('\n'),
71
+ managedImageMCP.join('\n'),
53
72
  managedProvider.join('\n')
54
73
  ]);
55
74
  assertNoDuplicateTomlKeys(next);
@@ -179,18 +198,34 @@ function claudeBaseUrl(baseUrl) {
179
198
  return `${root}/anthropic`;
180
199
  }
181
200
 
201
+ function codexImageMCPCommand() {
202
+ if (process.platform === 'win32') {
203
+ return {
204
+ command: 'cmd',
205
+ args: ['/c', 'npx', '-y', '-p', IMAGE_MCP_PACKAGE, IMAGE_MCP_BIN]
206
+ };
207
+ }
208
+ return {
209
+ command: 'npx',
210
+ args: ['-y', '-p', IMAGE_MCP_PACKAGE, IMAGE_MCP_BIN]
211
+ };
212
+ }
213
+
182
214
  function stripCodexManagedConfig(raw) {
183
215
  const rootLines = [];
184
216
  const rest = [];
185
217
  let inManagedProvider = false;
218
+ let inManagedImageMCP = false;
186
219
 
187
220
  for (const line of String(raw || '').split(/\r?\n/)) {
188
221
  if (isTableHeader(line)) {
189
222
  inManagedProvider = isGpteamProviderHeader(line);
190
- if (!inManagedProvider) rest.push(line);
223
+ inManagedImageMCP = isGpteamImageMCPHeader(line);
224
+ if (!inManagedProvider && !inManagedImageMCP) rest.push(line);
191
225
  continue;
192
226
  }
193
227
  if (isManagedRootLine(line)) continue;
228
+ if (inManagedImageMCP) continue;
194
229
  if (inManagedProvider) {
195
230
  if (isManagedProviderLine(line)) continue;
196
231
  if (isSalvageableRootLine(line)) rootLines.push(line);
@@ -245,6 +280,10 @@ function isGpteamProviderHeader(line) {
245
280
  return /^\s*\[model_providers\.gpteam\]\s*$/.test(line);
246
281
  }
247
282
 
283
+ function isGpteamImageMCPHeader(line) {
284
+ return /^\s*\[mcp_servers\.gpteam_image(?:\.[^\]]+)?\]\s*$/.test(line);
285
+ }
286
+
248
287
  function isManagedRootLine(line) {
249
288
  return /^\s*(model|model_provider|model_context_window|model_reasoning_effort|disable_response_storage)\s*=/.test(line);
250
289
  }
package/lib/help.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = 'gpteam';
2
- export const PACKAGE_VERSION = '0.1.14';
2
+ export const PACKAGE_VERSION = '0.1.15';
3
3
 
4
4
  export function getHelpText() {
5
5
  return [
@@ -22,6 +22,6 @@ export function getHelpText() {
22
22
  ' --help 显示帮助',
23
23
  ' --version 显示版本',
24
24
  '',
25
- '说明:输入 key 后会先请求 /v1/models 校验,通过后才继续。选好客户端后会检测本机命令是否可用,缺失时提示安装;Windows 暂不支持自动配置 OpenClaw。测速会请求 GET /api/health 和流式 POST /v1/responses。入口之间并行测速,写新配置前会先备份旧配置。'
25
+ '说明:输入 key 后会先请求 /v1/models 校验,通过后才继续。选好客户端后会检测本机命令是否可用,缺失时提示安装;Windows 暂不支持自动配置 OpenClaw。测速会请求 GET /api/health 和流式 POST /v1/responses。入口之间并行测速,写新配置前会先备份旧配置。Codex 配置会同时写入 GPTeam Image MCP,生图工具从 MCP env 读取 key。'
26
26
  ].join('\n');
27
27
  }
@@ -0,0 +1,186 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_BASE_URL = 'https://api.gpteamservices.com/v1';
6
+ export const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
7
+ export const DEFAULT_IMAGE_FORMAT = 'png';
8
+
9
+ export function buildImageGenerationPayload(input = {}) {
10
+ return {
11
+ model: String(input.model || DEFAULT_IMAGE_MODEL),
12
+ prompt: String(input.prompt || '').trim(),
13
+ response_format: 'b64_json',
14
+ size: String(input.size || '1024x1024'),
15
+ quality: String(input.quality || 'high'),
16
+ output_format: normalizeImageFormat(input.format || input.output_format || DEFAULT_IMAGE_FORMAT)
17
+ };
18
+ }
19
+
20
+ export function loadGPTeamCredentials(options = {}) {
21
+ const env = options.env || process.env;
22
+ const codexHome = resolveCodexHome(env, options.home || os.homedir());
23
+ const readFile = options.readFile || ((filePath) => fs.readFileSync(filePath, 'utf8'));
24
+ const configText = safeRead(path.join(codexHome, 'config.toml'), readFile);
25
+ const configuredBaseUrl = parseGPTeamBaseUrl(configText);
26
+ const apiKey = firstNonEmpty(env.GPTEAM_API_KEY);
27
+ if (!apiKey) {
28
+ throw new Error('没有找到 GPTeam API key。请在 MCP 配置 env 中设置 GPTEAM_API_KEY,或先运行 npx gpteam 完成本地配置。');
29
+ }
30
+ const baseUrl = normalizeBaseUrl(firstNonEmpty(env.GPTEAM_BASE_URL, configuredBaseUrl, DEFAULT_BASE_URL));
31
+ return { apiKey, baseUrl, codexHome };
32
+ }
33
+
34
+ export function parseGPTeamBaseUrl(configText) {
35
+ let inGPTeamProvider = false;
36
+ for (const rawLine of String(configText || '').split(/\r?\n/)) {
37
+ const line = rawLine.trim();
38
+ const table = line.match(/^\[([^\]]+)\]$/);
39
+ if (table) {
40
+ inGPTeamProvider = table[1] === 'model_providers.gpteam';
41
+ continue;
42
+ }
43
+ if (!inGPTeamProvider) continue;
44
+ const match = line.match(/^base_url\s*=\s*"((?:\\"|[^"])*)"/);
45
+ if (match) return unescapeTomlString(match[1]);
46
+ }
47
+ return '';
48
+ }
49
+
50
+ export function normalizeBaseUrl(value) {
51
+ const trimmed = String(value || '').trim().replace(/\/+$/, '');
52
+ if (!trimmed) return DEFAULT_BASE_URL;
53
+ if (/\/v1$/i.test(trimmed)) return trimmed;
54
+ return `${trimmed}/v1`;
55
+ }
56
+
57
+ export async function generateImage(input = {}, options = {}) {
58
+ const prompt = String(input.prompt || '').trim();
59
+ if (!prompt) throw new Error('prompt 不能为空');
60
+ const credentials = loadGPTeamCredentials(options);
61
+ const payload = buildImageGenerationPayload(input);
62
+ const fetchImpl = options.fetch || globalThis.fetch;
63
+ if (typeof fetchImpl !== 'function') {
64
+ throw new Error('当前 Node.js 运行时不支持 fetch,请升级到 Node.js 18.18 或更高版本。');
65
+ }
66
+ const response = await fetchImpl(`${credentials.baseUrl}/images/generations`, {
67
+ method: 'POST',
68
+ headers: {
69
+ Authorization: `Bearer ${credentials.apiKey}`,
70
+ 'Content-Type': 'application/json'
71
+ },
72
+ body: JSON.stringify(payload)
73
+ });
74
+ if (!response.ok) {
75
+ const detail = typeof response.text === 'function' ? await response.text() : '';
76
+ throw new Error(`GPTeam /v1/images/generations 返回 HTTP ${response.status}${detail ? `:${redactSecret(detail, credentials.apiKey)}` : ''}`);
77
+ }
78
+ const data = await response.json();
79
+ const first = Array.isArray(data && data.data) ? data.data[0] : null;
80
+ const b64 = first && typeof first.b64_json === 'string' ? first.b64_json : '';
81
+ if (!b64) {
82
+ throw new Error('GPTeam 图片接口没有返回 b64_json 图片数据。');
83
+ }
84
+ const format = normalizeImageFormat(payload.output_format);
85
+ const outputPath = resolveOutputPath(input.output_path, format, options);
86
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
87
+ fs.writeFileSync(outputPath, Buffer.from(b64, 'base64'));
88
+ return {
89
+ path: outputPath,
90
+ mimeType: mimeTypeForFormat(format),
91
+ b64,
92
+ model: payload.model,
93
+ size: payload.size,
94
+ quality: payload.quality,
95
+ format,
96
+ revisedPrompt: first && typeof first.revised_prompt === 'string' ? first.revised_prompt : ''
97
+ };
98
+ }
99
+
100
+ export function toolResultContent(result) {
101
+ const text = [
102
+ 'GPTeam Image 2 生成完成。',
103
+ `文件:${result.path}`,
104
+ `模型:${result.model}`,
105
+ `规格:${result.size} / ${result.quality} / ${result.format}`
106
+ ];
107
+ if (result.revisedPrompt) text.push(`修订提示词:${result.revisedPrompt}`);
108
+ return [
109
+ { type: 'text', text: text.join('\n') },
110
+ { type: 'image', data: result.b64, mimeType: result.mimeType }
111
+ ];
112
+ }
113
+
114
+ function resolveCodexHome(env, home) {
115
+ return expandHome(firstNonEmpty(env.GPTEAM_CODEX_HOME, env.CODEX_HOME, path.join(home, '.codex')), home);
116
+ }
117
+
118
+ function safeRead(filePath, readFile) {
119
+ try {
120
+ return readFile(filePath);
121
+ } catch {
122
+ return '';
123
+ }
124
+ }
125
+
126
+ function resolveOutputPath(rawOutputPath, format, options) {
127
+ const env = options.env || process.env;
128
+ const home = options.home || os.homedir();
129
+ const name = `gpteam-image-${new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-')}.${format}`;
130
+ if (rawOutputPath) {
131
+ const expanded = expandHome(String(rawOutputPath), home);
132
+ if (isDirectoryLike(expanded)) return path.join(expanded, name);
133
+ return expanded;
134
+ }
135
+ const outputDir = expandHome(firstNonEmpty(env.GPTEAM_IMAGE_OUTPUT_DIR, defaultImageOutputDir(home)), home);
136
+ return path.join(outputDir, name);
137
+ }
138
+
139
+ function defaultImageOutputDir(home) {
140
+ const desktop = path.join(home, 'Desktop');
141
+ return fs.existsSync(desktop) ? desktop : process.cwd();
142
+ }
143
+
144
+ function isDirectoryLike(value) {
145
+ return /[\\/]$/.test(value) || (fs.existsSync(value) && fs.statSync(value).isDirectory());
146
+ }
147
+
148
+ function normalizeImageFormat(value) {
149
+ const normalized = String(value || DEFAULT_IMAGE_FORMAT).trim().toLowerCase();
150
+ if (normalized === 'jpg') return 'jpeg';
151
+ if (['png', 'jpeg', 'webp'].includes(normalized)) return normalized;
152
+ return DEFAULT_IMAGE_FORMAT;
153
+ }
154
+
155
+ function mimeTypeForFormat(format) {
156
+ if (format === 'jpeg') return 'image/jpeg';
157
+ if (format === 'webp') return 'image/webp';
158
+ return 'image/png';
159
+ }
160
+
161
+ function firstNonEmpty(...values) {
162
+ for (const value of values) {
163
+ const text = String(value || '').trim();
164
+ if (text) return text;
165
+ }
166
+ return '';
167
+ }
168
+
169
+ function expandHome(value, home) {
170
+ const text = String(value || '');
171
+ if (text === '~') return home;
172
+ if (text.startsWith(`~${path.sep}`)) return path.join(home, text.slice(2));
173
+ if (text.startsWith('~/')) return path.join(home, text.slice(2));
174
+ return text;
175
+ }
176
+
177
+ function unescapeTomlString(value) {
178
+ return String(value || '').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
179
+ }
180
+
181
+ function redactSecret(text, secret) {
182
+ let result = String(text || '');
183
+ if (secret) result = result.split(secret).join('[redacted]');
184
+ result = result.replace(/sk-[A-Za-z0-9_-]{6,}/g, 'sk-[redacted]');
185
+ return result;
186
+ }
@@ -0,0 +1,74 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { generateImage, toolResultContent } from './image.js';
5
+
6
+ const GENERATE_IMAGE_TOOL = {
7
+ name: 'generate_image',
8
+ description: 'Generate an image through GPTeam Image 2 and save it to a local file.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ prompt: {
13
+ type: 'string',
14
+ description: 'Image prompt.'
15
+ },
16
+ size: {
17
+ type: 'string',
18
+ description: 'Image size, for example 1024x1024.',
19
+ default: '1024x1024'
20
+ },
21
+ quality: {
22
+ type: 'string',
23
+ description: 'Image quality.',
24
+ default: 'high'
25
+ },
26
+ format: {
27
+ type: 'string',
28
+ description: 'Output image format.',
29
+ enum: ['png', 'jpeg', 'webp'],
30
+ default: 'png'
31
+ },
32
+ output_path: {
33
+ type: 'string',
34
+ description: 'Optional output file path or directory.'
35
+ }
36
+ },
37
+ required: ['prompt'],
38
+ additionalProperties: false
39
+ }
40
+ };
41
+
42
+ export function createServer(deps = {}) {
43
+ const server = new Server({
44
+ name: 'gpteam-image-mcp',
45
+ version: '0.1.0'
46
+ }, {
47
+ capabilities: {
48
+ tools: {}
49
+ }
50
+ });
51
+
52
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
53
+ tools: [GENERATE_IMAGE_TOOL]
54
+ }));
55
+
56
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
57
+ const toolName = request.params && request.params.name;
58
+ if (toolName !== GENERATE_IMAGE_TOOL.name) {
59
+ throw new Error(`未知工具:${toolName}`);
60
+ }
61
+ const result = await generateImage(request.params.arguments || {}, deps);
62
+ return {
63
+ content: toolResultContent(result)
64
+ };
65
+ });
66
+
67
+ return server;
68
+ }
69
+
70
+ export async function runServer() {
71
+ const server = createServer();
72
+ const transport = new StdioServerTransport();
73
+ await server.connect(transport);
74
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "gpteam",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
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",
8
- "gpteam-api-config": "bin/gpteam-api-config.js"
8
+ "gpteam-api-config": "bin/gpteam-api-config.js",
9
+ "gpteam-image-mcp": "bin/gpteam-image-mcp.js"
9
10
  },
10
11
  "files": [
11
12
  "bin",
@@ -15,6 +16,7 @@
15
16
  "test": "node --test test/*.test.mjs"
16
17
  },
17
18
  "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
18
20
  "json5": "^2.2.3"
19
21
  },
20
22
  "engines": {