diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 6aeb5aba3..dd212d755 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1499,6 +1499,8 @@ export async function POST(request: NextRequest) { : undefined, scaleAdjustPreferSameNameProviderFailover: isScaleAdjustOperation && Boolean(scaleAdjustModelFamily), + scaleAdjustStrictSameFamilyFailover: + isScaleAdjustOperation && Boolean(scaleAdjustModelFamily), manualRegionReferenceImageCount: manualRegionReferenceImageUrls.length, regionReferenceImageCount: referenceImages.length, diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 03541d2dd..1e4ffb1b1 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -1737,6 +1737,39 @@ function normalizeModelNameForSimilarity(value: unknown) { .replace(/\s+/g, " "); } +function resolveCanvasScaleAdjustModelFamilyFromText(value: unknown) { + const normalized = normalizeModelNameForSimilarity(value); + const compact = normalized.replace(/[^a-z0-9]+/g, ""); + if (!normalized && !compact) return ""; + if ( + normalized.includes("gemini-3-1-flash-image-preview") || + normalized.includes("gemini-3.1-flash-image-preview") || + normalized.includes("nano-banana-2") || + compact.includes("nanobanana2") + ) { + return "nano-banana-2"; + } + if ( + normalized.includes("gemini-3-pro-image-preview") || + normalized.includes("nano-banana-pro") || + compact.includes("nanobananapro") + ) { + return "nano-banana-pro"; + } + if (normalized.includes("gpt-image-2") || compact.includes("gptimage2")) { + return "gpt-image-2"; + } + return ""; +} + +function getCanvasScaleAdjustModelFamily(item: CanvasModelWithProvider) { + return ( + resolveCanvasScaleAdjustModelFamilyFromText(item.model.modelId) || + resolveCanvasScaleAdjustModelFamilyFromText(item.model.displayName) || + resolveCanvasScaleAdjustModelFamilyFromText(item.model.id) + ); +} + function tokenizeModelNameForSimilarity(value: unknown) { const normalized = normalizeModelNameForSimilarity(value); const compact = normalized.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, ""); @@ -1795,10 +1828,17 @@ function findCanvasFailoverModel(params: { failedModelRefs: string[]; preferLowCostSameNameProviders?: boolean; requiresImageInput?: boolean; + scaleAdjustModelFamily?: string | null; }) { const failedRefs = new Set( params.failedModelRefs.map((item) => String(item || "").trim()).filter(Boolean) ); + const strictScaleAdjustModelFamily = + params.preferLowCostSameNameProviders && + typeof params.scaleAdjustModelFamily === "string" && + params.scaleAdjustModelFamily.trim() + ? params.scaleAdjustModelFamily.trim() + : ""; const selectedFallbackRefs = [ params.selectedModel.model.fallbackModelId1, params.selectedModel.model.fallbackModelId2, @@ -1807,10 +1847,17 @@ function findCanvasFailoverModel(params: { .filter(Boolean); const isUsableFallback = (item: CanvasModelWithProvider) => { + const isSameFamilyScaleAdjustFallback = strictScaleAdjustModelFamily + ? getCanvasScaleAdjustModelFamily(item) === strictScaleAdjustModelFamily + : false; const isSameNameScaleAdjustFallback = params.preferLowCostSameNameProviders && - scoreModelNameSimilarity(params.selectedModel, item) >= - CANVAS_FAILOVER_SAME_NAME_THRESHOLD; + (isSameFamilyScaleAdjustFallback || + scoreModelNameSimilarity(params.selectedModel, item) >= + CANVAS_FAILOVER_SAME_NAME_THRESHOLD); + if (strictScaleAdjustModelFamily && !isSameFamilyScaleAdjustFallback) { + return false; + } if ( !item.model.isEnabled || item.model.isSubModel || @@ -1849,6 +1896,18 @@ function findCanvasFailoverModel(params: { sameProviderType: item.provider.type === params.selectedModel.provider.type, })) .sort((left, right) => { + if (strictScaleAdjustModelFamily) { + const leftPriority = getCanvasScaleAdjustFailoverPriority(left.item); + const rightPriority = getCanvasScaleAdjustFailoverPriority(right.item); + if (leftPriority !== rightPriority) return leftPriority - rightPriority; + if (right.nameSimilarity !== left.nameSimilarity) { + return right.nameSimilarity - left.nameSimilarity; + } + if (left.sameProviderType !== right.sameProviderType) { + return left.sameProviderType ? -1 : 1; + } + return left.index - right.index; + } const leftSameName = left.nameSimilarity >= CANVAS_FAILOVER_SAME_NAME_THRESHOLD; const rightSameName = @@ -1922,6 +1981,10 @@ export async function generateCanvasImage( request.debugMeta && typeof request.debugMeta === "object" ? request.debugMeta : undefined; + const scaleAdjustModelFamily = + typeof debugMeta?.scaleAdjustModelFamily === "string" + ? debugMeta.scaleAdjustModelFamily.trim() + : ""; const localEditComposite = normalizeLocalEditCompositeConfig( request.localEditComposite ); @@ -1974,6 +2037,7 @@ export async function generateCanvasImage( debugMeta?.scaleAdjustPreferSameNameProviderFailover ), requiresImageInput, + scaleAdjustModelFamily, }); if (!imageInputFallback) { @@ -2578,6 +2642,7 @@ export async function generateCanvasImage( debugMeta?.scaleAdjustPreferSameNameProviderFailover ), requiresImageInput: normalizedReferenceImages.length > 0, + scaleAdjustModelFamily, }) : null;