Video generation
Serve the OpenAI Videos API as jobs that each caller creates, polls, downloads, deletes and lists through the gateway.
4 min readUpdated for v2.4.0Edit on GitHub
model_type: "video"cost_per_video per created jobA video is a job, not one request and one response. The create call returns a job id at once, the backend renders in the background, and the client polls the job until it can download the file.
POST /v1/videos create a job (prompt, optional reference image)
GET /v1/videos/{id} job status, as the backend reports it
GET /v1/videos/{id}/content the finished video
DELETE /v1/videos/{id} delete the job
GET /v1/videos the caller's own jobs
Run a job
- 1
Register the model
curl -X POST http://localhost:9180/api/v1/models \ -H "Authorization: Bearer $OBLETH_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "model_name": "video-gen", "model_type": "video", "upstream_model": "my-video-model", "api_base": "https://video.example.internal/v1", "cost_per_video": 0.50, "enabled": true }'api_baseends in/v1; the gateway appends/videos. You can also setcost_per_videoin the model's form on the Models page. - 2
Create a job
curl -s http://localhost:8088/v1/videos \ -H "Authorization: Bearer $OBLETH_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "video-gen", "prompt": "A paper boat on a rain-soaked street", "seconds": "4", "size": "1280x720"}'To send a reference image, use
multipart/form-datawith OpenAI'sinput_referencefield:curl -s http://localhost:8088/v1/videos \ -H "Authorization: Bearer $OBLETH_KEY" \ -F model=video-gen -F prompt="The boat sails out of frame" \ -F input_reference=@first-frame.pngThe response is the backend's own video object. The fields a job accepts and the statuses it goes through are the backend's; the gateway does not check them.
- 3
Poll, download and clean up
curl -s http://localhost:8088/v1/videos/$VIDEO_ID -H "Authorization: Bearer $OBLETH_KEY" curl -s http://localhost:8088/v1/videos/$VIDEO_ID/content -H "Authorization: Bearer $OBLETH_KEY" -o video.mp4 curl -s -X DELETE http://localhost:8088/v1/videos/$VIDEO_ID -H "Authorization: Bearer $OBLETH_KEY" curl -s http://localhost:8088/v1/videos -H "Authorization: Bearer $OBLETH_KEY"
With the OpenAI SDK:
import time
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8088/v1", api_key="sk_...")
video = client.videos.create(model="video-gen", prompt="A paper boat on a rain-soaked street")
while video.status in ("queued", "in_progress"):
time.sleep(10)
video = client.videos.retrieve(video.id)
if video.status == "completed":
client.videos.download_content(video.id).write_to_file("video.mp4")
What follow-up calls do
The gateway records every job it creates: its id, model, tenant, the key that created it and the endpoint that accepted it. Polls, downloads, deletes and listings are served from that record.
- They go back to the same endpoint, whatever its health now. If the model has since been disabled or removed the call returns 503; if the endpoint is gone from the model, 502.
- The backend's answer passes through. A download while the video is still rendering returns whatever not-ready response the backend gives.
- Downloads stream. The file comes through chunk by chunk with the backend's
Content-Type,Content-LengthandContent-Disposition. - Delete drops the record once the backend confirms (or answers 404). Any other answer keeps it, so you can retry.
- List asks each backend holding one of your jobs and returns only your entries, newest first. With no jobs you get an empty list.
- They are free. Follow-ups check the key and tenant but take no slot, reserve no budget and write no usage row.
Who can see a job
OBLETH_VIDEO_JOB_SCOPE (Helm obleth.videoJobScope) decides who may follow up on a job:
| Scope | Poll, download, delete | GET /v1/videos |
|---|---|---|
key (default) | Only the key that created the job | Only the caller's own jobs |
tenant | Any key of the same tenant | Every job of the caller's tenant |
A job id you don't own returns 404 no such video job, the same as an id that never existed, and the backend is never asked. Another tenant's job is always a 404. Any other value falls back to key with a warning at startup.
Each user is one key: a person with a secret key, or an SSO caller with a per-user identity key. Under key, each user sees only their own videos. Choose tenant when a tenant's keys deliberately share renders. The scope applies at request time, so changing it affects existing jobs immediately.
Under key, a disabled key is refused before the lookup, and a rotated or recreated key is a new key, so neither reaches the old key's jobs. Those jobs finish or expire on the backend.
Billing and admission
A create is charged the model's cost_per_video once the backend accepts the job and the gateway has recorded it, as request type video with no token counts.
- A create the backend rejects is not charged.
- A reply with no usable job id returns 502
the upstream did not return a video job id, not charged. - If the job cannot be recorded, the gateway deletes it on the backend (best effort) and returns 503, not charged. You never get a job id that was not recorded.
- A create is never served from the response cache.
The create takes a slot in the model's pool only while the create call runs, so fairshare limits how fast jobs are created, not how many render at once. Budgets reserve and settle on the create like any request. Input guardrails scan the prompt; a reference image is not scanned.
Job records, ids and retention
Records live in the Postgres video_jobs table rather than Redis, because the Redis eviction policy could drop a key with a TTL mid-render. Every gateway replica prunes records older than OBLETH_VIDEO_JOB_RETENTION_HOURS (default 24; Helm obleth.videoJobRetentionHours) once an hour. A follow-up for a pruned job returns 404.
Job ids are the backend's. The gateway accepts any id of 1 to 256 printable ASCII characters except one containing /, \, ?, # or %, or . and .., and forwards it percent-encoded as one path segment.
A multipart create is sent once and never retried. A JSON create follows the model's retry and failover settings. The model's upstream headers go on the create and every follow-up.
Health and what is not handled yet
Video models get no generation probe; like image models, the check is whether the backend's catalog lists them. GET /v1/models reports them as model_type: "video", mode: "video_generation".
Not handled in this release: admission that counts renders in progress, charging on completion, progress webhooks, and other Videos API operations such as remix.
