Streamlit Sora demo: switch to Sume and st.video, no double billing

Streamlit reruns your script on every click. Derive the Sume Idempotency-Key from the prompt so a rerun returns the first job, then show the clip with st.video.

5 min readSume
All posts

A Streamlit app that called Sora reruns its whole script on every widget change, so a naive port to Sume can submit the same billable video twice. Derive the Idempotency-Key from the prompt, so a rerun with the same text gets the first job back, then fetch the bytes from /v1/videos/{id}/content and pass them to st.video, because the content URL needs your API key and a browser cannot send it.

OpenAI's deprecations page, read 2026-10-08, lists the Videos API as removed on September 24, 2026, so a demo that still calls it is already broken.

Why a prompt-derived key

Sume answers 409 idempotency_conflict when a key is reused with a different body. That works in your favour: the same prompt and the same fields hash to the same key and replay the first job; a changed prompt hashes to a new key and starts a new job. If a user wants a second take of the same text, add a take number to the hashed string.

Streamlit rerun cases on Sume, docs read 2026-10-08
User actionScript rerunsKey resultOutcome
Click Generate with new textyesnew hashnew job, reserve list price x 1.25
Change an unrelated widgetyesnone, the button is not pressedno request is sent
Click Generate again with the same textyessame hashfirst job returned
Same key, different bodyyessame key409 idempotency_conflict

The app

Run it with streamlit run app.py and SUME_API_KEY set. The call blocks the script thread while it polls, which is acceptable for a demo; use a worker and a webhook for anything with real traffic.

import hashlib, os, time
import requests
import streamlit as st

BASE = "https://api.sume.com"
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}

def generate(prompt):
    key = hashlib.sha256(prompt.encode()).hexdigest()[:32]
    res = requests.post(BASE + "/v1/videos", headers={**HEAD, "Idempotency-Key": key},
                        json={"model": "sume/auto", "prompt": prompt}, timeout=60)
    res.raise_for_status()
    job = res.json()
    while job["status"] in ("pending", "in_progress"):
        time.sleep(30)
        job = requests.get(BASE + "/v1/videos/" + job["id"], headers=HEAD, timeout=60).json()
    if job["status"] != "completed":
        return None
    url = BASE + "/v1/videos/" + job["id"] + "/content?index=0"
    return requests.get(url, headers=HEAD, timeout=300).content

prompt = st.text_input("Prompt")
if st.button("Generate") and prompt:
    with st.spinner("Rendering, this can take a minute or more"):
        data = generate(prompt)
    if data:
        st.video(data)
    else:
        st.error("The job did not complete")

Keep the key off the page

Never pass the content URL to st.video or an HTML tag in the browser; the request needs your Bearer token, and the token must stay server side. Save the bytes to disk or object storage if the user may come back later, as the storage post recommends.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume