Repository navigation
feat: add Generate resource for AI image generation (Media Generation API) - #15
adidagancld wants to merge 3 commits into
Conversation
… API) Adds a Generate resource backed by the Cloudinary Media Generation API (Image Generation add-on): Generate Image From Text, Generate Image From Reference Images, and Get Generation Task. - mediaGeneration.client.ts: dependency-free client whose types mirror the OpenAPI spec in CloudinaryLtd/media_generation (app/schema/schema.yml), one function per operationId, Basic auth on the /v2 base. - Errors surface the spec's error code, a per-status hint and request_id. httpRequestWithAuthentication already throws a NodeApiError, which `new NodeApiError()` returns untouched, so the client rewrites it in place. - Output is one item per generated image with storage fields flattened (public_id, asset_id, secure_url) so it pipes into Transform/Asset ops; Simplify off returns the raw response. Async returns task_id for polling. - Additive only (new resource value + gated fields): no typeVersion bump. - Local n8n e2e: portable scripts + Generate fixture under docs/backward-compatibility-check/local-n8n/, guide rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync the client with the latest media_generation schema:
- Add the experimental root-level `notices` array ({severity, text}) to the
200, 202, task GET and error/429 envelopes.
- Carry notices onto simplified output items alongside limits/request_id.
- Show notices in the error description in place of the generic per-status
hint (the quota-wall notice says not to retry; the 429 hint said to).
- Model family/tier now report `none` instead of `unmapped` (doc only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review — pass 1Head Verdict: approve with nits. Nothing blocking. Hunted: async/sync/failed task classification and output; error extraction vs. what n8n actually throws, continueOnFail; auth and task-ID/URL injection; request bodies vs. the OpenAPI spec; backward compat, runtime deps, scanner, packaging; whether tests reach real code paths. Findings
PR body
Not runTests, |
Head
Open findings
Closednone Reviewed under
Responsesnone yet NextVerify each open finding against the new head; pick an unmined angle (e.g. UI field gating/displayOptions, AI-tool usage). |
|
Credit cost hints: link to live data, don't hardcode it The Image Generation docs don't publish per-model credit costs. They only say cost varies by model and resolution. Hardcoded numbers will drift from the backend without anyone noticing:
Suggestion:
|
|
Model Tier: difference between Standard and Premium is unclear
|
|
Reuse The "run builder → rethrow as
Suggest making |
| // ── Model selection (`ModelSelection` = ModelByFamily | ModelById | ModelAuto) ── | ||
|
|
||
| /** `ModelByFamily.family` */ | ||
| export const MODEL_FAMILIES = ['flux', 'recraft', 'gpt-image', 'nano-banana', 'ideogram'] as const; |
There was a problem hiding this comment.
@adidagancld note that this list needs to be maintained and matched with changes that are happening in production.
- Get Generation Task throws on a failed task so Continue On Fail / the error output apply - Set context.itemIndex on every Media Generation API error - Read the error body only from NodeApiError context.data; drop the speculative response.body / JSON.parse fallbacks and their test - Soften the README 429 wording; drop hardcoded per-model credit costs - Add a credit-use hint on the Model field and describe Standard vs Premium tiers - Make buildComponents generic and reuse it in the Generate builders Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @eitanp461 — addressed in 8bf6ec2. Review pass 1
Credit cost hints — dropped the per-model numbers from the README and Model Tier — each option now has a description, taken from the OpenAPI spec since the public docs don't define it: Standard = the family's faster, lower-cost model; Premium = its highest-quality model, typically more credits. The field notes tiers stay valid as models are upgraded.
Tests (271), build, and lint pass. 🤖 Generated with Claude Code |
Summary
Adds a Generate resource that creates images with AI through the Cloudinary Media Generation API (Image Generation add-on). It has three operations, one per
operationIdin the service's OpenAPI spec (CloudinaryLtd/media_generation→app/schema/schema.yml, v1.0.0):POST /v2/generate/{cloud}/text_to_imagePOST /v2/generate/{cloud}/image_to_imageGET /v2/generate/{cloud}/tasks/{task_id}Everything is additive: a new
resourcevalue, with new fields shown only for it. There's notypeVersionbump, andbackward-compatibility-checkreports 0 breaking changes. Version goes from 0.2.3 to 0.3.0.Design
mediaGeneration.client.tsis hand-written against the spec. Its types mirrorcomponents.schemasone-to-one: eachoneOfbecomes a TS union, and names stay snake_case as on the wire. The UI option lists (model IDs, families, aspect ratios, …) are built from its constants, so a spec change is edited in one place. I didn't use a code generator because of the no-runtime-deps rule, and the API is only three endpoints./v2base with JSON bodies.model,image_sizeandtargetobjects are flattened in the UI into a mode selector plus fields that only show for that mode. Pure builders inoperations/generate/shared.tsmap them back and validate against the spec's bounds before any request: 64–4096 px, 1–4 references, HTTPS URLs, the task-ID pattern. Only fields the user set are sent, so the service defaults apply to everything else.-editmodels are only valid onimage_to_imageand the others only ontext_to_image, so each operation has its own Model ID list.storageflattened to the top level. That putspublic_id,asset_id,secure_url,resource_type,typeandversionalongsideformat,width,height,model,seed,request_idandlimits. It pipes straight into Transform ({{ $json.public_id }}) and Asset ({{ $json.asset_id }}). Turning Simplify off returns the raw body.task_id(202) to poll with Get Generation Task or to receive via a Notification URL webhook. The node never polls on its own.error.code, with a hint per status and therequest_id. For example:MG_00704: reference asset '…' not found, or on 429, how many quota units remain.Non-obvious: rewriting the NodeApiError in place
httpRequestWithAuthenticationalready throws aNodeApiError, andnew NodeApiError(node, err)returnserruntouched when it already is one, silently ignoring themessage/descriptionoptions. So the client reads the response body fromerror.context.data, where NodeApiError keeps it, and rewritesmessage/description/httpCodeon the existing error. Without this, users would see n8n's generic "The service is receiving too many requests from you" instead of the API's message. This is covered by a unit test and was verified inside real n8n (below).Testing
Unit. 21 new Vitest tests covering the request contract for each operation, every
ModelSelection/ImageSize/Targetform, validation, output shaping, and error mapping with a realNodeApiError. The suite is now 263 tests, all passing; lint (both configs) andtscare clean.Live API smoke (cloud
adidagan). I ran all three operations against production: the default model, family/tier, auto model, exact dimensions, temporary vs. managed storage, image-to-image with a URL and an asset-ID reference, async → poll tocompleted, and the 401 / 400 / 404 errors.Real n8n 2.41.3. I installed the package exactly the way n8n installs a community package and ran
fixtures/generate-smoke.workflow.jsonwithn8n execute. All 9 nodes succeeded:MG_…errors surfaced through n8n's real HTTP helper with continue on fail.n8n also generated the AI-tool variant (
cloudinaryTool) with the Generate resource.Local n8n e2e (docs)
I rewrote
local-n8n-test.md, which had hardcoded paths from one machine and covered only the 0.0.9 upgrade. It now comes with portable scripts underlocal-n8n/:~/.n8n;install-branch.sh, which mirrors n8n'sdownloadPackagestep. That means nonpm link, which would load the repo's ownn8n-workflowand breakinstanceofchecks;.local/smoke.envand never prints the secret;The guide also documents two setup problems. n8n needs Node 24 or newer, and its native build needs a Python that still has
distutils(PYTHON=/usr/bin/python3). Andn8n executefails silently while the editor is running, because of the task-broker port.Notes for reviewers
nano-banana-2, premium) generation used 9 units, while FLUX.2 Klein used 1. The 429 hint says "quota units" rather than "generations" for that reason, and the README mentions it.🤖 Generated with Claude Code