AI Image Generation in Python: One Helper for GPT Image, Nano Banana and FLUX
Python AI image generation with one requests helper: text-to-image, edits with references, seeds, batches and parallel model tests on Kadraris API.
A Python script that makes AI images needs three things: an API key, a POST request with a model name and a prompt, and a download step for the file that comes back. On Kadraris API that is POST /v1/generate?wait=60 called with requests. The script stays the same whether you call GPT Image 2.5, Nano Banana Pro or FLUX.2 Pro; only the model string changes. You pay per image, and the price is visible before the job starts.
Below is the helper I keep in my own projects, followed by recipes for plain prompts, edits, seeds, batches and side-by-side tests.
Install and authenticate
You only need the requests package and a key from your account.
pip install requests
export KADRARIS_API_KEY=sk-kd-...The helper that does all the work
The function below sends the job with ?wait=60. The server keeps the connection open until the image is done (the cap is 120 seconds). If the job is still in progress when the wait ends, the loop keeps checking the task until it finishes.
import os
import time
import pathlib
import requests
API = "https://kadraris.com/v1"
KEY = os.environ["KADRARIS_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
def generate(model, **input_):
r = requests.post(
f"{API}/generate?wait=60",
headers=HEADERS,
json={"model": model, "input": input_},
timeout=130,
)
task = r.json()
if r.status_code >= 400:
msg = task["error"]["message"]
raise RuntimeError(f"{r.status_code}: {msg}")
# still busy after the wait? keep asking
while task["status"] in ("queued", "running"):
time.sleep(3)
task = requests.get(
f"{API}/tasks/{task['id']}",
headers=HEADERS, timeout=30,
).json()
if task["status"] == "failed":
# failed jobs are free
raise RuntimeError(f"failed: {task['error']}")
return task
def save_outputs(task, folder="out"):
pathlib.Path(folder).mkdir(exist_ok=True)
paths = []
for i, item in enumerate(task["output"]):
clean = item["url"].split("?")[0]
ext = pathlib.Path(clean).suffix or ".png"
name = f"{task['id']}_{i}{ext}"
path = pathlib.Path(folder) / name
data = requests.get(item["url"], timeout=60).content
path.write_bytes(data)
paths.append(path)
return pathsCall save_outputs straight after generate. Result links expire after a limited time, so treat them as a download slot, not storage. Your generation history stays on the account, but the file itself should live on your disk or bucket.
Your first image from a prompt
task = generate(
"flux-2-pro",
prompt=(
"Product photo of a matte black espresso "
"machine on a polished concrete counter, "
"morning side light, shallow depth of field"
),
aspect_ratio="4:3",
)
print(save_outputs(task), task["cost"])To try a different model, swap the name. A poster with readable text, for example:
task = generate(
"nano-banana-pro",
prompt='Minimal concert poster, headline '
'"NIGHT SHIFT", date "OCT 14", '
"two-color risograph look, cream paper",
aspect_ratio="3:4",
)Fields every model shares, and fields that vary
Only two inputs behave the same across the image models:
| Field | Type | Purpose |
|---|---|---|
prompt | text | Describes the image |
image_urls | list of public URLs | References or the image to edit; the limit depends on the model |
The rest is model-specific. A few examples:
aspect_ratiotakes ratios such as4:3in FLUX.2 Pro and Nano Banana Pro. GPT Image 2.5 wants pixel sizes (1024x1024,1536x1024,1024x1536) orauto.num_images,seed,negative_prompt,resolutionandqualityappear only in some schemas.
To see what a model accepts, read input_schema from GET /v1/models/{id}, for instance GET /v1/models/gpt-image-2.5. If you send a field or value the model does not know, you get a 422 response and nothing is charged.
Edits and reference images
For background swaps, style transfer or placing a product into a new scene, send one or more public image URLs in image_urls and write what should change.
task = generate(
"gpt-image-2.5",
prompt="Place this bottle on a wet rock by a "
"mountain stream, overcast light, "
"keep the label exactly as is",
image_urls=[
"https://example.com/assets/bottle.png",
],
# pixel size here, this model has no ratios
aspect_ratio="1536x1024",
)With several references, number them in the prompt so the model knows which image supplies which element:
task = generate(
"nano-banana-pro",
prompt="The person from the first image wearing "
"the jacket from the second image, walking "
"down the street from the third image, "
"golden hour",
image_urls=[
"https://example.com/refs/person.jpg",
"https://example.com/refs/jacket.jpg",
"https://example.com/refs/street.jpg",
],
aspect_ratio="9:16",
)What helped me get cleaner results:
- The reference that matters most goes first in the list.
- Product shots on a plain background and faces in good light work better than busy photos.
- Name what must stay untouched, for example "keep the label exactly as is".
Picking between the three main models
These are the models I reach for first. The facts come from each producer's release notes.
| GPT Image 2.5 | Nano Banana Pro | FLUX.2 Pro | |
|---|---|---|---|
| Producer | OpenAI | Google (Gemini 3 Pro Image) | Black Forest Labs |
| Released | Sept 8, 2026 | Nov 2025 | Nov 25, 2025 |
| Output | up to 4K | up to 4K | up to 4 MP |
| References | up to 16 | up to 14 | several |
And the strengths, in one line each:
- GPT Image 2.5: careful edits and staying close to the reference photo. This was the focus of the 2.5 release. Price: $0.05 per image.
- Nano Banana Pro: readable text in images, from short taglines to longer blocks and several languages. Good for posters, thumbnails, labels and infographics. Price: from $0.07 per image.
- FLUX.2 [pro]: photoreal product and editorial shots with clean light and materials. Price: from $0.0688 per image.
If a brief does not point clearly at one of them, I run the prompt on all three with the script in the next section. For a longer comparison, see GPT Image 2.5 vs Nano Banana Pro vs FLUX.2.
Same prompt, three models, at once
Running models one by one means waiting three times. A thread pool sends the jobs together:
from concurrent.futures import ThreadPoolExecutor
PROMPT = ("Studio photo of a ceramic mug with "
"'good morning' in hand-lettered script")
MODELS = ["gpt-image-2.5", "nano-banana-pro",
"flux-2-pro"]
# pixel size for GPT Image, ratio for the others
ASPECT = {"gpt-image-2.5": "1024x1024",
"nano-banana-pro": "1:1",
"flux-2-pro": "1:1"}
def run(model):
try:
task = generate(model, prompt=PROMPT,
aspect_ratio=ASPECT[model])
out = save_outputs(task, f"out/{model}")
return model, out, task["cost"]
except RuntimeError as e:
return model, None, str(e)
with ThreadPoolExecutor(max_workers=3) as pool:
for row in pool.map(run, MODELS):
print(*row)I start most new client briefs with this. One prompt, three files, and the winner is usually clear on first look. If a safety filter blocks the prompt on one model, that task ends as failed with cost 0 while the other two still return images.
Several variations in one job
Some models list num_images in their schema, Nano Banana 2 among them, and return up to four variations per task. Billing is per image, so four variations cost four images. GPT Image 2.5, Nano Banana Pro and FLUX.2 Pro give one image per task; for more, send more tasks.
task = generate(
"nano-banana-2",
prompt="Flat vector illustration of a fox "
"reading a newspaper in a cafe, "
"warm palette",
aspect_ratio="1:1",
num_images=4,
)
for p in save_outputs(task):
print(p)Repeatable results with a seed
A client approves a layout and then asks for "the same thing, with a blue roof". A fixed seed keeps the starting point stable between the two runs:
base = dict(
prompt="Isometric tiny house on a floating "
"island, soft clay render",
aspect_ratio="1:1",
seed=421,
)
a = generate("flux-2-max", **base)
blue = base["prompt"] + ", blue roof"
b = generate("flux-2-max", **{**base, "prompt": blue})Keep expectations realistic: a seed makes output more repeatable, but a changed prompt still changes the picture. Not every model takes one. Among image models, FLUX.2 [max], FLUX.1 Kontext [pro] and Wan 2.7 Image accept seed. GPT Image 2.5, Nano Banana Pro and FLUX.2 Pro do not, and a request with seed returns 422. Look at the schema first.
Hundreds of images: go async
For large runs, drop ?wait and submit tasks without blocking. Collect results by polling GET /v1/tasks/{id} or by passing a webhook_url. The flow is the same as for video, described in async AI video with webhooks.
def submit(model, **input_):
r = requests.post(
f"{API}/generate", headers=HEADERS,
json={"model": model, "input": input_},
timeout=30,
)
r.raise_for_status()
return r.json()["id"]
items = ["kettle", "bicycle", "tent", "lamp"]
prompts = [f"Minimal line icon of a {x}, "
"black on white" for x in items]
ids = [submit("flux-2-pro", prompt=p,
aspect_ratio="1:1") for p in prompts]If you fire many requests in a short burst and see 429, wait a moment and retry.
Reading the cost
Every image model is billed per image. The 202 response includes estimated_cost, the finished task includes cost, and a failed task costs nothing. All rates are on /pricing, and the cost calculator helps when you plan a batch.
Turning a still into a clip
A good still often makes a good first frame for video. Pass the image URL as image_url to a video model such as Seedance 2.5 and describe the motion. Code for that is in the Seedance 2.5 API guide. If you prefer clicking to coding, the same models are in Kadraris Studio and the AI image generator, paid from the same balance.
Questions
Do I need an SDK to call the image models from Python?
No. Plain requests is enough. You send JSON with model and input, add the bearer key in the header, and read the image URL from output in the response.
Why does GPT Image 2.5 reject my aspect ratio?
It expects pixel sizes such as 1024x1024 or 1536x1024, or auto, rather than ratios like 4:3. Other models in this guide take ratios. The schema at GET /v1/models/{id} lists the allowed values.
Am I charged when a prompt is blocked or a job fails?
No. A failed task ends with cost 0 and the reserved amount goes back to your balance. A 422 for a bad field is returned before any charge.
How do I keep the generated files?
Download them right after the task succeeds, as the save_outputs helper does. The links in the response stop working after a limited time.


























