gpteam 0.1.20 → 0.1.22
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 +1 -1
- package/lib/help.js +1 -1
- package/lib/image-mcp/image.js +129 -17
- package/lib/image-mcp/server.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ The Image MCP exposes both a synchronous compatibility tool and a local async jo
|
|
|
27
27
|
|
|
28
28
|
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`.
|
|
29
29
|
|
|
30
|
-
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. Pass `mask` 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. 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.
|
|
30
|
+
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.
|
|
31
31
|
|
|
32
32
|
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.
|
|
33
33
|
|
package/lib/help.js
CHANGED
package/lib/image-mcp/image.js
CHANGED
|
@@ -29,14 +29,15 @@ export function buildImageGenerationPayload(input = {}, options = {}) {
|
|
|
29
29
|
model: String(input.model || DEFAULT_IMAGE_MODEL),
|
|
30
30
|
prompt: String(input.prompt || '').trim(),
|
|
31
31
|
response_format: 'b64_json',
|
|
32
|
+
stream: true,
|
|
32
33
|
size: String(input.size || '1024x1024'),
|
|
33
34
|
quality: String(input.quality || 'high'),
|
|
34
35
|
output_format: normalizeImageFormat(input.format || input.output_format || DEFAULT_IMAGE_FORMAT)
|
|
35
36
|
};
|
|
36
37
|
const imageOptions = { ...options, home: options.home };
|
|
37
|
-
const images = normalizeInputImages(input
|
|
38
|
+
const images = normalizeInputImages(collectInputImageValues(input), imageOptions);
|
|
38
39
|
if (images.length > 0) payload.images = images.map((imageURL) => ({ image_url: imageURL }));
|
|
39
|
-
const mask = normalizeImageReference(input.mask, imageOptions);
|
|
40
|
+
const mask = normalizeImageReference(firstPresentImageReference(input.mask, input.mask_path), imageOptions);
|
|
40
41
|
if (mask) payload.mask = { image_url: mask };
|
|
41
42
|
copyOptionalImageToolOption(payload, input, 'background');
|
|
42
43
|
copyOptionalImageToolOption(payload, input, 'moderation');
|
|
@@ -285,6 +286,8 @@ export function getCapabilities(options = {}) {
|
|
|
285
286
|
supports_idempotency_key: true,
|
|
286
287
|
supports_image_to_image: true,
|
|
287
288
|
supports_mask: true,
|
|
289
|
+
image_input_fields: ['images', 'image', 'image_path', 'image_paths', 'input_image', 'input_images'],
|
|
290
|
+
mask_input_fields: ['mask', 'mask_path'],
|
|
288
291
|
sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'],
|
|
289
292
|
formats: ['png', 'jpeg', 'webp'],
|
|
290
293
|
quality: ['low', 'medium', 'high', 'auto'],
|
|
@@ -330,21 +333,8 @@ async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
|
330
333
|
signal: requestSignal.signal
|
|
331
334
|
});
|
|
332
335
|
if (!response.ok) throw await imageErrorFromHTTPResponse(response, credentials.apiKey);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const b64 = first && typeof first.b64_json === 'string' ? first.b64_json : '';
|
|
336
|
-
if (!b64) {
|
|
337
|
-
throw new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
|
|
338
|
-
code: 'image_data_missing',
|
|
339
|
-
category: 'response_invalid',
|
|
340
|
-
stage: 'local',
|
|
341
|
-
retryable: false
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
b64,
|
|
346
|
-
revisedPrompt: first && typeof first.revised_prompt === 'string' ? first.revised_prompt : ''
|
|
347
|
-
};
|
|
336
|
+
if (isEventStreamResponse(response)) return await readImageSSE(response);
|
|
337
|
+
return await readImageJSON(response);
|
|
348
338
|
} catch (error) {
|
|
349
339
|
if (error instanceof ImageMCPError) throw error;
|
|
350
340
|
throw imageErrorFromFetch(error, {
|
|
@@ -357,6 +347,98 @@ async function fetchImageOnce(fetchImpl, credentials, payload, options) {
|
|
|
357
347
|
}
|
|
358
348
|
}
|
|
359
349
|
|
|
350
|
+
async function readImageJSON(response) {
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
const first = Array.isArray(data && data.data) ? data.data[0] : null;
|
|
353
|
+
return imageResultFromItem(first);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function readImageSSE(response) {
|
|
357
|
+
const text = await response.text();
|
|
358
|
+
let lastError = null;
|
|
359
|
+
for (const event of parseSSEDataEvents(text)) {
|
|
360
|
+
if (!event || event === '[DONE]') continue;
|
|
361
|
+
let payload;
|
|
362
|
+
try {
|
|
363
|
+
payload = JSON.parse(event);
|
|
364
|
+
} catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (payload && payload.error) {
|
|
368
|
+
lastError = imageErrorFromSSEPayload(payload);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const item = imageResultFromStreamPayload(payload);
|
|
372
|
+
if (item) return item;
|
|
373
|
+
}
|
|
374
|
+
if (lastError) throw lastError;
|
|
375
|
+
throw missingImageDataError();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseSSEDataEvents(text) {
|
|
379
|
+
const events = [];
|
|
380
|
+
let current = [];
|
|
381
|
+
for (const rawLine of String(text || '').split(/\r?\n/)) {
|
|
382
|
+
if (rawLine === '') {
|
|
383
|
+
if (current.length > 0) events.push(current.join('\n'));
|
|
384
|
+
current = [];
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (rawLine.startsWith('data:')) current.push(rawLine.slice(5).trimStart());
|
|
388
|
+
}
|
|
389
|
+
if (current.length > 0) events.push(current.join('\n'));
|
|
390
|
+
return events;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function imageResultFromStreamPayload(payload) {
|
|
394
|
+
const type = String(payload && payload.type || '');
|
|
395
|
+
if (!type.endsWith('.completed')) return null;
|
|
396
|
+
return imageResultFromItem(payload);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function imageResultFromItem(first) {
|
|
400
|
+
const b64 = first && typeof first.b64_json === 'string' ? first.b64_json : imageDataURLToB64(first && first.url);
|
|
401
|
+
if (!b64) throw missingImageDataError();
|
|
402
|
+
return {
|
|
403
|
+
b64,
|
|
404
|
+
revisedPrompt: first && typeof first.revised_prompt === 'string' ? first.revised_prompt : ''
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function imageDataURLToB64(value) {
|
|
409
|
+
const text = String(value || '').trim();
|
|
410
|
+
const match = text.match(/^data:image\/[a-z0-9.+-]+;base64,(.+)$/i);
|
|
411
|
+
return match ? match[1] : '';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function missingImageDataError() {
|
|
415
|
+
return new ImageMCPError('GPTeam 图片接口没有返回 b64_json 图片数据。', {
|
|
416
|
+
code: 'image_data_missing',
|
|
417
|
+
category: 'response_invalid',
|
|
418
|
+
stage: 'local',
|
|
419
|
+
retryable: false
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function imageErrorFromSSEPayload(payload) {
|
|
424
|
+
const error = payload && payload.error;
|
|
425
|
+
const message = typeof error === 'string' ? error : String(error && error.message || 'GPTeam 图片流返回错误。');
|
|
426
|
+
const code = typeof error === 'object' && error ? String(error.code || payload.code || 'upstream_stream_error') : String(payload.code || 'upstream_stream_error');
|
|
427
|
+
return new ImageMCPError(message, {
|
|
428
|
+
code,
|
|
429
|
+
category: 'upstream',
|
|
430
|
+
stage: 'upstream',
|
|
431
|
+
retryable: code === 'rate_limit_exceeded' || code === 'server_error'
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isEventStreamResponse(response) {
|
|
436
|
+
const contentType = response && response.headers && typeof response.headers.get === 'function'
|
|
437
|
+
? String(response.headers.get('content-type') || '')
|
|
438
|
+
: '';
|
|
439
|
+
return /text\/event-stream/i.test(contentType);
|
|
440
|
+
}
|
|
441
|
+
|
|
360
442
|
function buildSuccessResult(input) {
|
|
361
443
|
const result = {
|
|
362
444
|
ok: true,
|
|
@@ -552,6 +634,36 @@ function normalizeInputImages(value, options = {}) {
|
|
|
552
634
|
return rawImages.map((item) => normalizeImageReference(item, options)).filter(Boolean);
|
|
553
635
|
}
|
|
554
636
|
|
|
637
|
+
function collectInputImageValues(input = {}) {
|
|
638
|
+
const values = [];
|
|
639
|
+
appendImageAlias(values, input.images);
|
|
640
|
+
appendImageAlias(values, input.image);
|
|
641
|
+
appendImageAlias(values, input.image_path);
|
|
642
|
+
appendImageAlias(values, input.image_paths);
|
|
643
|
+
appendImageAlias(values, input.input_image);
|
|
644
|
+
appendImageAlias(values, input.input_images);
|
|
645
|
+
return values;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function appendImageAlias(target, value) {
|
|
649
|
+
if (Array.isArray(value)) {
|
|
650
|
+
for (const item of value) appendImageAlias(target, item);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (value) target.push(value);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function firstPresentImageReference(...values) {
|
|
657
|
+
for (const value of values) {
|
|
658
|
+
if (Array.isArray(value)) {
|
|
659
|
+
if (value.length > 0) return value[0];
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (value) return value;
|
|
663
|
+
}
|
|
664
|
+
return '';
|
|
665
|
+
}
|
|
666
|
+
|
|
555
667
|
function normalizeImageReference(value, options = {}) {
|
|
556
668
|
if (!value) return '';
|
|
557
669
|
if (typeof value === 'object') {
|
package/lib/image-mcp/server.js
CHANGED
|
@@ -53,10 +53,32 @@ const imageInputProperties = {
|
|
|
53
53
|
items: { type: 'string' },
|
|
54
54
|
description: 'Optional input images for image-to-image or edit. Values may be data URLs, HTTPS URLs, or local file paths.'
|
|
55
55
|
},
|
|
56
|
+
image_path: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Optional single local image path alias for image-to-image or edit.'
|
|
59
|
+
},
|
|
60
|
+
image_paths: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { type: 'string' },
|
|
63
|
+
description: 'Optional local image path aliases for image-to-image or edit.'
|
|
64
|
+
},
|
|
65
|
+
input_image: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Optional single input image alias. Accepts a data URL, HTTPS URL, or local file path.'
|
|
68
|
+
},
|
|
69
|
+
input_images: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
items: { type: 'string' },
|
|
72
|
+
description: 'Optional input image aliases. Accepts data URLs, HTTPS URLs, or local file paths.'
|
|
73
|
+
},
|
|
56
74
|
mask: {
|
|
57
75
|
type: 'string',
|
|
58
76
|
description: 'Optional mask image for image edit. Value may be a data URL, HTTPS URL, or local file path.'
|
|
59
77
|
},
|
|
78
|
+
mask_path: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Optional local mask image path alias for image edit.'
|
|
81
|
+
},
|
|
60
82
|
input_fidelity: {
|
|
61
83
|
type: 'string',
|
|
62
84
|
description: 'Accepted for compatibility. The GPTeam Image 2 bridge currently ignores this option because the upstream Codex image tool rejects it on edits.',
|