Repository navigation
feat: add Freepik (Magnific Mystic) image generation provider (#6654) - #7597
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the Freepik (Magnific Mystic) image generation provider, implementing an asynchronous submit-and-poll flow, registering the provider configuration, and adding unit tests. The review feedback highlights a security concern where raw fetch is used to download the generated image, bypassing SSRF and DNS-rebinding protections; it is recommended to use fetchRemoteImage instead. Additionally, the feedback points out that the polling logic in pollMysticTask should check res.ok to handle API errors gracefully and avoid potential infinite polling loops.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import { saveCallLog } from "@/lib/usageDb"; | ||
| import { sleep } from "../../../utils/sleep.ts"; | ||
| import { sanitizeErrorMessage } from "../../../utils/error.ts"; |
There was a problem hiding this comment.
To ensure proper security controls (such as SSRF protection and DNS-rebinding guards) are applied when downloading the generated image, we should import fetchRemoteImage from @/shared/network/remoteImageFetch instead of using raw fetch.
| import { saveCallLog } from "@/lib/usageDb"; | |
| import { sleep } from "../../../utils/sleep.ts"; | |
| import { sanitizeErrorMessage } from "../../../utils/error.ts"; | |
| import { saveCallLog } from "@/lib/usageDb"; | |
| import { sleep } from "../../../utils/sleep.ts"; | |
| import { sanitizeErrorMessage } from "../../../utils/error.ts"; | |
| import { fetchRemoteImage } from "@/shared/network/remoteImageFetch"; |
| async function downloadGeneratedImage(imageUrl: string): Promise< | ||
| { ok: true; b64: string } | { ok: false; status: number; error: string } | ||
| > { | ||
| const imgRes = await fetch(imageUrl); | ||
| if (!imgRes.ok) { | ||
| return { ok: false, status: imgRes.status, error: `Failed to download image: ${imgRes.status}` }; | ||
| } | ||
| const buf = await imgRes.arrayBuffer(); | ||
| return { ok: true, b64: Buffer.from(buf).toString("base64") }; | ||
| } |
There was a problem hiding this comment.
Using raw fetch to download the generated image from an external URL returned by the provider bypasses the repository's SSRF and DNS-rebinding protection. Use fetchRemoteImage instead, which already has built-in DNS-rebinding guards (as stubbed in the unit tests).
| async function downloadGeneratedImage(imageUrl: string): Promise< | |
| { ok: true; b64: string } | { ok: false; status: number; error: string } | |
| > { | |
| const imgRes = await fetch(imageUrl); | |
| if (!imgRes.ok) { | |
| return { ok: false, status: imgRes.status, error: `Failed to download image: ${imgRes.status}` }; | |
| } | |
| const buf = await imgRes.arrayBuffer(); | |
| return { ok: true, b64: Buffer.from(buf).toString("base64") }; | |
| } | |
| async function downloadGeneratedImage(imageUrl: string): Promise< | |
| { ok: true; b64: string } | { ok: false; status: number; error: string } | |
| > { | |
| try { | |
| const remoteImage = await fetchRemoteImage(imageUrl); | |
| return { ok: true, b64: remoteImage.buffer.toString("base64") }; | |
| } catch (err) { | |
| const message = (err as Error)?.message || String(err); | |
| return { ok: false, status: 502, error: "Failed to download image: " + message }; | |
| } | |
| } |
| async function pollMysticTask(params: { | ||
| providerConfig: FreepikProviderConfig; | ||
| token: string; | ||
| taskId: string; | ||
| }): Promise<{ status: string; imageUrl?: string }> { | ||
| const { providerConfig, token, taskId } = params; | ||
| const statusBase = providerConfig.statusUrl || providerConfig.baseUrl; | ||
| const res = await fetch(`${statusBase}/${taskId}`, { | ||
| headers: { ...freepikAuthHeader(providerConfig, token) }, | ||
| }); | ||
| const json = await res.json(); | ||
| const task = json?.data || json; | ||
| const status = typeof task?.status === "string" ? task.status : "IN_PROGRESS"; | ||
| const generated = Array.isArray(task?.generated) ? task.generated : []; | ||
| return { status, imageUrl: typeof generated[0] === "string" ? generated[0] : undefined }; | ||
| } |
There was a problem hiding this comment.
In pollMysticTask, if the polling request fails (e.g., due to rate limits, invalid API key, or server errors), res.ok will be false. Currently, the code doesn't check res.ok and attempts to parse the response as JSON. If the response is not valid JSON, it throws. However, if it is valid JSON but represents an error payload (without a status field), status will default to "IN_PROGRESS". This causes the loop in pollUntilDone to continue polling the failing endpoint until it times out. We should check res.ok and throw an error immediately to abort polling.
async function pollMysticTask(params: {
providerConfig: FreepikProviderConfig;
token: string;
taskId: string;
}): Promise<{ status: string; imageUrl?: string }> {
const { providerConfig, token, taskId } = params;
const statusBase = providerConfig.statusUrl || providerConfig.baseUrl;
const res = await fetch(statusBase + "/" + taskId, {
headers: { ...freepikAuthHeader(providerConfig, token) },
});
if (!res.ok) {
const errorText = await res.text();
throw new Error("Polling failed with status " + res.status + ": " + errorText);
}
const json = await res.json();
const task = json?.data || json;
const status = typeof task?.status === "string" ? task.status : "IN_PROGRESS";
const generated = Array.isArray(task?.generated) ? task.generated : [];
return { status, imageUrl: typeof generated[0] === "string" ? generated[0] : undefined };
}…der (#6654) Adds an official, API-key-based Freepik image-gen provider using the Mystic endpoint (POST /v1/ai/mystic -> async task_id -> GET /v1/ai/mystic/{id} polling), modeled on the existing leonardo.ts generationId adapter pattern. - open-sse/config/providers/registry/freepik/index.ts: new registry module (kept separate to avoid pushing the frozen imageRegistry.ts over the file-size cap) with the 6 real Mystic style models (realism, fluid, zen, flexible, super_real, editorial_portraits) — not the "Flux/Imagen3" list from the original feature request, which independent research showed was stale. - open-sse/handlers/imageGeneration/providers/freepik.ts: submit+poll adapter; all error paths route through sanitizeErrorMessage() (Hard Rule #12), configurable poll interval/timeout via body.poll_interval_ms / poll_timeout_ms for fast, deterministic tests. - Registered in providers.ts (IMAGE_ONLY_PROVIDER_IDS) and apikey/specialty-media.ts (catalog metadata), with the corrected free-tier note (one-time ~€5 credit, not a recurring "100/month" allotment). Drops the "100 free credits/month" and "Flux/Imagen3 selectable models" claims from the original issue - verification showed the free tier is a one-time ~€5 API credit and Imagen 3 only underlies the `fluid` style, not a separately selectable model. Domain: api.freepik.com is still live as of this writing despite Freepik's April-2026 API-docs rebrand to Magnific (docs.freepik.com -> docs.magnific.com); noted inline for future re-verification. Closes #6654
f8de9e0 to
02c03c5
Compare
…freepik-pikaso-image-provider
…r-inference#6654) (cheaper-inference#7597) * feat(providers): add Freepik (Magnific Mystic) image generation provider (cheaper-inference#6654) Adds an official, API-key-based Freepik image-gen provider using the Mystic endpoint (POST /v1/ai/mystic -> async task_id -> GET /v1/ai/mystic/{id} polling), modeled on the existing leonardo.ts generationId adapter pattern. - open-sse/config/providers/registry/freepik/index.ts: new registry module (kept separate to avoid pushing the frozen imageRegistry.ts over the file-size cap) with the 6 real Mystic style models (realism, fluid, zen, flexible, super_real, editorial_portraits) — not the "Flux/Imagen3" list from the original feature request, which independent research showed was stale. - open-sse/handlers/imageGeneration/providers/freepik.ts: submit+poll adapter; all error paths route through sanitizeErrorMessage() (Hard Rule cheaper-inference#12), configurable poll interval/timeout via body.poll_interval_ms / poll_timeout_ms for fast, deterministic tests. - Registered in providers.ts (IMAGE_ONLY_PROVIDER_IDS) and apikey/specialty-media.ts (catalog metadata), with the corrected free-tier note (one-time ~€5 credit, not a recurring "100/month" allotment). Drops the "100 free credits/month" and "Flux/Imagen3 selectable models" claims from the original issue - verification showed the free tier is a one-time ~€5 API credit and Imagen 3 only underlies the `fluid` style, not a separately selectable model. Domain: api.freepik.com is still live as of this writing despite Freepik's April-2026 API-docs rebrand to Magnific (docs.freepik.com -> docs.magnific.com); noted inline for future re-verification. Closes cheaper-inference#6654 * test: align APIKEY_PROVIDERS count to 171 after freepik + release merge (cheaper-inference#7597)
…r-inference#6654) (cheaper-inference#7597) * feat(providers): add Freepik (Magnific Mystic) image generation provider (cheaper-inference#6654) Adds an official, API-key-based Freepik image-gen provider using the Mystic endpoint (POST /v1/ai/mystic -> async task_id -> GET /v1/ai/mystic/{id} polling), modeled on the existing leonardo.ts generationId adapter pattern. - open-sse/config/providers/registry/freepik/index.ts: new registry module (kept separate to avoid pushing the frozen imageRegistry.ts over the file-size cap) with the 6 real Mystic style models (realism, fluid, zen, flexible, super_real, editorial_portraits) — not the "Flux/Imagen3" list from the original feature request, which independent research showed was stale. - open-sse/handlers/imageGeneration/providers/freepik.ts: submit+poll adapter; all error paths route through sanitizeErrorMessage() (Hard Rule cheaper-inference#12), configurable poll interval/timeout via body.poll_interval_ms / poll_timeout_ms for fast, deterministic tests. - Registered in providers.ts (IMAGE_ONLY_PROVIDER_IDS) and apikey/specialty-media.ts (catalog metadata), with the corrected free-tier note (one-time ~€5 credit, not a recurring "100/month" allotment). Drops the "100 free credits/month" and "Flux/Imagen3 selectable models" claims from the original issue - verification showed the free tier is a one-time ~€5 API credit and Imagen 3 only underlies the `fluid` style, not a separately selectable model. Domain: api.freepik.com is still live as of this writing despite Freepik's April-2026 API-docs rebrand to Magnific (docs.freepik.com -> docs.magnific.com); noted inline for future re-verification. Closes cheaper-inference#6654 * test: align APIKEY_PROVIDERS count to 171 after freepik + release merge (cheaper-inference#7597)
Closes #6654
What was built
Adds Freepik's official API-key-based image generation provider (Mystic endpoint) as a new
/v1/images/generationsprovider, following the existing extension-point pattern (open-sse/config/imageRegistry.ts+open-sse/handlers/imageGeneration/providers/).This is a different, cleaner implementation than the earlier #1276 proposal (cookie/subscription scraping, deferred over reverse-engineering concerns): it uses Freepik's documented
x-freepik-api-keyheader auth againstPOST /v1/ai/mystic, an async submit-then-poll flow (task_id→GET /v1/ai/mystic/{task_id}untilCOMPLETED/FAILED), modeled directly on the existingleonardo.tsgenerationIdadapter.Corrections vs. the original issue, verified against live docs (
docs.magnific.com, the April-2026 Freepik→Magnific docs rebrand) at implementation time:realism,fluid,zen,flexible,super_real,editorial_portraits— not "Flux, Imagen3" as separate selectable models (Imagen 3 only underliesfluid).api.freepik.comis still the live API host as of this writing even though the docs domain moved todocs.magnific.com; noted inline in the registry module for future re-verification if it ever 404s.Files
open-sse/config/providers/registry/freepik/index.ts— new registry module (kept separate fromimageRegistry.tsto avoid pushing that frozen file over the file-size cap; spread intoIMAGE_PROVIDERS.freepik)open-sse/handlers/imageGeneration/providers/freepik.ts— submit+poll adapter; poll interval/timeout are overridable viabody.poll_interval_ms/poll_timeout_msfor deterministic testssrc/shared/constants/providers.ts—freepikadded toIMAGE_ONLY_PROVIDER_IDSsrc/shared/constants/providers/apikey/specialty-media.ts— catalog metadata (icon, auth hint, corrected free-tier note)open-sse/handlers/imageGeneration.ts— dispatch wiring (format === "freepik-image")Error handling
All error paths (submit failure, missing
task_id,FAILEDstatus, timeout, exception) route throughsanitizeErrorMessage()per Hard Rule #12 — verified by a dedicated test that asserts a stack-trace-shaped upstream error body is not leaked into the response.Gates run (all green)
npm run typecheck:core— cleannpm run typecheck:noimplicit:core— clean on touched files (pre-existing unrelated errors incombo.ts/usageTracking.ts/cliRuntime.tsconfirmed present onorigin/release/v3.8.49before this change)npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>— 0 errorsnpm run check:complexity-ratchets— OK (2055 ≤ baseline 2056; 889 ≤ baseline 890)npm run check:cycles— OK, no cyclesnode scripts/check/check-file-size.mjs— OK (extracted the new registry entry into its own module after the inline version pushedimageRegistry.tsto 811 lines > 800 cap)node scripts/check/check-test-discovery.mjs— OK, new test file collectednpm run check:provider-consistency,npm run check:provider-assets,npm run check:any-budget:t11,npm run check:known-symbols,npm run check:docs-sync— all OKBehavior test (TDD, Hard Rule #18)
tests/unit/freepik-image-handler.test.ts— 4 tests, all passing, run undernode:test(the gating runner):handleImageGeneration()with mockedfetchFAILEDstatus → sanitized 502 errorAlso reran the existing sibling suites (
new-content-providers.test.ts,nanobanana-image-handler.test.ts,nanobanana-image-generation.test.ts) to confirm no regression: 21/21 passing.No golden snapshot regen needed — image providers don't have translator entries in
tests/snapshots/provider/(that directory covers chat translation paths only; this PR only touches the image-generation dispatch).