Skip to content

feat(videos): unified /v1/videos submit/poll/content surface (Phase 1, DashScope)#811

Merged
moonming merged 3 commits into
mainfrom
feat/videos-endpoint-p1
Jul 23, 2026
Merged

feat(videos): unified /v1/videos submit/poll/content surface (Phase 1, DashScope)#811
moonming merged 3 commits into
mainfrom
feat/videos-endpoint-p1

Conversation

@moonming

@moonming moonming commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the typed video-generation endpoint family — Phase 1 of the unified /v1/videos surface. Design decisions are pinned in the internal issue api7/AISIX-Cloud#1118 (#1116 surfaced the gap: video traffic could only ride the raw passthrough tunnel, so model routing, model-level rate limiting, budgets, and guardrail attribution were absent for it).

Three routes in crates/aisix-proxy/src/videos.rs:

  • POST /v1/videos — submit a generation task. OpenAI-shape request (model, prompt, optional seconds as string or int, optional size as WIDTHxHEIGHT); responds with the video job object.
  • GET /v1/videos/{video_id} — poll status.
  • GET /v1/videos/{video_id}/content302 redirect to the provider's video URL when the task succeeded; 400 invalid_request_error while queued/in-progress or when generation failed.

Contract sources

  • Client-facing contract: the videos API three-stage shape, https://platform.openai.com/docs/api-reference/videos. Field names pinned against the official openai-python SDK (src/openai/types/video.py, video_create_params.py, video_create_error.py): id, object: "video", model, status (queued / in_progress / completed / failed), progress, created_at, seconds, size, and the {code, message} error object on failed jobs. The docs page rejects unauthenticated fetches, so the generated SDK types serve as the verifiable spec snapshot.
  • Provider mapping: Alibaba Model Studio text-to-video reference, https://help.aliyun.com/zh/model-studio/text-to-video-api-reference. Submit POST {api_base}/api/v1/services/aigc/video-generation/video-synthesis with the mandatory X-DashScope-Async: enable header and the {model, input: {prompt}, parameters} envelope; poll GET {api_base}/api/v1/tasks/{task_id}; the video URL arrives in the poll response (output.video_url). Status mapping: PENDING→queued, RUNNING→in_progress, SUCCEEDED→completed, FAILED/CANCELED/UNKNOWN→failed.

A unified submit/poll/content surface with this exact status enum is the shape established gateways converge on for video generation; the differentiation here is provider coverage, starting with DashScope.

Design decisions (binding, from the internal issue)

  • Stateless task addressing: video_id = base64url_nopad(model_entry_id + ":" + upstream_task_id). GET routes decode the id, re-resolve the Model row and its ProviderKey from the live snapshot, and call the provider's task endpoint. Nothing is persisted — multi-instance DPs need no shared task store. Unknown or undecodable ids → typed 404 (video_not_found, new ProxyError::VideoNotFound variant). Decoded task ids pass the same charset guard the jobs surface applies before URL interpolation (jobs::require_safe_upstream_id, widened to pub(crate)).
  • Rate limiting: submit calls quota::enforce(state, auth, Some(&model_rl)) — the same shape as chat/embeddings and the layer #1116 added to passthrough. The two GET routes pass None for the model layer by design: a client polling a task it already paid an RPM slot to submit must not starve itself. Key-level layers still apply to polls.
  • Guardrails: the resolved input guardrail chain scans the submit prompt (same resolution path as embeddings), before the rate-limit reservation so a policy block doesn't burn an RPM slot; blocked-pattern detail stays in operator logs only. No output scan on the GET routes (the outputs are URLs/status, not text).
  • Usage: one zero-token UsageEvent per accepted submit (inbound_protocol: "openai", label videos), with per-PK attribution and guardrail observability fields; failed submits emit the standard zero-token error event. Per-second cost is deliberately not modeled here — the ModelCost per-second extension is a separate control-plane PR (see Deferred). GET routes emit access logs + bounded metrics but no usage events (poll traffic carries no billing signal).
  • ACL/IP: auth.key().can_access(...) + the model client-IP allowlist are enforced on all three routes before any upstream contact. Metric model labels are the resolved display name or the fixed unresolved sentinel — never raw caller input.

