Use Decimal, not float, to reconcile Sume video costs to the cent
Sume bills list x 1.25 and rounds each video job up to the cent. Python Decimal with ROUND_CEILING reproduces the bill exactly, where floats drift by a cent.

Use Python's decimal.Decimal with ROUND_CEILING to estimate a Sume video bill: multiply the provider list price per second by 1.25 and by the seconds, then round up to the cent for each job. Floats can land a hair on the wrong side of a rounding boundary and move a job by a cent, which becomes a reconciliation mystery at volume.
When the Sora video models went away on 2026-09-24, per OpenAI's deprecations page, many teams rebuilt their cost reports from scratch. A report that cannot match the invoice to the cent is not a report, it is a guess.
The billing rule
The repo docs say Sume bills provider list times 1.25, rounded up to the cent for each job. List prices come from the model docs: Omni at 720p is $0.10 a second, Wan 3.0 at 1080p is $0.20, and minimax-h3 at 768p is $0.06. A five-second Omni clip is therefore 5 x 0.10 x 1.25 = 0.625, which bills as $0.63.
The ceiling applies to each job, then you sum. Summing exact values first and rounding at the end gives a different, smaller total than the invoice.
| Job | Arithmetic | Exact | Billed |
|---|---|---|---|
| Omni 720p, 5 s | 0.10 x 1.25 x 5 | $0.625 | $0.63 |
| Wan 3.0 1080p, 7 s | 0.20 x 1.25 x 7 | $1.75 | $1.75 |
| minimax-h3 768p, 9 s | 0.06 x 1.25 x 9 | $0.675 | $0.68 |
Reproducing it
The script builds each price from strings, never from float literals, so 0.10 is exactly one tenth. It multiplies, then quantizes to two places with ROUND_CEILING. The three jobs bill $0.63, $1.75 and $0.68, totaling $3.06, while the exact sum is $3.05, a cent apart. The last line prints a classic float comparison, which is False, as a reminder of why the table is built from strings.
from decimal import Decimal, ROUND_CEILING
LIST = {("gemini-omni-flash-1.1", "720p"): Decimal("0.10"),
("wan-3.0", "1080p"): Decimal("0.20"),
("minimax-h3", "768p"): Decimal("0.06")}
MARGIN = Decimal("1.25")
def bill(model, res, seconds):
exact = LIST[(model, res)] * MARGIN * seconds
return exact, exact.quantize(Decimal("0.01"), rounding=ROUND_CEILING)
if __name__ == "__main__":
total = Decimal(0)
for model, res, s in [("gemini-omni-flash-1.1", "720p", 5), ("wan-3.0", "1080p", 7),
("minimax-h3", "768p", 9)]:
exact, billed = bill(model, res, s)
total += billed
print(f"{model} {res} {s}s exact {exact} billed {billed}")
print("total", total)
print("float check:", 0.1 + 0.2 == 0.3)
Where to apply it
Use it for the estimate you show before submit, and for the expected value you compare against the cost field on each completed job. The poll response carries usage.cost, and a mismatch larger than a cent is worth a look: the wrong resolution in your table is the usual cause.
Store money as integer cents or as strings in your database, not as floating-point columns. Convert to Decimal at the edges.
- Quantize per job, then sum.
- Keep the rate table in one module, with the date you read it.
- Seedance bills per 1,000 video tokens, so do not force it into this per-second table.
A test worth writing
Add a unit test with the three jobs above and assert the billed values $0.63, $1.75 and $0.68 and the total of $3.06. When prices or the margin change, the test fails and tells you to update the table. That one test protects every dashboard number downstream from a silent change.
Sources
Related posts
More in Developers
- 'Use either first/end frame fields or reference_*_urls, not both'
A first frame pins the opening shot; references only guide it. Video Router refuses to mix them, so pick one mode per job and chain jobs if you need both.
- /v1/videos 409 job_failed vs job_not_completed in retry loops
On /v1/videos/{id}/content, 409 job_not_completed is retryable and means keep polling; 409 job_failed is not retryable. Branch on the code, not the 409.
- /v1/videos canonical_slug: the stable model id to store
Store canonical_slug (or id) from GET /v1/videos/models, not the display name. On Sume ids are bare, like seedance-2, with no org prefix.
- /v1/videos status expired: in the enum, never sent by Sume
The /v1/videos status enum includes expired for OpenRouter compatibility, but Sume never emits it. Handle it as terminal anyway; five other statuses occur.
Written by Sume