Skip to content

feat: add Freepik (Magnific Mystic) image generation provider (#6654) - #7597

Merged
diegosouzapw merged 3 commits into
release/v3.8.49from
feat/6654-freepik-pikaso-image-provider
Jul 17, 2026
Merged

diegosouzapw merged 3 commits into
release/v3.8.49from
feat/6654-freepik-pikaso-image-provider

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Closes #6654

What was built

Adds Freepik's official API-key-based image generation provider (Mystic endpoint) as a new /v1/images/generations provider, 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-key header auth against POST /v1/ai/mystic, an async submit-then-poll flow (task_id → GET /v1/ai/mystic/{task_id} until COMPLETED/FAILED), modeled directly on the existing leonardo.ts generationId adapter.

Corrections vs. the original issue, verified against live docs (docs.magnific.com, the April-2026 Freepik→Magnific docs rebrand) at implementation time:

  • Model roster is the real Mystic style list — realism, fluid, zen, flexible, super_real, editorial_portraits — not "Flux, Imagen3" as separate selectable models (Imagen 3 only underlies fluid).
  • Free tier is a one-time ~€5 API credit, not a recurring "100 credits/month" (unconfirmed, dropped from provider metadata).
  • api.freepik.com is still the live API host as of this writing even though the docs domain moved to docs.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 from imageRegistry.ts to avoid pushing that frozen file over the file-size cap; spread into IMAGE_PROVIDERS.freepik)
  • open-sse/handlers/imageGeneration/providers/freepik.ts — submit+poll adapter; poll interval/timeout are overridable via body.poll_interval_ms / poll_timeout_ms for deterministic tests
  • src/shared/constants/providers.ts — freepik added to IMAGE_ONLY_PROVIDER_IDS
  • src/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, FAILED status, timeout, exception) route through sanitizeErrorMessage() 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 — clean
  • npm run typecheck:noimplicit:core — clean on touched files (pre-existing unrelated errors in combo.ts / usageTracking.ts / cliRuntime.ts confirmed present on origin/release/v3.8.49 before this change)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — 0 errors
  • npm run check:complexity-ratchets — OK (2055 ≤ baseline 2056; 889 ≤ baseline 890)
  • npm run check:cycles — OK, no cycles
  • node scripts/check/check-file-size.mjs — OK (extracted the new registry entry into its own module after the inline version pushed imageRegistry.ts to 811 lines > 800 cap)
  • node scripts/check/check-test-discovery.mjs — OK, new test file collected
  • npm 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 OK

Behavior test (TDD, Hard Rule #18)

tests/unit/freepik-image-handler.test.ts — 4 tests, all passing, run under node:test (the gating runner):

  1. registry-shape assertions (provider registered, correct format/authHeader/models)
  2. full submit→poll→download success path through handleImageGeneration() with mocked fetch
  3. FAILED status → sanitized 502 error
  4. submit error containing a stack-trace-shaped body → response does not leak the raw path (Hard Rule fix(ui): fix Select dropdown dark theme inconsistency #12 regression guard)
✔ freepik provider is registered (registry shape)
✔ handleImageGeneration(freepik): async submit+poll returns b64_json payload
✔ handleImageGeneration(freepik): FAILED status returns sanitized 502 error
✔ handleImageGeneration(freepik): submit error response is sanitized, not raw upstream body
ℹ tests 4
ℹ pass 4
ℹ fail 0

Also 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).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +13
import { saveCallLog } from "@/lib/usageDb";
import { sleep } from "../../../utils/sleep.ts";
import { sanitizeErrorMessage } from "../../../utils/error.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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";

Comment on lines +116 to +125
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") };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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).

Suggested change
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 };
}
}

Comment on lines +99 to +114
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 };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
@diegosouzapw
diegosouzapw force-pushed the feat/6654-freepik-pikaso-image-provider branch from f8de9e0 to 02c03c5 Compare July 17, 2026 13:50
@diegosouzapw
diegosouzapw merged commit 76c3b3b into release/v3.8.49 Jul 17, 2026
4 of 6 checks passed
@diegosouzapw
diegosouzapw deleted the feat/6654-freepik-pikaso-image-provider branch July 19, 2026 21:00
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…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)
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(providers): add Freepik AI (Pikaso) as image-gen provider (free tier available)

1 participant