Mapping notes / divergences

  • secondsparameters.duration (integer). Accepted as string (the SDK's wire type) or bare integer.
  • size "WIDTHxHEIGHT"parameters.size "WIDTH*HEIGHT" — the explicit-dimension parameter the Wan model families document. The Alibaba reference notes the wan2.7 line replaced size with resolution/ratio quality tiers ("wan2.7 不再使用 size 字段…而早期模型直接使用 size 字段"); an arbitrary WIDTHxHEIGHT cannot be represented in that tier system without a lossy invented mapping, so this PR maps to the documented explicit-dimension form and callers targeting tier-based models should omit size. A tier mapping for those models is a follow-up on the same surface.
  • created_at is populated at submit time; DashScope's poll response reports no machine-readable creation timestamp and the gateway stores nothing, so poll responses carry 0. seconds on completed polls comes from the provider's usage block when present.
  • There is no built-in default api_base for the alibaba vendor — the ProviderKey must carry the regional DashScope endpoint (existing resolve_base_url error path).
  • Non-DashScope providers on these routes return a typed 501 not_implemented naming the provider, so "wrong provider" is distinguishable from "wrong request".

Deferred (explicitly out of this PR, tracked in the internal issue)

  • Zhipu CogVideoX and Volcano Seedance provider mappings (Phase 1 follow-ups on this same surface).
  • CP-side per-second cost schema (cp-admin.yaml ModelCost extension + dashboard form) — a separate control-plane PR; until it lands, video usage events carry zero tokens and no cost.
  • Streaming content proxy for providers whose URLs require gateway credentials (Phase 1 fetches by 302 redirect — zero relay bandwidth).
  • MiniMax (Files-API fetch indirection) — Phase 2 per the issue.

Audit remediation (independent cold review of 028299f)

The audit returned 1 HIGH + 5 MEDIUM gating findings; all six are addressed in the follow-up commit:

  • HIGH-1 (metric cardinality): the GET routes' unsupported-provider branch recorded the caller-supplied video id as the Prometheus model label. Fixed — GET error paths record the fixed unresolved sentinel; pinned by a unit test that renders the metrics registry and asserts the sentinel appears and the raw id does not.
  • MED-2 (wildcard-alias ACL asymmetry): submit checked the requested alias, the GETs checked the stored display name — a key allowlisted for wan/turbo on a wan/* Model could not poll its own task. Fixed — the id now carries the requested alias as an inner base64url segment (b64url(entry_id : b64url(alias) : task_id)) and the GETs run can_access(alias), the identical check the submit performed. Covered by a wildcard submit→poll→content unit journey.
  • MED-3 (disclosure oracle on crafted ids): a guessed entry UUID could surface the model display name via 403 and distinguish existence via 403/501-vs-404. Fixed — on the GET routes, ACL denial and unsupported-provider both fold into a uniform 404 video_not_found. Submit keeps its distinct 403/501: the caller already knows the model name they asked for, so there is nothing to disclose. Covered by a restricted-key test asserting 404 + zero upstream calls + no name in the message.
  • MED-4 (redirect target validation): the content route now parses the provider-supplied URL and forwards only absolute http/https targets; other schemes are a typed upstream-decode error. Accepted residual: the redirect target is provider-supplied — the gateway validates scheme and header-safety, but an operator-misconfigured or compromised upstream can still choose the host; the upstream is operator-trusted infrastructure and the gateway never fetches the URL itself.
  • MED-5 (api_base foot-gun): alibaba keys commonly carry the OpenAI-compatible base (…/compatible-mode/v1), which produced …/compatible-mode/v1/api/v1/services/… upstream 404s. Fixed — dashscope_root() strips one trailing /compatible-mode/v1 / /api/v1 / /v1 in both URL builders; unit tests pin the rewrite including the outbound path seen by a mock upstream.
  • MED-6 (coverage): mandated tests above, plus an allowed_cidrs enforcement case on the poll route.

Non-gating fixes taken: require_safe_upstream_id now rejects bare . / .. segments (shared with the jobs surface); submit responses report progress: 100 when the provider returns an already-completed status.

Low-severity dispositions (not changed in this PR):

  • L-1 (no default upstream client timeout): pre-existing family behavior — the shared http_client has no default timeout and every handler applies the model's configured timeout identically; changing the default is a family-wide decision, not a videos-surface one.
  • L-2 (raw requested alias as the metric label on the submit success path): pre-existing family convention shared with embeddings/images — the label is post-resolution (the name resolved against the Model table), so it is bounded by configuration; aligning the whole family on resolved-display-name labels is a family-wide follow-up.
  • L-6 (no modality gate): the Model schema has no modality field yet — provider == "alibaba" is the Phase 1 gate; a modality marker is a control-plane schema question (canonical in cp-admin.yaml) and is tracked with the per-second cost work.
  • L-7 (snapshot TOCTOU between decode and dispatch): benign — a Model row swapped between snapshot loads yields at worst a 404/501 on a stale id, never a credential mix-up (the PK is resolved from the same snapshot as the entry).

Test evidence

  • Unit (videos.rs mod tests, 25 cases): status-mapping table; video_id encode/decode roundtrip incl. task ids containing :; tamper rejection (non-base64, missing separator, empty halves, non-UTF8, raw provider ids); submit param mapping incl. omitted-params and malformed-size rejection; seconds string/int normalisation; wiremock handler tests for submit happy path (asserts the DashScope wire body + X-DashScope-Async header), poll status mapping, failed-task error object, content 302 + Location, content not-ready 400, unknown-id 404 (both undecodable and ghost-model), unsupported-provider 501, missing-field 400 envelope, guardrail block 422 with expect(0) upstream calls, model rpm=1 submit 429 with polling exempt, and upstream 4xx message passthrough.
  • E2E (tests/e2e/src/cases/videos-e2e.test.ts, source-blind, real binary against etcd + scripted DashScope-shaped mock upstreams): submit→poll→content-302 happy path asserting the client envelope and the upstream wire shape; model rpm=1 → second submit 429 rate_limit_exceeded with Retry-After and zero extra upstream calls; polling not blocked after the cap; unknown/ghost video ids → 404 with no upstream contact.
  • cargo fmt clean; cargo clippy --workspace --tests zero warnings; cargo test -p aisix-proxy --lib 688 passed; cargo build --bin aisix ok; new e2e file 3/3 passed; existing passthrough e2e family (4 files, 7 tests) all passed. Branch rebased onto latest main (post-fix(proxy): estimate ensemble panel + judge sub-call usage on missing-usage backends #806) and re-verified; full gate re-run after the audit remediation commit (fmt clean, workspace clippy 0 warnings, 688 lib tests, binary build, videos e2e 3/3 + passthrough family 7/7).

Summary by CodeRabbit

  • New Features

    • Added OpenAI-compatible video generation endpoints for submitting jobs, checking status, and retrieving completed video content.
    • Added support for queued, in-progress, completed, and failed video states.
    • Added validation for video duration, size, redirect URLs, access permissions, and request rate limits.
    • Unknown video IDs now return a clear 404 error.
  • Bug Fixes

    • Improved protection against unsafe identifiers and path injection.
    • Prevented redirects to unsupported URL schemes.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb325a76-0f86-43cf-bd9f-db4e0d471403

📥 Commits

Reviewing files that changed from the base of the PR and between d03358d and f7808eb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/error.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/videos-e2e.test.ts
📝 Walkthrough

Walkthrough

Adds a unified /v1/videos API backed by DashScope, covering video submission, status polling, and content redirects with validation, access control, rate limiting, telemetry, error mapping, unit tests, and end-to-end tests.

Changes

Unified video API

Layer / File(s) Summary
Video contracts and routing
Cargo.toml, crates/aisix-proxy/Cargo.toml, crates/aisix-proxy/src/error.rs, crates/aisix-proxy/src/jobs.rs, crates/aisix-proxy/src/lib.rs, crates/aisix-proxy/src/videos.rs
Adds video request/response types, identifier encoding, status normalization, error mappings, safe upstream-id validation, and the three /v1/videos routes.
Video submission flow
crates/aisix-proxy/src/videos.rs
Resolves providers, validates requests, applies guardrails and submission rate limits, submits asynchronous DashScope tasks, and emits zero-token usage events.
Polling and content delivery
crates/aisix-proxy/src/videos.rs
Polls DashScope tasks, maps statuses and failures, exempts polling from model rate limits, and returns validated HTTP redirects for completed content.
Lifecycle and security validation
crates/aisix-proxy/src/videos.rs, tests/e2e/src/cases/videos-e2e.test.ts
Covers codec and parameter validation, ACL and allowlist behavior, guardrails, rate limiting, provider gating, redirect safety, unknown ids, and the complete submit–poll–content flow.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • api7/AISIX-Cloud#1118 — Covers the same unified /v1/videos submit–poll–content API, DashScope integration, validation, rate limiting, and end-to-end testing.

Suggested reviewers: jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error Success path uses raw body.model as the request metric label, enabling wildcard aliases to explode shared Prometheus cardinality. Bound the success-path label like the error path: use metric_model_label(...) or thread the resolved display name into CreateSuccess.
E2e Test Quality Review ⚠️ Warning E2E flow coverage is solid, but submit success still records raw caller model_name in metrics, risking unbounded label cardinality despite bounded-label helpers elsewhere. Use metric_model_label (or resolved display_name) in the success telemetry.finish call, and add a regression test for wildcard-alias submits.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a Phase 1 DashScope-backed unified /v1/videos submit/poll/content API.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/videos-endpoint-p1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-proxy/src/videos.rs`:
- Around line 585-611: Bound the success-path telemetry model label in the
submit handler before calling telemetry.finish, using the existing
metric_model_label(&snap, &model_name) logic already used by the error path.
Pass the bounded label instead of the raw caller-controlled model_name, while
preserving the existing success response and usage-event behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 953ff6ea-e0b3-40c6-a4b2-0d7e87691529

📥 Commits

Reviewing files that changed from the base of the PR and between f8b5e0f and d03358d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/error.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/videos-e2e.test.ts

Comment thread crates/aisix-proxy/src/videos.rs
moonming added 3 commits July 23, 2026 17:37
…hScope mapping

Add the typed video-generation endpoint family (api7/AISIX-Cloud#1118
Phase 1):

- POST /v1/videos — submit a task. OpenAI-shape request (model, prompt,
  seconds, size); returns the video job object (object:"video", status
  queued/in_progress/completed/failed, progress, created_at).
- GET /v1/videos/{video_id} — poll status.
- GET /v1/videos/{video_id}/content — 302 redirect to the provider's
  video URL once the task succeeded.

Stateless task addressing: video_id = base64url_nopad(model_entry_id +
":" + upstream_task_id). The GET routes decode it, re-resolve the Model
and ProviderKey from the snapshot, and call the provider's task
endpoint — nothing is persisted, so multi-instance DPs need no shared
task store. Unknown/undecodable ids are a typed 404 (video_not_found).

Provider coverage in this PR: Alibaba DashScope (Wan) — submit POST
{api_base}/api/v1/services/aigc/video-generation/video-synthesis with
the mandatory X-DashScope-Async: enable header, poll GET
{api_base}/api/v1/tasks/{task_id}; task statuses normalise
PENDING→queued, RUNNING→in_progress, SUCCEEDED→completed,
FAILED/CANCELED/UNKNOWN→failed. seconds → parameters.duration; size
"WIDTHxHEIGHT" → parameters.size "WIDTH*HEIGHT". Other providers return
a typed 501 not_implemented.

Wiring matches the typed-endpoint family: ACL + client-IP allowlist,
input guardrail chain over the submit prompt (before the rate-limit
reservation), model-level quota enforcement on submit, one zero-token
UsageEvent per accepted submit, bounded metric labels. The two GET
routes are exempt from model-level rate limits by design — polling a
task must not burn the model RPM the submit already paid.

Deferred within Phase 1 (tracked in api7/AISIX-Cloud#1118): Zhipu
CogVideoX and Volcano Seedance mappings, the CP-side per-second cost
schema, and a streaming content proxy for large files.
Remediations from the independent PR audit (api7/AISIX-Cloud#1118):

- Bounded metric labels on the GET routes: the unsupported-provider
  branch recorded the caller-supplied video id as the Prometheus model
  label — an authenticated caller could explode metric cardinality by
  minting random ids. GET error paths now record the fixed `unresolved`
  sentinel.
- Wildcard-alias ACL parity: the video id now carries the requested
  alias as an inner base64url segment (entry_id : b64url(alias) :
  task_id), and the GET routes run the key ACL against that alias —
  the identical check the submit performed — so a key allowlisted for
  `wan/turbo` can poll the task it submitted on a `wan/*` Model.
- Disclosure-oracle fold on GETs: ACL denials and the unsupported-
  provider case both surface as a uniform 404 `video_not_found`, so
  crafted ids cannot probe the Model table (403 echoed a model
  identity; 501 revealed entry existence + provider family). Submit
  keeps its distinct 403/501 — the caller already knows the name there.
- Redirect scheme validation: the content route parses the provider-
  supplied video URL and only forwards absolute http(s) targets; any
  other scheme is a typed upstream-decode error.
- api_base tolerance: `dashscope_root()` strips one trailing
  `/compatible-mode/v1`, `/api/v1`, or `/v1` from the ProviderKey base
  so keys configured with the OpenAI-compatible root still reach the
  native task endpoints instead of opaque upstream 404s.
- Hardening: `require_safe_upstream_id` rejects bare `.` / `..` path
  segments (shared with the jobs surface); submit responses report
  progress 100 when the provider returns an already-completed status.

New unit coverage: metric-label boundedness, wildcard submit→poll→
content journey, restricted-key 404 with zero upstream calls,
compatible-mode base URL rewrite, non-http scheme rejection,
allowed_cidrs on the poll route, extended codec roundtrip/tamper table.
…lias

Review follow-up: on the submit success (and 501) path the raw
requested model name reached the Prometheus model label; a wildcard
Model accepts unbounded alias strings, so this was the #451
cardinality shape on the new route. Label by the resolved entry's
display_name (exact matches unchanged; wildcard traffic aggregates
under the pattern), with a registry-rendering test pinning that the
alias never appears.
@moonming
moonming force-pushed the feat/videos-endpoint-p1 branch from 333ce37 to f7808eb Compare July 23, 2026 09:40
@moonming
moonming merged commit 0401fa7 into main Jul 23, 2026
12 checks passed
@moonming
moonming deleted the feat/videos-endpoint-p1 branch July 23, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant