160x600 skyscraper ad image: 3.75:1 is past GPT Image 2.5's 3:1 cap

A 160x600 ad is 1:3.75, taller than the 3:1 limit on GPT Image 2.5 custom sizes. Render 576x1728 (1:3) and crop 20% of the width in Pillow. Math included.

5 min readSume
All posts

You cannot generate a 160x600 skyscraper ad image directly with GPT Image 2.5, because 600 / 160 = 3.75 and the model's custom sizes stop at a 3:1 ratio. The workable path is to render 576x1728 (exactly 1:3) and let Pillow crop the sides down to 160x600.

This is the one standard ad shape that breaks the rule outright. 300x600 and 300x250 fit comfortably, but 160x600 is thinner than the model allows.

The crop math

Scale 576x1728 by 1 / 2.88 and you get 200x600. The target is 160 wide, so 40 pixels go, 20 per side. That is 20 percent of the width, and none of the height. Because the shape change is a pure side crop, you control it with one sentence in the prompt: keep the subject in the middle 80 percent of the width.

Why 160x600 fails and 576x1728 works (read 2026-10-05)
Check160x600 direct576x1728 render
Both edges multiples of 16no (160 yes, 600 no)yes (36 x 16, 108 x 16)
Aspect ratio at most 3:13.75:1, fail3.000:1, pass
Pixels 655,360 to 8,294,40096,000, fail995,328, pass
Longest edge at most 3840600, pass1728, pass

Code

ImageOps.fit scales to cover the target and crops the overflow from the center, which is the 20 percent side trim above. The helper stops on any non-200 status so a 202 never gets saved as an image.

import os, requests
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def generate(body):
    r = requests.post("https://api.sume.com/v1/images", headers=H, json=body, timeout=60)
    if r.status_code != 200:  # 202 = still running, read data.status_url
        raise SystemExit(f"{r.status_code}: {r.text[:300]}")
    return r.json()["data"][0]["url"]
from io import BytesIO
from PIL import Image, ImageOps

url = generate({
    "model": "openai/gpt-image-2.5",
    "prompt": "Tall poster of a single red bicycle against a plain wall, bike centered, plenty of margin left and right",
    "image_size": {"width": 576, "height": 1728},
    "quality": "high",
})
img = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
ImageOps.fit(img, (160, 600), Image.LANCZOS).save("ad-160x600.png")

Price

The output-token rate on the fal Flare page is the base for the figures below. Sume bills provider list times 1.25, before the small input-token charge for the prompt.

Output-only price per 576x1728 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0018$0.0022
medium$0.0043$0.0054
high$0.0173$0.0216

When to pick another model

If the crop costs you too much, check the catalog before you commit. Each model lists its own aspect_ratio values at GET /v1/images/models, and Sume rejects anything a row does not list with 400 unsupported_parameter. A model that lists 1:8 gives you a thinner starting shape than 1:3.

Draft cheap, finish once

A display set usually needs several concepts before one is approved. Request the first round at quality: "low", pick the layout, then repeat only the winner at high. Quality is a per-request field on both ChatGPT Image 2.5 variants (openai/gpt-image-2.5 and openai/gpt-image-2.5-sunburst), and the accepted values are auto, low, medium, high, xhigh and max. Avoid auto: Sume reserves the max price for it, so set the tier yourself.

Flare is the faster variant and Sunburst is tuned for edit precision, according to the fal pages. Both share the same token rates, so for an ad pass you can pick on behavior, not on price. Auto routing on Sume (sume/auto) uses Flare.

If the call returns 202 instead of 200

POST /v1/images waits up to 30 seconds and answers 200 with the images when the job finishes in time. When it does not, the route answers 202 with the standard job envelope, and the images come from GET /v1/jobs/{id}/result. Slow settings such as 4K, high quality and large n are the likeliest to fall back. A 1-2 megapixel ad master at high quality usually stays in the wait budget, but the code should branch on the status code, not the body shape.

For a batch of ad sizes, send each request with mode: "async" and read the results afterwards, or add a webhook_url with mode: "webhook". A failed synchronous job returns 502 with an error code and a next_action, and Sume does not bill failed generations.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume