I used to debug a bad AI product image by rewriting the prompt. Now I check
the request contract first.
A GPT Image 2 API request can be valid JSON and still be wrong for the selected
model. A generic UI can leak a field from another provider. Two dimension
controls can conflict. A timeout can turn one intended image into two
generations. A technically successful output can still change the product.
This is the seven-check preflight I put in front of the generation call.
Disclosure: I work on XPLA. The workflow below is an engineering pattern,
not a promise about price, access, speed or image quality.
I do not let one universal form submit every visible field to every image
model. The application selects a model contract first:
const imageContracts = {
"gpt-image-2": {
endpoint: "/v1/images/generations",
allowed: new Set([
"model", "prompt", "image", "images",
"size", "aspectRatio", "quality", "replyType", "n"
]),
forbidden: new Set(["imageSize"])
}
};
The UI should be generated from that record. A control the selected model does
not support should not appear.
Silently removing an unsupported field creates plausible but uncontrolled
outputs. The user chooses a setting, the backend drops it, a default is used,
and everyone blames the model.
function rejectUnknownFields(body, contract) {
const errors = [];
for (const key of Object.keys(body)) {
if (!contract.allowed.has(key)) {
errors.push(`Unsupported field for ${body.model}: ${key}`);
}
}
for (const key of contract.forbidden) {
if (body[key] !== undefined) {
errors.push(`Forbidden field for ${body.model}: ${key}`);
}
}
return errors;
}
An explicit local error is easier to fix than an attractive image produced
from the wrong request.
An image form may expose a ratio, a pixel size, a provider-specific size field
and an orientation inferred from the prompt. I allow one supported decision:
function validateDimensions(body) {
if (body.imageSize !== undefined) {
return ["imageSize is not valid for this standard contract"];
}
const supplied = [
body.size !== undefined,
body.aspectRatio !== undefined
].filter(Boolean).length;
return supplied > 1
? ["Choose size or aspectRatio, not both"]
: [];
}
I store the requested orientation beside the output so QA can verify it.
A reference is more than a URL. I store the product identity and the allowed
transformation:
{
"source_type": "merchant_upload",
"rights_state": "confirmed_for_internal_calibration",
"product_id": "merchant-sku-104",
"variant": "matte-black-500ml",
"must_preserve": [
"single bottle",
"matte black body",
"silver cap",
"existing label geometry"
],
"allowed_changes": [
"background",
"surface",
"lighting direction"
]
}
A marketplace image being public does not automatically grant permission to
download, transform or use it in advertising.
This is the operational rule I care about most.
If a client times out, it may not know whether the server failed before or
after generation. Repeating the request can create another billable task.
Before submission, I persist:
{
"generation_id": "img-job-20260902-001",
"intent": "new_calibration",
"request_hash": "sha256-of-normalized-request",
"state": "submitted",
"attempt": 1,
"result_state": "unknown",
"next_action": "check_before-repeating"
}
My state transition is:
draft
-> approved
-> submitted
-> completed
-> accepted | repair | rejected
submitted
-> transport_unknown
-> reconcile before another generation
A new creative candidate receives a new generation ID. An uncertain transport
result enters reconciliation. I do not hide both actions behind one “Try
again” button.
For the first run I use:
The question is narrow: can the workflow preserve the product facts that
matter? More outputs do not improve the evidence if no one reviews them.
My QA table looks like this:
| Check | Expected | Decision |
|---|---|---|
| Product count | one unit | accept / repair / reject |
| Body color | matte black | accept / repair / reject |
| Cap | silver | accept / repair / reject |
| Label geometry | unchanged | accept / repair / reject |
| Invented claims | none | accept / repair / reject |
| Orientation | requested ratio | accept / repair / reject |
| Crop safety | fully visible | accept / repair / reject |
“Looks good” is not a release decision. An attractive output can still invent
a bundle, accessory, certification or quantity.
function preflightGPTImage2(body, rightsRecord) {
const contract = imageContracts[body.model];
const errors = [];
if (!contract) errors.push(`Unknown model contract: ${body.model}`);
if (!body.prompt?.trim()) errors.push("prompt is required");
if (body.n !== undefined && body.n !== 1) {
errors.push("Use n: 1 and create separate intentional requests");
}
if (body.replyType !== undefined && body.replyType !== "json") {
errors.push('replyType must be "json"');
}
if (contract) {
errors.push(...rejectUnknownFields(body, contract));
errors.push(...validateDimensions(body));
}
if ((body.image || body.images) &&
rightsRecord?.rights_state !== "confirmed_for_internal_calibration") {
errors.push("Reference-image rights are not confirmed");
}
return { ok: errors.length === 0, errors };
}
This is only the request boundary. A production implementation still needs
media-type, file-size, URL, privacy, account, policy and storage checks.
| Failure class | Example | Default action |
|---|---|---|
| Client | empty prompt | fix locally |
| Contract | unsupported field | reject before submission |
| Access | unauthorized key | stop and fix access |
| Transport | lost response | reconcile before repeating |
| Provider | transient failure | bounded retry policy |
| Safety | risky request | stop or revise |
| Output QA | altered product | repair or reject |
I keep the final order simple:
contract -> preflight -> approval -> one calibration
-> product QA -> intentional scale
The current XPLA-specific request shape and model-name boundaries are
documented in the GPT Image 2 API guide.
Recheck the live contract before production integration.