mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
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:
@@ -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 3–4+ 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.",
|
||||
|
||||
@@ -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 (3–4+ 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({
|
||||
|
||||
@@ -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 1–2 only. Sending 3–4+ (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,
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user