Sume 404 resource_not_found: job_not_found and model_not_found overlap
Every Sume 404 reports public_reason resource_not_found, whatever the code. Branch on error.code to tell a missing job from a bad model id or a missing asset.

On the Sume API every 404 carries public_reason: resource_not_found, category: validation, retryable: false and next_action: fix_input, whether the thing missing is a job, a model id or an asset. The public reason therefore cannot tell them apart. Branch on error.code (job_not_found, model_not_found, and so on) and use details for the specifics.
Why is the reason the same for every 404?
The error classifier treats any error with status 404, or the code not_found, as a missing resource and fills the same four fields. That keeps the envelope simple for generic clients: a 404 never means wait and retry, so retryable is false and the action is to fix the input.
A switch written on public_reason will fold a mistyped model id into the same branch as a job lookup under the wrong key, which need very different fixes. A switch on code keeps them apart.
What fixes each 404?
For a job, the usual cause is the key: a job id is visible to the member that created it, so a lookup under another member's key reads as not found. For a model, the response lists the accepted ids at the catalog_url in its details. For an asset or file, check the id belongs to your workspace and was not deleted.
| error.code | Fixed fields | Fix |
|---|---|---|
job_not_found | resource_not_found, fix_input | Use the creating member's key; check the id |
model_not_found | resource_not_found, fix_input | Pick an id from details.catalog_url |
not_found | resource_not_found, fix_input | Check path and ids |
How do I write the switch?
Keep the order: code first, then the shared fallback.
def explain_404(body: dict) -> str:
err = body.get("error") or {}
code = err.get("code")
details = err.get("details") or {}
if code == "job_not_found":
return "No such job for this key. Was it created by another member?"
if code == "model_not_found":
return f"Unknown model id. See {details.get('catalog_url', 'the model catalog')}."
if err.get("public_reason") == "resource_not_found":
return f"Not found ({code}). Check the id and your workspace."
return "Not a 404 envelope."
print(explain_404({"error": {"code": "model_not_found", "public_reason": "resource_not_found",
"details": {"catalog_url": "https://docs.sume.com/models"}}}))Sources
Related posts
More in Developers
- Sume 409 job_not_queued: the job left the queue before it started
What the Sume 409 job_not_queued means: a submit found its job no longer queued, sent nothing to the provider, and the error is not retryable by resending.
- Sume generation_capacity_exhausted: the 503, job reason and flag
Sume reports provider capacity three ways: HTTP 503 provider_capacity_exceeded, a job reason generation_capacity_exhausted, and a sync flag. All three retry.
- Sume job failed artifact_too_large: shrink the output, don't rerun
A Sume job failing with artifact_too_large or artifact_upload_rejected made its file but could not store it. Make the file smaller; rerunning changes nothing.
- pending_usd_micros vs held: what Sume's settle sweeper still owns
Sume /v1/usage splits open holds into held and pending_usd_micros. Read the two fields and settle_state to tell parked rows from spend, and when final flips.
Written by Sume