From fac5ff28386ae7d08dd2798d0ca38daa8ca45d61 Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 26 Apr 2026 08:32:44 +0800 Subject: [PATCH] feat(image-gen): add gpt-image-2 to OpenAiImageProvider --- .../image/provider/OpenAiImageProvider.java | 146 +++++++++++++++--- 1 file changed, 125 insertions(+), 21 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/tool/image/provider/OpenAiImageProvider.java b/mateclaw-server/src/main/java/vip/mate/tool/image/provider/OpenAiImageProvider.java index 21e46bc9..151f53b1 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/image/provider/OpenAiImageProvider.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/image/provider/OpenAiImageProvider.java @@ -17,11 +17,17 @@ import java.util.List; import java.util.Set; /** - * OpenAI 图片生成 Provider — 支持 DALL-E 3 / DALL-E 2 / gpt-image-1 + * OpenAI 图片生成 Provider + * — 支持 DALL-E 3 / DALL-E 2 / gpt-image-1 / gpt-image-2 (low/medium/high) *

- * 同步模式:直接返回图片 URL。 + * 同步模式:返回图片 URL(DALL-E 系列)或 base64 data URL(gpt-image-2 系列)。 * 复用已有的 OpenAI LLM provider 的 API Key。 * + *

gpt-image-2 三档质量做成 3 个虚拟 model ID(参考 hermes-agent + * plugins/image_gen/openai/__init__.py 的 model catalog 设计),让 picker + * 能直接选 fast/balanced/high。三档底层都打到 API model {@code "gpt-image-2"}, + * 区别仅在 {@code quality} 参数。 + * * @author MateClaw Team */ @Slf4j @@ -34,6 +40,20 @@ public class OpenAiImageProvider implements ImageGenerationProvider { private static final String DEFAULT_MODEL = "dall-e-3"; + /** + * gpt-image-2 真实 API model 名。三档虚拟 ID(gpt-image-2-low/medium/high) + * 在 submit 时全部打到这个 API model + 不同 quality 参数。 + */ + private static final String GPT_IMAGE_2_API_MODEL = "gpt-image-2"; + + /** gpt-image-2 系列虚拟 ID 列表 — 用于 capabilities 与分支判定。 */ + private static final List GPT_IMAGE_2_TIERS = + List.of("gpt-image-2-low", "gpt-image-2-medium", "gpt-image-2-high"); + + /** gpt-image-2 支持的尺寸(与 DALL-E 不同!1536x1024 / 1024x1024 / 1024x1536)。 */ + private static final List GPT_IMAGE_2_SIZES = + List.of("1024x1024", "1536x1024", "1024x1536"); + @Override public String id() { return "openai"; @@ -61,13 +81,28 @@ public class OpenAiImageProvider implements ImageGenerationProvider { @Override public ImageProviderCapabilities detailedCapabilities() { + // 合并 DALL-E 与 gpt-image-2 两套尺寸(去重)。运行时按选定 model + // 做尺寸校验,picker 只展示并集即可。 + List allSizes = new ArrayList<>(); + allSizes.add("1024x1024"); + allSizes.add("1024x1792"); // dall-e + allSizes.add("1792x1024"); // dall-e + allSizes.add("1024x1536"); // gpt-image-2 + allSizes.add("1536x1024"); // gpt-image-2 + + List models = new ArrayList<>(); + models.add("dall-e-3"); + models.add("dall-e-2"); + models.add("gpt-image-1"); + models.addAll(GPT_IMAGE_2_TIERS); // gpt-image-2-low/medium/high + return ImageProviderCapabilities.builder() .modes(capabilities()) - .supportedSizes(List.of("1024x1024", "1024x1792", "1792x1024")) + .supportedSizes(allSizes) .aspectRatios(List.of("1:1", "9:16", "16:9")) - .maxCount(1) // DALL-E 3 只支持 n=1 + .maxCount(1) // DALL-E 3 / gpt-image-2 都只支持 n=1 .defaultModel(DEFAULT_MODEL) - .models(List.of("dall-e-3", "dall-e-2", "gpt-image-1")) + .models(models) .build(); } @@ -89,15 +124,26 @@ public class OpenAiImageProvider implements ImageGenerationProvider { } try { - String model = request.getModel() != null && !request.getModel().isBlank() + String requestedModel = request.getModel() != null && !request.getModel().isBlank() ? request.getModel() : DEFAULT_MODEL; + // gpt-image-2 系列:三档虚拟 ID 全部打到 API model "gpt-image-2" + // + 对应 quality 参数;DALL-E 系列保留原行为。 + boolean isGptImage2 = GPT_IMAGE_2_TIERS.contains(requestedModel); ObjectNode body = objectMapper.createObjectNode(); - body.put("model", model); + body.put("model", isGptImage2 ? GPT_IMAGE_2_API_MODEL : requestedModel); body.put("prompt", request.getPrompt()); - body.put("size", normalizeSize(request.getSize(), request.getAspectRatio())); + body.put("size", normalizeSize(request.getSize(), request.getAspectRatio(), isGptImage2)); body.put("n", 1); - body.put("response_format", "url"); + + if (isGptImage2) { + // gpt-image-2 强制 b64_json,且 REJECT 任何 response_format 字段 + // (API 会以 unknown parameter 报错)。仅传 quality。 + body.put("quality", qualityForTier(requestedModel)); + } else { + // DALL-E 系列保留 URL 模式。 + body.put("response_format", "url"); + } String url = (baseUrl != null ? baseUrl : "https://api.openai.com") + "/v1/images/generations"; @@ -105,7 +151,8 @@ public class OpenAiImageProvider implements ImageGenerationProvider { .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .body(body.toString()) - .timeout(60_000) + // gpt-image-2 high 档官方文档约 ~2min;这里给到 180s 留余地 + .timeout(isGptImage2 ? 180_000 : 60_000) .execute(); JsonNode result = objectMapper.readTree(response.body()); @@ -113,15 +160,28 @@ public class OpenAiImageProvider implements ImageGenerationProvider { if (response.getStatus() == 200 && result.has("data")) { List imageUrls = new ArrayList<>(); for (JsonNode item : result.get("data")) { - String imageUrl = item.has("url") ? item.get("url").asText() : null; - if (imageUrl != null) { - imageUrls.add(imageUrl); + if (isGptImage2) { + // gpt-image-2 永远返回 b64_json。包成 data URL,交给前端 + // 直接 渲染,沿用 + // GoogleImagenProvider / MiniMaxImageProvider 的现成模式。 + String b64 = item.has("b64_json") ? item.get("b64_json").asText() : null; + if (b64 != null && !b64.isBlank()) { + imageUrls.add("data:image/png;base64," + b64); + } + } else { + String imageUrl = item.has("url") ? item.get("url").asText() : null; + if (imageUrl != null) { + imageUrls.add(imageUrl); + } } } if (imageUrls.isEmpty()) { - return ImageSubmitResult.failure(id(), "API 返回成功但未包含图片 URL"); + return ImageSubmitResult.failure(id(), + isGptImage2 ? "API 返回成功但未包含 b64_json 图片数据" + : "API 返回成功但未包含图片 URL"); } - log.info("[OpenAI Image] Generated {} image(s) (model={})", imageUrls.size(), model); + log.info("[OpenAI Image] Generated {} image(s) (model={})", + imageUrls.size(), requestedModel); return ImageSubmitResult.syncSuccess(id(), imageUrls); } else { String errMsg = result.has("error") @@ -136,6 +196,24 @@ public class OpenAiImageProvider implements ImageGenerationProvider { } } + /** Map gpt-image-2-{low|medium|high} → quality string sent to API. + * Package-private + static for unit testability. */ + static String qualityForTier(String tierModelId) { + return switch (tierModelId) { + case "gpt-image-2-low" -> "low"; + case "gpt-image-2-high" -> "high"; + default -> "medium"; // gpt-image-2-medium + 任何未来 tier 都默认 medium + }; + } + + /** Returns true if the given model id is a gpt-image-2 virtual tier. + * Package-private + static for unit testability. + *

Null-safe: {@code List.of(...).contains(null)} throws NPE, which we + * pre-empt with an explicit null check. */ + static boolean isGptImage2Tier(String modelId) { + return modelId != null && GPT_IMAGE_2_TIERS.contains(modelId); + } + private String getOpenAiApiKey() { try { var providerEntity = modelProviderService.getProviderConfig("openai"); @@ -154,14 +232,34 @@ public class OpenAiImageProvider implements ImageGenerationProvider { } } - private String normalizeSize(String size, String aspectRatio) { - // 优先使用 size - if (size != null && !size.isBlank()) { - List supported = List.of("1024x1024", "1024x1792", "1792x1024"); - if (supported.contains(size)) return size; + /** + * 按 model 选合适的尺寸集合: + *

+ */ + /** Package-private + static-ish for unit testability. Kept instance-method to + * stay close to the call site — no instance state is touched. */ + String normalizeSize(String size, String aspectRatio, boolean isGptImage2) { + List supported = isGptImage2 + ? GPT_IMAGE_2_SIZES + : List.of("1024x1024", "1024x1792", "1792x1024"); + + // 优先使用 size(如果在该 model 支持范围内) + if (size != null && !size.isBlank() && supported.contains(size)) { + return size; } - // 根据 aspectRatio 推断 + + // 根据 aspectRatio 推断(gpt-image-2 与 DALL-E 的竖图/横图尺寸不一样) if (aspectRatio != null) { + if (isGptImage2) { + return switch (aspectRatio) { + case "9:16", "2:3", "3:4" -> "1024x1536"; + case "16:9", "3:2", "4:3" -> "1536x1024"; + default -> "1024x1024"; + }; + } return switch (aspectRatio) { case "9:16" -> "1024x1792"; case "16:9" -> "1792x1024"; @@ -170,4 +268,10 @@ public class OpenAiImageProvider implements ImageGenerationProvider { } return "1024x1024"; } + + // 保留旧签名给可能存在的其它 caller(向后兼容)。新增 boolean 默认 false (DALL-E)。 + @SuppressWarnings("unused") + private String normalizeSize(String size, String aspectRatio) { + return normalizeSize(size, aspectRatio, false); + } }