fix(adobe-firefly): cap gpt-image refs at 2 + adaptive poll timeout (#8870)

Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
This commit is contained in:
NOXX - Commiter
2026-08-06 05:52:44 +03:00
committed by GitHub
parent 63c484d062
commit 4f89f2b7bf
4 changed files with 136 additions and 19 deletions

View File

@@ -359,7 +359,7 @@
"open-sse/mcp-server/server.ts": 1407,
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
"open-sse/services/accountFallback.ts": 1966,
"open-sse/services/adobeFireflyClient.ts": 2322,
"open-sse/services/adobeFireflyClient.ts": 2385,
"open-sse/services/claudeCodeCompatible.ts": 1202,
"open-sse/services/combo.ts": 3648,
"open-sse/services/compression/strategySelector.ts": 1060,
@@ -415,6 +415,7 @@
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file.",
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_28_8870_firefly_ref_cap_timeout": "PR #8870 (fix/adobe-firefly-gpt-ref-cap-timeout) own growth: adobeFireflyClient.ts 2322->2385 (+63 = gpt-image subject-ref hard cap at 2 + adaptive poll timeout budget (base 300s + 60s/ref, max 600s) + defensive .slice on referenceBlobs for gpt/nano/generic families). Fixes live 504s on multi-screenshot listing jobs (Featured Promo / Box Art) where 34+ subject refs stall colligo until the old 180s poll budget expires. Helpers adobeFireflyMaxImageRefs/adobeFireflyImageTimeoutMs live next to the existing payload/poll chokepoint (not extractable without splitting the wire recipe mid-PR). Covered by tests/unit/adobe-firefly.test.ts (ref-cap + timeout cases). Structural shrink tracked in #3501.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_01_8964_xai_agent_tools": "PR #8964 own growth: chatCore.ts 5020->5034 at the existing native-passthrough chokepoint. Adds xAI Agent Tools passthrough for /v1/responses (xai/xai-oauth/xao): resolve nativeXaiResponsesPassthrough, force openai-responses targetFormat, stamp body marker, and OR into the existing nativeCodexPassthrough sites (web-search bypass + requestEndpointPath). Leaf logic in passthroughHelpers, responsesEndpoint, targetFormat, xai executor, responseSanitizer, usageTracking. Cohesive wiring at the Codex passthrough boundary.",
"_rebaseline_2026_08_01_8964_response_sanitizer": "PR #8964 own growth: responseSanitizer.ts 1115->1128. Keep cost_in_usd_ticks / server_side_tool_usage(_details) through sanitizeResponsesApiResponse allowlists so native xAI tool responses retain usage.",

View File

@@ -15,16 +15,13 @@ import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGenerat
import {
AdobeFireflyError,
adobeFireflyGenerateImage,
adobeFireflyImageTimeoutMs,
adobeFireflyMaxImageRefs,
resolveAdobeAccessToken,
resolveAdobeSourceImageIds,
resolveAdobeImageModel,
} from "../../../services/adobeFireflyClient.ts";
function normalizePositiveNumber(value: unknown, fallback: number): number {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
export async function handleAdobeFireflyImageGeneration({
model,
provider,
@@ -69,7 +66,6 @@ export async function handleAdobeFireflyImageGeneration({
try {
const accessToken = await resolveAdobeAccessToken(credentials, fetchImpl);
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 180_000);
const seed =
typeof body.seed === "number"
? body.seed
@@ -87,12 +83,10 @@ export async function handleAdobeFireflyImageGeneration({
? credentials.accessToken
: undefined);
// Cap uploads by model family (matches MediaViewModel GetSourceImageLimit).
// Cap uploads by model family. gpt-image: 2 subject refs max (34+ stalls colligo → 504).
// nano: 4 general refs for multi-panel composition.
const { id: resolvedId } = resolveAdobeImageModel(model);
const maxRefs =
resolvedId.includes("nano-banana") || resolvedId.includes("gpt-image")
? 4
: 2;
const maxRefs = adobeFireflyMaxImageRefs(resolvedId);
const sourceImageIds = await resolveAdobeSourceImageIds({
accessToken,
@@ -104,10 +98,22 @@ export async function handleAdobeFireflyImageGeneration({
log,
});
const explicitTimeout =
typeof body.timeout_ms === "number"
? body.timeout_ms
: typeof body.timeout_ms === "string" && body.timeout_ms.trim()
? Number(body.timeout_ms)
: undefined;
const timeoutMs = adobeFireflyImageTimeoutMs({
timeoutMs: explicitTimeout,
refCount: sourceImageIds.length,
});
log?.info?.(
"IMAGE",
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
(sourceImageIds.length ? ` | refs: ${sourceImageIds.length}` : "")
(sourceImageIds.length ? ` | refs: ${sourceImageIds.length}/${maxRefs}` : "") +
` | pollTimeoutMs=${timeoutMs}`
);
const result = await adobeFireflyGenerateImage({

View File

@@ -49,11 +49,57 @@ const DEFAULT_USER_AGENT =
const DEFAULT_SEC_CH_UA =
'"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"';
const DEFAULT_POLL_INTERVAL_MS = 3000;
const DEFAULT_IMAGE_TIMEOUT_MS = 180_000;
/**
* Poll budget for image generate-async. Multi-ref gpt-image / nano jobs commonly
* exceed 3 minutes (upload + colligo + render at detailLevel 5). 180s was the
* previous default and produced widespread 504s on listing assets with screenshots.
*/
export const DEFAULT_IMAGE_TIMEOUT_MS = 300_000;
const DEFAULT_VIDEO_TIMEOUT_MS = 300_000;
/** Extra poll budget per uploaded reference blob (large screenshots + image2image). */
export const ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS = 60_000;
export const ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS = 600_000;
/**
* gpt-image family accepts subject refs, but the live SPA and colligo are reliable
* with 12 only. Sending 34+ (e.g. Store listing "5 screenshots") often hangs until
* poll timeout. Nano multi-ref composition supports more via usage "general".
*/
export const ADOBE_FIREFLY_GPT_IMAGE_MAX_REFS = 2;
export const ADOBE_FIREFLY_NANO_MAX_REFS = 4;
export const ADOBE_FIREFLY_GENERIC_IMAGE_MAX_REFS = 2;
const FIREFLY_ORIGIN = "https://firefly.adobe.com";
const FIREFLY_REFERER = "https://firefly.adobe.com/";
/** Cap reference uploads by Firefly image model family. */
export function adobeFireflyMaxImageRefs(model: string): number {
const raw = String(model || "").toLowerCase();
if (raw.includes("nano-banana") || raw.includes("nanobanana") || raw.includes("gemini-flash")) {
return ADOBE_FIREFLY_NANO_MAX_REFS;
}
if (raw.includes("gpt-image") || raw.includes("gptimage")) {
return ADOBE_FIREFLY_GPT_IMAGE_MAX_REFS;
}
return ADOBE_FIREFLY_GENERIC_IMAGE_MAX_REFS;
}
/**
* Resolve poll timeout: explicit body.timeout_ms wins; else base + per-ref budget.
* Covers multi-screenshot listing jobs without unbounded waits.
*/
export function adobeFireflyImageTimeoutMs(opts?: {
timeoutMs?: number;
refCount?: number;
}): number {
const explicit = Number(opts?.timeoutMs);
if (Number.isFinite(explicit) && explicit > 0) {
return Math.min(ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS, Math.floor(explicit));
}
const refs = Math.max(0, Math.floor(Number(opts?.refCount) || 0));
const budget =
DEFAULT_IMAGE_TIMEOUT_MS + refs * ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS;
return Math.min(ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS, budget);
}
export type AdobeFireflyImageModelId =
| "nano-banana-pro"
| "nano-banana"
@@ -716,9 +762,14 @@ export function buildAdobeImagePayload(opts: {
};
if (opts.sourceImageIds?.length) {
// gpt-image subject references (mask path uses separate mask blob when present).
// Cap to wire-stable count — extra subject blobs stall colligo until poll 504.
const refIds = opts.sourceImageIds
.map((id) => String(id))
.filter(Boolean)
.slice(0, ADOBE_FIREFLY_GPT_IMAGE_MAX_REFS);
payload.generationMetadata = { module: "image2image", submodule: "ff-image-generate" };
payload.referenceBlobs = opts.sourceImageIds.map((id) => ({
id: String(id),
payload.referenceBlobs = refIds.map((id) => ({
id,
usage: "subject",
}));
payload.modelSpecificPayload = {};
@@ -751,8 +802,16 @@ export function buildAdobeImagePayload(opts: {
if (Object.keys(genSettings).length) payload.generationSettings = genSettings;
if (opts.sourceImageIds?.length) {
payload.referenceBlobs = opts.sourceImageIds.map((id) => ({
id: String(id),
const maxRefs =
opts.modelSpec.family === "generic"
? ADOBE_FIREFLY_GENERIC_IMAGE_MAX_REFS
: ADOBE_FIREFLY_NANO_MAX_REFS;
const refIds = opts.sourceImageIds
.map((id) => String(id))
.filter(Boolean)
.slice(0, maxRefs);
payload.referenceBlobs = refIds.map((id) => ({
id,
usage: "general",
}));
// Flux / Seedream / Runway image historically used image2image; nano keeps text2image.
@@ -2167,11 +2226,15 @@ export async function adobeFireflyGenerateImage(opts: {
}
pollUrl = normalizeAdobePollUrl(pollUrl);
const pollTimeoutMs = adobeFireflyImageTimeoutMs({
timeoutMs: opts.timeoutMs,
refCount: opts.sourceImageIds?.length ?? 0,
});
const { mediaUrl, latest } = await pollAdobeJob({
pollUrl,
accessToken: opts.accessToken,
kind: "image",
timeoutMs: opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_IMAGE_TIMEOUT_MS,
timeoutMs: pollTimeoutMs,
fetchImpl,
log: opts.log,
});

View File

@@ -4,8 +4,13 @@ import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
import {
ADOBE_FIREFLY_IMAGE_MODELS,
ADOBE_FIREFLY_VIDEO_MODELS,
ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS,
ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS,
DEFAULT_IMAGE_TIMEOUT_MS,
adobeFireflyApiKey,
adobeFireflyBalanceApiKey,
adobeFireflyImageTimeoutMs,
adobeFireflyMaxImageRefs,
buildAdobeImagePayload,
buildAdobePollHeaders,
buildAdobeSubmitHeaders,
@@ -269,6 +274,48 @@ test("buildAdobeImagePayload attaches referenceBlobs like live adobe_atach_image
(gpt.generationMetadata as Record<string, unknown>).module,
"image2image"
);
// gpt-image: only first 2 subject refs survive (extra screenshots hang colligo).
const gptMany = buildAdobeImagePayload({
prompt: "edit me",
aspectRatio: "1:1",
outputResolution: "1K",
modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["gpt-image-2"],
sourceImageIds: ["id-1", "id-2", "id-3", "id-4", "id-5"],
});
assert.deepEqual(gptMany.referenceBlobs, [
{ id: "id-1", usage: "subject" },
{ id: "id-2", usage: "subject" },
]);
// nano keeps up to 4 general refs for multi-panel composition.
const nanoMany = buildAdobeImagePayload({
prompt: "compose",
aspectRatio: "16:9",
outputResolution: "2K",
modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-2"],
sourceImageIds: ["a", "b", "c", "d", "e"],
});
assert.equal((nanoMany.referenceBlobs as unknown[]).length, 4);
assert.equal((nanoMany.referenceBlobs as Array<{ usage: string }>)[0].usage, "general");
});
test("adobeFireflyMaxImageRefs + adaptive image timeout", () => {
assert.equal(adobeFireflyMaxImageRefs("gpt-image-2"), 2);
assert.equal(adobeFireflyMaxImageRefs("adobe-firefly/gpt-image"), 2);
assert.equal(adobeFireflyMaxImageRefs("nano-banana-2"), 4);
assert.equal(adobeFireflyMaxImageRefs("flux-2"), 2);
assert.equal(adobeFireflyImageTimeoutMs({ refCount: 0 }), DEFAULT_IMAGE_TIMEOUT_MS);
assert.equal(
adobeFireflyImageTimeoutMs({ refCount: 2 }),
DEFAULT_IMAGE_TIMEOUT_MS + 2 * ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS
);
assert.equal(adobeFireflyImageTimeoutMs({ timeoutMs: 120_000, refCount: 5 }), 120_000);
assert.equal(
adobeFireflyImageTimeoutMs({ refCount: 99 }),
ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS
);
});
test("extractAdobeSourceImageSources reads Media page image fields", () => {