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 张
return [1, 2];
};
const supportsSize = imageGenConfig?.supportsSize !== false;
const clampParamsByProvider = (raw: ImageGenerationParams): ImageGenerationParams => {
const options = getImageCountOptions();
const fallbackCount = options[0] ?? 1;
const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount;
const imageSize = supportsSize
? (raw.imageSize || "2K")
: "2K";
return {
...raw,
imageSize,
imageCount,
};
};
@ -204,26 +209,37 @@ export function ImageParamsBar({
</DropdownMenu>
{/* 分辨率 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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">
<Maximize2 size={14} />
<span className="font-medium text-xs">{params.imageSize}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="min-w-[100px]">
{IMAGE_SIZE_OPTIONS.map((option) => (
<DropdownMenuItem
key={option.value}
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}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{supportsSize ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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">
<Maximize2 size={14} />
<span className="font-medium text-xs">{params.imageSize}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="min-w-[100px]">
{IMAGE_SIZE_OPTIONS.map((option) => (
<DropdownMenuItem
key={option.value}
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}
</DropdownMenuItem>
))}
</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 && (
<DropdownMenu>

View File

@ -18,6 +18,7 @@ let cachedRewriteConfig: VolcanoUrlRewriteConfig | null | undefined;
const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000;
const VOLCANO_INLINE_MAX_SIDE = 1024;
const DEFAULT_VOLCANO_IMAGE_SIZE = "2K";
const SEEDREAM_MODEL_ID_PATTERN = /doubao-seedream-4-5/i;
function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" {
const normalized = String(imageSize || "").trim().toUpperCase();
@ -27,6 +28,23 @@ function normalizeVolcanoImageSize(imageSize?: string): "1K" | "2K" | "4K" {
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 {
const cleanUrl = url.split("?")[0]?.toLowerCase() || "";
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);
// 构建请求体
const normalizedSize = normalizeVolcanoImageSize(imageSize);
const normalizedSize = resolveVolcanoImageSizeForModel(modelId, imageSize);
const requestBody: any = {
model: modelId,
prompt,
@ -711,7 +729,10 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
const activeModelId = options.overrideModelId || modelId;
console.log("[Volcano Vision] Using modelId:", activeModelId);
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 张
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 imageSize = normalizeVolcanoImageSize(options.imageParams?.imageSize);
const imageSize = resolveVolcanoImageSizeForModel(
modelId,
options.imageParams?.imageSize
);
for await (const event of sendVolcanoStreamRequest(baseURL, apiKey, options.messages, modelId, imageSize)) {
if (event.type === "image_generation.partial_succeeded" && event.url) {
// 每张图片生成完成后立即上传并发送