Pin the image model id on the queue row so a swap can't break old jobs
Store the Sume model id on each queued render row at enqueue time, then re-point only rows still carrying a retired id. Python sqlite3 sample for gpt-image-1.

Write the model id into the job row when you enqueue it, and have the worker read it from the row, not from current config. That way a deploy that changes your default model never changes what an in-flight job asks for. It also gives you one query that finds every queued row still holding a retired id once OpenAI's gpt-image-1 shutdown on October 23, 2026 gets close.
Without the column, a worker reads today's config for yesterday's job. That is fine until you change the config mid-batch and half the batch renders on each model.
Schema and the remap query
The table needs a model column that is not null. The remap is a single update that touches only queued rows whose id is in a retired set, so rows that already succeeded keep the id they were rendered with.
import sqlite3
RETIRED = ("openai/gpt-image-1", "gpt-image-1", "gpt-image-1-mini", "gpt-image-1.5")
NEW = "openai/gpt-image-2.5-sunburst"
db = sqlite3.connect("jobs.db")
db.execute("create table if not exists render_jobs("
"id integer primary key, prompt text, model text not null, "
"status text default 'queued')")
def enqueue(prompt, model=NEW):
cur = db.execute("insert into render_jobs(prompt, model) values (?, ?)",
(prompt, model))
db.commit()
return cur.lastrowid
def remap_queued():
marks = ",".join("?" * len(RETIRED))
cur = db.execute(f"update render_jobs set model = ? where status = 'queued' "
f"and model in ({marks})", (NEW, *RETIRED))
db.commit()
return cur.rowcount
if __name__ == "__main__":
enqueue("mug on linen", "gpt-image-1")
print("re-pointed", remap_queued())Which ids count as retired?
On Sume the matching ids are openai/gpt-image-2.5-sunburst and openai/gpt-image-2.5. Sume does not list any gpt-image-1 id, so an unknown id returns 404 model_not_found rather than quietly routing somewhere else.
| Retired id | Shutdown date | Vendor-listed replacement |
|---|---|---|
| gpt-image-1 | 2026-10-23 | gpt-image-2.5-sunburst or gpt-image-2.5-flare |
| gpt-image-1-mini | 2026-12-01 | gpt-image-2.5-sunburst or gpt-image-2.5-flare |
| gpt-image-1.5 | 2026-12-01 | gpt-image-2.5-sunburst or gpt-image-2.5-flare |
| chatgpt-image-latest | 2026-12-01 | gpt-image-2.5-sunburst or gpt-image-2.5-flare |
Keep the idempotency key in the row too
Store the Idempotency-Key next to the model. A retry with the same key and payload returns the original job with idempotency_hit: true. After a remap the payload changes, so mint a new key for the re-pointed row; reusing the old one returns 409 idempotency_conflict.
Sources
Related posts
More in Developers
- Poll many transcription jobs without a 429: next_poll_after_seconds
Poll Sume job status with the next_poll_after_seconds the API sends, back off on 429 with retry-after, and stop on a terminal status.
- Clicks or gaps when joining TTS MP3 clips: render WAV, join once
Joined MP3 voiceover clips can gap or click. Sume's Timeline audio docs explain why: MP3 adds priming padding at each edge. Keep WAV until the last step.
- Postgres SKIP LOCKED poll table for AI video jobs with next_poll_at
Track Sume video jobs in one Postgres table with next_poll_at, claim due rows with FOR UPDATE SKIP LOCKED and reschedule from next_poll_after_seconds.
- pytest: fail CI when a configured image model id isn't in Sume's list
A pytest that reads your image model ids and checks each against GET /v1/images/models, so a retired id such as gpt-image-1 fails CI before it fails a customer.
Written by Sume