fix: prevent unsupported seedream size and lock size when model disallows
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-20 18:14:09 +08:00
parent b3ce87ca2b
commit b28f1013da
2 changed files with 63 additions and 23 deletions

View File

@ -102,13 +102,18 @@ export function ImageParamsBar({
// Gemini:1~2 张 // Gemini:1~2 张
return [1, 2]; return [1, 2];
}; };
const supportsSize = imageGenConfig?.supportsSize !== false;
const clampParamsByProvider = (raw: ImageGenerationParams): ImageGenerationParams => { const clampParamsByProvider = (raw: ImageGenerationParams): ImageGenerationParams => {
const options = getImageCountOptions(); const options = getImageCountOptions();
const fallbackCount = options[0] ?? 1; const fallbackCount = options[0] ?? 1;
const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount; const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount;
const imageSize = supportsSize
? (raw.imageSize || "2K")
: "2K";
return { return {
...raw, ...raw,
imageSize,
imageCount, imageCount,
}; };
}; };
@ -204,26 +209,37 @@ export function ImageParamsBar({
</DropdownMenu> </DropdownMenu>
{/* 分辨率 */} {/* 分辨率 */}
<DropdownMenu> {supportsSize ? (
<DropdownMenuTrigger asChild> <DropdownMenu>
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"> <DropdownMenuTrigger asChild>
<Maximize2 size={14} /> <Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
<span className="font-medium text-xs">{params.imageSize}</span> <Maximize2 size={14} />
</Button> <span className="font-medium text-xs">{params.imageSize}</span>
</DropdownMenuTrigger> </Button>
<DropdownMenuContent align="center" className="min-w-[100px]"> </DropdownMenuTrigger>
{IMAGE_SIZE_OPTIONS.map((option) => ( <DropdownMenuContent align="center" className="min-w-[100px]">
<DropdownMenuItem {IMAGE_SIZE_OPTIONS.map((option) => (
key={option.value} <DropdownMenuItem
onClick={() => updateParam("imageSize", option.value)} key={option.value}
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0" onClick={() => updateParam("imageSize", option.value)}
> className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
<Check className={cn("mr-2 h-4 w-4", params.imageSize === option.value ? "opacity-100" : "opacity-0")} /> >
{option.label} <Check className={cn("mr-2 h-4 w-4", params.imageSize === option.value ? "opacity-100" : "opacity-0")} />
</DropdownMenuItem> {option.label}
))} </DropdownMenuItem>
</DropdownMenuContent> ))}
</DropdownMenu> </DropdownMenuContent>
</DropdownMenu>
) : (
<Button
variant="ghost"
className="h-8 px-2 gap-1 text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
disabled
>
<Maximize2 size={14} />
<span className="font-medium text-xs">2K</span>
</Button>
)}
{showImageCountSelector && ( {showImageCountSelector && (
<DropdownMenu> <DropdownMenu>

View File

@ -18,6 +18,7 @@ let cachedRewriteConfig: VolcanoUrlRewriteConfig | null | undefined;
const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000; const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000;
const VOLCANO_INLINE_MAX_SIDE = 1024; const VOLCANO_INLINE_MAX_SIDE = 1024;
const DEFAULT_VOLCANO_IMAGE_SIZE = "2K"; const DEFAULT_VOLCANO_IMAGE_SIZE = "2K";
const SEEDREAM_MODEL_ID_PATTERN = /doubao-seedream-4-5/i;
function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" { function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" {
const normalized = String(imageSize || "").trim().toUpperCase(); const normalized = String(imageSize || "").trim().toUpperCase();
@ -27,6 +28,23 @@ function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" {
return DEFAULT_VOLCANO_IMAGE_SIZE; return DEFAULT_VOLCANO_IMAGE_SIZE;
} }
function resolveVolcanoImageSizeForModel(
modelId: string,
imageSize?: string
): "1K" | "2K" | "4K" {
const normalized = normalizeVolcanoImageSize(imageSize);
if (SEEDREAM_MODEL_ID_PATTERN.test(modelId)) {
// doubao-seedream-4-5 在当前链路下 1K 会返回 InvalidParameter,强制回落到 2K
if (normalized === "1K") {
console.log(
`[Volcano Vision] Size normalized for ${modelId}: 1K -> 2K (model constraint)`
);
return "2K";
}
}
return normalized;
}
function guessImageMimeTypeFromUrl(url: string): string { function guessImageMimeTypeFromUrl(url: string): string {
const cleanUrl = url.split("?")[0]?.toLowerCase() || ""; const cleanUrl = url.split("?")[0]?.toLowerCase() || "";
if (cleanUrl.endsWith(".png")) return "image/png"; if (cleanUrl.endsWith(".png")) return "image/png";
@ -478,7 +496,7 @@ async function sendVolcanoRequest(
console.log("[Volcano Vision] Image count:", image ? (Array.isArray(image) ? image.length : 1) : 0); console.log("[Volcano Vision] Image count:", image ? (Array.isArray(image) ? image.length : 1) : 0);
// 构建请求体 // 构建请求体
const normalizedSize = normalizeVolcanoImageSize(imageSize); const normalizedSize = resolveVolcanoImageSizeForModel(modelId, imageSize);
const requestBody: any = { const requestBody: any = {
model: modelId, model: modelId,
prompt, prompt,
@ -711,7 +729,10 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
const activeModelId = options.overrideModelId || modelId; const activeModelId = options.overrideModelId || modelId;
console.log("[Volcano Vision] Using modelId:", activeModelId); console.log("[Volcano Vision] Using modelId:", activeModelId);
console.log("[Volcano Vision] Image params from options:", options.imageParams); console.log("[Volcano Vision] Image params from options:", options.imageParams);
const imageSize = normalizeVolcanoImageSize(options.imageParams?.imageSize); const imageSize = resolveVolcanoImageSizeForModel(
activeModelId,
options.imageParams?.imageSize
);
// 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张 // 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张
const imageCount = options.imageParams?.imageCount || options.maxImages || 1; const imageCount = options.imageParams?.imageCount || options.maxImages || 1;
@ -772,7 +793,10 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
const allImages: Array<{ thumbnail: string; original: string }> = []; const allImages: Array<{ thumbnail: string; original: string }> = [];
// 使用流式请求 // 使用流式请求
const imageSize = normalizeVolcanoImageSize(options.imageParams?.imageSize); const imageSize = resolveVolcanoImageSizeForModel(
modelId,
options.imageParams?.imageSize
);
for await (const event of sendVolcanoStreamRequest(baseURL, apiKey, options.messages, modelId, imageSize)) { for await (const event of sendVolcanoStreamRequest(baseURL, apiKey, options.messages, modelId, imageSize)) {
if (event.type === "image_generation.partial_succeeded" && event.url) { if (event.type === "image_generation.partial_succeeded" && event.url) {
// 每张图片生成完成后立即上传并发送 // 每张图片生成完成后立即上传并发送