Hy Image 3.5 Preview returns base64 PNG; Sume returns a URL

Moving image code from a base64 PNG response (Hy Image 3.5 Preview on OpenRouter) to Sume means downloading data[].url. A 12-line Python version.

5 min readSume
All posts

Tencent's Hy Image 3.5 Preview, as listed on OpenRouter, returns the image as base64 PNG. Sume's POST /v1/images works differently: the response holds data[].url, a Sume-hosted signed URL, and no inline base64. If your code decodes base64 today, the change is to download the URL instead. Sume does not list Hy Image 3.5 Preview, so the example below calls a model that Sume does list.

What each side returns

The OpenRouter listing for Hy Image 3.5 Preview (read 2026-10-08) says the output is an image in PNG format via base64, and that the response carries usage.cost, the USD charge for the call. Sume's Image API docs say the result payload is data[].url, not inline base64, because Sume already mirrors generated media and URLs keep responses small. In Sume's usage object, cost is the billed USD amount and token counts are 0 in v1.

Response shape, Hy Image 3.5 Preview on OpenRouter vs Sume POST /v1/images, as of 2026-10-08
ItemHy Image 3.5 Preview (OpenRouter listing)Sume /v1/images
Image bytesBase64 PNG in the responseSigned Sume-hosted URL in data[].url
Cost fieldusage.cost, USD charge for the callusage.cost, billed USD amount
Token countsNot stated on the page0 in v1
Reference imagesUp to 20Per model, from the input_references descriptor
Slow callsNot stated on the page200 with the image, or 202 with a job envelope

Download the URL

The code posts a request, treats 202 as a job, and saves the file. The signed URL is the only place the bytes live, so fetch it soon after the job completes. The Content-Type header tells you the extension to use.

import os, requests

H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
body = {
    "model": "bytedance-seed/seedream-5-lite",
    "prompt": "A paper boat on a calm lake, morning light",
    "aspect_ratio": "1:1",
}
r = requests.post("https://api.sume.com/v1/images", headers=H, json=body, timeout=60)
r.raise_for_status()
if r.status_code == 202:
    raise SystemExit("job envelope: poll the status_url from the body")
url = r.json()["data"][0]["url"]
img = requests.get(url, timeout=60)
ext = img.headers.get("content-type", "image/png").split("/")[-1]
open(f"out.{ext}", "wb").write(img.content)
print("saved", len(img.content), "bytes as", ext)

Handle the 202 case

Examine the status code, not the body shape. Sume's docs say 200 is the image response and 202 is the job envelope, and that slow configurations (4K, high quality, large n) are the most likely to degrade to 202. A 202 body carries status_url and result_url. Poll the status URL until the job is terminal, then fetch the result, which holds the artifacts with content_type and url.

If your pipeline expects bytes in the first response, wrap the call in a function that returns bytes after either path. The submit-then-poll pattern also works with a webhook for the terminal event, which means no polling loop is needed.

What not to carry over

Do not port a request field just because it exists upstream. Sume rejects a parameter that the selected model does not list with 400 unsupported_parameter, rather than ignoring it. Read GET /v1/images/models for the row you chose, and send only the fields in its supported_parameters.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume