Run an OpenRouter-style image script against Sume: four changes

Moving a script from OpenRouter's /api/v1/images to Sume's /v1/images: base64 becomes a hosted URL, 202 jobs appear, stream and seed return 400. Python check.

5 min readSume
All posts

To run an OpenRouter-style image script against Sume, change the base URL and key, then handle four differences: results come back as data[].url instead of b64_json, a slow job returns 202 with a job envelope, stream and seed return 400 unsupported_parameter, and the provider routing fields only accept sume. The request field names (model, prompt, n, aspect_ratio, quality, input_references) line up, so most scripts need a small diff, not a rewrite.

The comparison below uses OpenRouter's image generation guide and the Sume Image API page, both read 2026-10-04.

Which endpoints map to which?

OpenRouter vs Sume image endpoints, read 2026-10-04
PurposeOpenRouterSume
GeneratePOST /api/v1/imagesPOST /v1/images
List modelsGET /api/v1/images/modelsGET /v1/images/models
Per-endpoint recordsGET /api/v1/images/models/{id}/endpointsGET /v1/images/models/{id}/endpoints
Result payloaddata[].b64_json plus media_typedata[].url plus media_type
Streamingstream: true on supporting models400 streaming_not_supported

What are the four changes?

  • Results are URLs. OpenRouter's page says images return as base64-encoded bytes. Sume returns Sume-hosted HTTPS URLs, so replace your base64.b64decode step with a download.
  • Slow jobs return 202. Sume blocks up to 30 seconds by default, then answers 202 with a job envelope; read the images from GET /v1/jobs/{id}/result. Slow configurations such as 4K, high quality or a large n are the likely ones.
  • Some parameters are rejected, not ignored. Sume validates against each model's published capability descriptors, so stream, seed, output_compression and an explicit-pixel size return 400 unsupported_parameter or the streaming error. Remove them before the call.
  • Provider routing is narrow. provider.only and provider.order accept only sume; any other slug returns 400 provider_not_available. ignore, sort and allow_fallbacks are accepted and do nothing.

What does the adapted call look like?

This keeps the OpenRouter field names, drops seed and stream, and downloads from a URL instead of decoding base64.

import os, requests

def generate(prompt, model="bytedance-seed/seedream-4.5"):
    r = requests.post(
        "https://api.sume.com/v1/images",
        headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
        json={"model": model, "prompt": prompt, "n": 1},
        timeout=60,
    )
    if r.status_code == 202:
        raise RuntimeError("slow job: poll " + r.json()["data"]["status_url"])
    r.raise_for_status()
    body = r.json()
    img = requests.get(body["data"][0]["url"], timeout=60)
    img.raise_for_status()
    return img.content, body["usage"]["cost"]

if __name__ == "__main__":
    data, cost = generate("a red panda astronaut, studio lighting")
    open("out.png", "wb").write(data)
    print("billed USD:", cost)

Does the cost field mean the same thing?

On Sume, usage.cost is the billed USD amount, catalog list price times 1.25, and the token counts are always 0. If your OpenRouter code derived cost from tokens, switch it to read cost directly.

Model ids use the same org/slug shape (for example openai/gpt-image-2.5, google/nano-banana-2, bytedance-seed/seedream-4.5), and legacy bare ids still resolve as aliases. Check GET /v1/images/models for what your key can call before you hard-code a list.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume