mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
/**
|
|
* Adobe Firefly credits balance → UsageQuota for Limits page.
|
|
*
|
|
* Live capture (adobe/balance.txt):
|
|
* GET https://firefly.adobe.io/v1/credits/balance
|
|
* Authorization: Bearer <IMS access_token>
|
|
* x-api-key: SunbreakWebUI1
|
|
* x-account-id: <user_id from JWT>
|
|
*
|
|
* Response shape:
|
|
* {
|
|
* total: { quota: { total, used, available }, availableUntil },
|
|
* credits: {
|
|
* firefly_free_credit: { quota: { total, used, available } },
|
|
* firefly_plan_credit: { quota: { total, used, available } }
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
import {
|
|
fetchAdobeCreditsBalance,
|
|
parseAdobeCreditsBalance,
|
|
resolveAdobeAccessToken,
|
|
type AdobeFireflyCreditsBalance,
|
|
} from "../adobeFireflyClient.ts";
|
|
import { type UsageQuota, parseResetTime } from "./quota.ts";
|
|
|
|
export { parseAdobeCreditsBalance };
|
|
|
|
function oneQuota(
|
|
used: number,
|
|
total: number,
|
|
remaining: number,
|
|
resetAt: string | null,
|
|
displayName: string
|
|
): UsageQuota {
|
|
const t = Math.max(0, total);
|
|
const u = Math.max(0, Math.min(t, used));
|
|
const r = remaining > 0 ? remaining : Math.max(0, t - u);
|
|
const remainingPercentage =
|
|
t > 0 ? Math.round((r / t) * 1000) / 10 : r > 0 ? 100 : 0;
|
|
return {
|
|
used: u,
|
|
total: t,
|
|
remaining: r,
|
|
remainingPercentage,
|
|
resetAt,
|
|
unlimited: false,
|
|
displayName,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build a **Record** of quotas (NOT an array). providerLimits only caches when
|
|
* `isRecord(usage.quotas)` is true — arrays are ignored and Limits stays empty.
|
|
*/
|
|
export function buildAdobeFireflyCreditsQuota(
|
|
balance: AdobeFireflyCreditsBalance
|
|
): UsageQuota {
|
|
const resetAt = parseResetTime(balance.availableUntil);
|
|
return oneQuota(
|
|
balance.used,
|
|
balance.total,
|
|
balance.remaining,
|
|
resetAt,
|
|
"Firefly credits"
|
|
);
|
|
}
|
|
|
|
export function buildAdobeFireflyQuotasRecord(
|
|
balance: AdobeFireflyCreditsBalance
|
|
): Record<string, UsageQuota> {
|
|
const resetAt = parseResetTime(balance.availableUntil);
|
|
const quotas: Record<string, UsageQuota> = {};
|
|
|
|
// Aggregate first (what Limits primarily shows)
|
|
if (balance.total > 0 || balance.remaining > 0) {
|
|
quotas["firefly_total"] = oneQuota(
|
|
balance.used,
|
|
balance.total,
|
|
balance.remaining,
|
|
resetAt,
|
|
"Firefly credits"
|
|
);
|
|
}
|
|
if (balance.freeTotal > 0) {
|
|
quotas["firefly_free"] = oneQuota(
|
|
balance.freeUsed,
|
|
balance.freeTotal,
|
|
balance.freeRemaining,
|
|
resetAt,
|
|
"Free credits"
|
|
);
|
|
}
|
|
if (balance.planTotal > 0) {
|
|
quotas["firefly_plan"] = oneQuota(
|
|
balance.planUsed,
|
|
balance.planTotal,
|
|
balance.planRemaining,
|
|
resetAt,
|
|
"Plan credits"
|
|
);
|
|
}
|
|
|
|
// Fallback if all zeros but we still got a parse
|
|
if (Object.keys(quotas).length === 0) {
|
|
quotas["firefly_total"] = oneQuota(0, 0, 0, resetAt, "Firefly credits");
|
|
}
|
|
return quotas;
|
|
}
|
|
|
|
export async function getAdobeFireflyUsage(
|
|
apiKey?: string,
|
|
accessToken?: string,
|
|
providerSpecificData?: Record<string, unknown> | null,
|
|
fetchImpl: typeof fetch = fetch
|
|
): Promise<
|
|
| { quotas: Record<string, UsageQuota>; plan?: string }
|
|
| { message: string }
|
|
> {
|
|
try {
|
|
const token = await resolveAdobeAccessToken(
|
|
{
|
|
apiKey,
|
|
accessToken,
|
|
providerSpecificData: providerSpecificData as {
|
|
cookie?: unknown;
|
|
access_token?: unknown;
|
|
accessToken?: unknown;
|
|
} | null,
|
|
},
|
|
fetchImpl
|
|
);
|
|
const balance = await fetchAdobeCreditsBalance(token, fetchImpl);
|
|
if (balance.total <= 0 && balance.remaining <= 0 && balance.planTotal <= 0 && balance.freeTotal <= 0) {
|
|
return {
|
|
message:
|
|
"Adobe Firefly returned an empty credits balance. Paste a fresh IMS access_token JWT (Authorization: Bearer on firefly-3p generate/discovery) from a signed-in session — not firefly.adobe.com page cookies alone.",
|
|
};
|
|
}
|
|
return {
|
|
quotas: buildAdobeFireflyQuotasRecord(balance),
|
|
plan: balance.planTotal > 0 ? "Firefly plan" : "Firefly free",
|
|
};
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
// Surface a short Limits-friendly message for guest/cookie failures
|
|
if (/guest|GUEST|Bearer|session cookies are empty|token invalid/i.test(msg)) {
|
|
return {
|
|
message:
|
|
"Adobe Firefly Limits need a signed-in IMS JWT. Open firefly.adobe.com → F12 → Network → firefly-3p → Authorization → copy token after Bearer (eyJ…). Page Cookie alone only mints a guest token.",
|
|
};
|
|
}
|
|
return { message: msg || "Failed to fetch Adobe Firefly credits balance" };
|
|
}
|
|
}
|