Skip to content

feat(videos): content-streaming proxy + OpenAI Sora as first consumer - #820

Merged
moonming merged 3 commits into
mainfrom
feat/videos-content-proxy-sora
Jul 26, 2026
Merged

feat(videos): content-streaming proxy + OpenAI Sora as first consumer#820
moonming merged 3 commits into
mainfrom
feat/videos-content-proxy-sora

Conversation

@moonming

@moonming moonming commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What & why

Foundational piece for the /v1/videos surface: a per-provider content-delivery mode so providers whose finished video requires the provider's own credential to download can be served without ever exposing that credential to the client. Ships the mechanism plus its first consumer (OpenAI Sora) as one e2e-able unit. Refs api7/AISIX-Cloud#1118 (binding design).

The four existing providers (Alibaba / Zhipu / Volcengine / Runway) return signed, credential-free public URLs and stay exactly as they are (302 redirect).

Part A — content-delivery mode (Redirect vs Proxy)

GET /v1/videos/:id/content, on a completed task, now dispatches on a ContentDelivery enum:

Mode Providers Behavior
Redirect(url) Alibaba, Zhipu, Volcengine, Runway Existing 302 to the provider's signed URL. Zero relay bandwidth. Unchanged (still scheme-validated http/https).
Proxy { url } OpenAI Sora (later Google-direct/Vertex Veo, Azure Sora) Gateway GETs the provider's authenticated content endpoint with the provider bearer injected and streams the bytes back. Credential never reaches the client.

Why streaming, not buffering: reqwest bytes_stream() is bridged straight into axum::body::Body::from_stream — the video is never buffered whole in memory (constant memory). An established gateway buffers the entire file into memory before returning it; we stream. Stream-bridging API verified against the tree versions: reqwest 0.12 (stream feature on) → Response::bytes_stream(), axum 0.7Body::from_stream, reusing the same #554 wrappers (send_with_deadline for connect, with_read_timeout_bytes for per-chunk reads, bounded by the model's stream_timeout_effective()).

Upstream-error correctness: the upstream content status is checked before any body is constructed. A non-2xx (403 expired/not-ready, 404) is read as a small body, mapped to a typed BridgeError::upstream_status, and returned as a JSON error envelope — never streamed back labelled video/mp4.

Relays upstream Content-Type and Content-Length (when present); sets Content-Disposition: attachment. Rate limiting on the content route stays enforce(state, auth, None) (caller layers only, exempt from model limits — a download is a bandwidth op). Decoded task id is charset-guarded before it's interpolated into the new content-proxy URL.

Part B — OpenAI Sora (openai), first Proxy consumer

All facts pinned against the OpenAI videos API reference and the openai-python SDK:

Fact Value Result Source
submit POST {base}/videos confirmed resources/videos.py (self._post("/videos", …))
poll GET {base}/videos/{id} confirmed resources/videos.py (self._get("/videos/{video_id}", …))
content GET {base}/videos/{id}/contentProxy confirmed resources/videos.py (self._get("/videos/{video_id}/content", …))
variant query param video/thumbnail/spritesheet, default video (= MP4) not set — default is the MP4 we want resources/videos.py download_content
Range / resumable not documented plain streamed GET (noted) SDK returns HttpxBinaryResponseContent, no Range param
status enum queued/in_progress/completed/failed 1:1 with our four values (identity map; unknown→failed) types/video.py
progress int 0–100 passed through (others stay binary 0/100) types/video.py
seconds Literal["4","8","12"] (string) our seconds → string enum (near-identity) types/video_create_params.py
size WIDTHxHEIGHT (e.g. 1280x720) verbatim after shape validation types/video_create_params.py
error {code, message} mapped to unified error object types/video_create_error.py
model sora-2 / sora-2-pro, default sora-2 passed through as upstream model_name types/video_create_params.py
default base https://api.openai.com/v1 first video provider with a default base — openai Model w/ no api_base falls back to the same default the chat path uses (aisix_provider_openai::OPENAI_DEFAULT_BASE); the other four still require api_base aisix-provider-openai/src/bridge.rs

OpenAI composes its URLs via the shared build_v1_url (owns the /v1 prefix, tolerates both bare-host and …/v1 bases), so no bespoke root-stripper was invented.

Deferred (now unblocked by the proxy — each its own follow-up PR)

  • Google-direct Veo, Vertex Veo — same Proxy mode, different auth (GCP OAuth).
  • Azure Sora — Azure OpenAI base + api-version.
  • Luma, Bedrock Nova Reel — S3-mediated fetch.
  • CP per-second cost accounting stays a separate CP PR; provider: openai already exists in the catalog, so no new cp-admin.yaml enum is required for this DP change.

Test evidence

  • Rust unit (cargo test -p aisix-proxy --lib): 722 passed. New: openai status map, near-identity submit body (string seconds), build_v1_url/default-base URL composition, content-mode selection (openai→Proxy, other four→Redirect + task-id path-traversal rejection), and the Proxy upstream-403→typed-error path. Full Sora submit→poll→content-proxy handler test asserts the bearer is injected on the upstream content GET, bytes stream back as video/mp4, and the provider key never appears in the client body.
  • e2e (videos-sora-content-proxy-e2e.test.ts): full Sora journey against a mock upstream — (a) provider bearer injected on the upstream content GET, (b) MP4 bytes streamed to the client as video/mp4 with Content-Disposition: attachment, (c) provider key absent from the client bytes, (d) upstream 403 on content → JSON error envelope (not a video body). Existing 4-provider 302 journeys (videos-providers-e2e.test.ts, videos-e2e.test.ts) stay green — still 302, not proxy.
  • Full gate: cargo fmt --check clean, cargo clippy --workspace --tests 0 warnings, cargo build --bin aisix ok, videos + passthrough e2e families green (etcd on 127.0.0.1:2379).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for creating, tracking, and downloading videos through OpenAI Sora.
    • Video downloads now stream efficiently while preserving content type and attachment details.
    • Progress updates are displayed using provider-reported percentages when available.
    • Invalid download URLs and upstream service errors now receive clear, structured responses.
  • Bug Fixes

    • Improved handling of provider errors and failed video downloads.
    • Prevented provider credentials from appearing in downloaded content.
  • Tests

    • Added coverage for Sora video submission, progress tracking, streaming downloads, and error handling.

Add a per-provider content-delivery mode to /v1/videos so providers whose
finished video requires the provider's own credential to download can be
served without ever exposing that credential to the client.

Part A — content-streaming proxy mechanism:
- `GET /v1/videos/:id/content` now branches on a ContentDelivery enum:
  - Redirect(url): the existing 302 to a signed, credential-free provider
    URL (Alibaba/Zhipu/Volcengine/Runway) — unchanged, zero relay bandwidth.
  - Proxy { url }: the gateway GETs the provider's authenticated content
    endpoint with the provider bearer injected and streams the bytes back.
- True streaming pass-through: reqwest `bytes_stream()` is bridged straight
  into `axum::body::Body::from_stream` — the whole file is never buffered in
  memory. (An established gateway buffers the entire file before returning;
  we stream, for constant memory.)
- Upstream status is checked BEFORE any body is built: a non-2xx content GET
  (403 expired/not-ready, 404) maps to a typed error and returns a JSON error
  envelope — never a truncated `video/mp4` body.
- Relays upstream Content-Type / Content-Length (when present); sets
  `Content-Disposition: attachment`.
- Connect + per-chunk reads bounded by the model streaming budget via the
  existing #554 wrappers (send_with_deadline / with_read_timeout_bytes) — no
  new timeout knob.
- Content route rate limiting stays `enforce(None)` (caller layers only).

Part B — OpenAI Sora (openai), first Proxy-mode consumer:
- submit `POST {base}/videos`, poll `GET {base}/videos/{id}`, content
  `GET {base}/videos/{id}/content` (Proxy). Status enum maps 1:1 to the four
  unified values; real `progress` percentage passed through (others stay
  binary 0/100). seconds→string enum, size WIDTHxHEIGHT verbatim (near
  identity — inbound is already OpenAI-shaped).
- First video provider WITH a default base: an openai video Model with no
  api_base falls back to the same default the chat path uses; the other four
  still require api_base.
- Field names/paths pinned against the openai-python SDK (resources/videos.py,
  types/video.py, types/video_create_params.py, types/video_create_error.py).

Tests: Sora status map, submit body shape, default-base URL composition,
content-mode selection (openai→Proxy, others→Redirect), upstream-error→typed
error for Proxy content, and a full Sora submit→poll→content-proxy e2e (bearer
injected upstream, bytes streamed as video/mp4, key not leaked, upstream 403 →
JSON error). Existing 4-provider unit + e2e journeys stay green (still 302).

Refs api7/AISIX-Cloud#1118
@coderabbitai

coderabbitai Bot commented Jul 26, 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: 36 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: e24567d6-d6dd-4868-9c06-e7fb274cee21

📥 Commits

Reviewing files that changed from the base of the PR and between 350cef0 and 1d40b33.

📒 Files selected for processing (2)
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts
📝 Walkthrough

Walkthrough

The /v1/videos gateway now supports OpenAI Sora submission, polling, granular progress, default base URLs, and authenticated proxy streaming for video content, with unified error handling and unit/e2e coverage.

Changes

OpenAI Sora provider integration

Layer / File(s) Summary
Provider adapter and routing
crates/aisix-proxy/src/videos.rs
Adds OpenAI request mapping, status normalization, URL composition, provider detection, and default base URL handling.
Polling and error normalization
crates/aisix-proxy/src/videos.rs
Maps Sora progress, duration, statuses, and nested errors into unified video responses while sharing vendor error extraction.
Authenticated content delivery
crates/aisix-proxy/src/videos.rs
Adds proxy streaming for OpenAI, redirect URL validation for other providers, response header forwarding, and typed upstream error responses.
Integration and end-to-end validation
crates/aisix-proxy/src/videos.rs, tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts, tests/e2e/src/harness/upstream-openai.ts
Tests Sora submission, polling, proxy streaming, bearer authentication, raw MP4 responses, and upstream 403 handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant VideoGateway
  participant OpenAI
  Client->>VideoGateway: Submit video request
  VideoGateway->>OpenAI: Create Sora video
  OpenAI-->>VideoGateway: Video job
  Client->>VideoGateway: Poll video status
  VideoGateway->>OpenAI: Get video job
  OpenAI-->>VideoGateway: Status and progress
  Client->>VideoGateway: Fetch video content
  VideoGateway->>OpenAI: Authenticated content request
  OpenAI-->>VideoGateway: MP4 bytes or error
  VideoGateway-->>Client: Streamed content or JSON error
Loading

Possibly related PRs

  • api7/aisix#811: Extends the same unified /v1/videos surface with provider-specific video behavior.
  • api7/aisix#814: Shares the provider routing, polling normalization, content handling, and error-processing infrastructure.

Suggested reviewers: jarvis9443

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a content-streaming proxy for videos with OpenAI Sora as the first consumer.
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.
E2e Test Quality Review ✅ Passed New e2e covers submit→poll→proxy content and 403 error against real app+etcd with scripted upstreams; assertions are clear and independent.
Security Check ✅ Passed No new secret leaks, auth bypasses, or ownership gaps found; OpenAI content proxy validates task IDs, redirects validate http(s), and errors stay typed JSON.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/videos-content-proxy-sora

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

🧹 Nitpick comments (2)
crates/aisix-proxy/src/videos.rs (1)

996-1015: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Handle a string-valued error field.

Some vendors return {"error": "human message"}. scope then becomes a JSON string, both .get("code") and .get("message") yield None, and the caller sees the generic "upstream error" instead of the vendor detail.

♻️ Suggested fallback
         .and_then(|p| {
+            if let Some(s) = nonempty_str(p.get("error")) {
+                return Some(s.to_string());
+            }
             let scope = if p.get("error").is_some() {
                 p.get("error").cloned().unwrap_or_default()
             } else {
                 p
             };
🤖 Prompt for 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.

In `@crates/aisix-proxy/src/videos.rs` around lines 996 - 1015, Update
parse_provider_error_message so a string-valued JSON error field is returned as
the provider message instead of falling back to "upstream error". Preserve the
existing code/message extraction for object-valued errors and the generic
fallback when no usable error detail exists.
tests/e2e/src/harness/upstream-openai.ts (1)

148-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Binary-streaming tests use a UTF-8-safe fixture, so they can't catch real byte-corruption bugs in the content-proxy path. The root cause is that rawBody is typed and encoded as a JS string (Buffer.from(str) defaults to UTF-8), so neither the harness nor the test can represent arbitrary binary bytes as a real MP4 would contain.

  • tests/e2e/src/harness/upstream-openai.ts#L148-L158: accept the payload directly as a Buffer/Uint8Array (skip Buffer.from re-encoding when already a Buffer) instead of always converting via UTF-8.
  • tests/e2e/src/harness/upstream-openai.ts#L33-L43: widen OpenAiUpstreamOptions.rawBody to string | Buffer.
  • tests/e2e/src/harness/upstream-openai.ts#L68-L71: widen OpenAiUpstreamStep.rawBody to string | Buffer to match.
  • tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts#L218-L279: define MP4_BYTES as a Buffer containing non-UTF-8-safe byte values, and assert via .arrayBuffer()/Buffer.compare instead of .text(), to genuinely validate byte-for-byte, constant-memory streaming.
🤖 Prompt for 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.

In `@tests/e2e/src/harness/upstream-openai.ts` around lines 148 - 158, The
binary-streaming fixture currently re-encodes string data as UTF-8, so it cannot
test arbitrary bytes. In tests/e2e/src/harness/upstream-openai.ts at lines 33-43
and 68-71, widen OpenAiUpstreamOptions.rawBody and OpenAiUpstreamStep.rawBody to
string | Buffer; at lines 148-158, send Buffer/Uint8Array payloads directly
while retaining string conversion for strings. In
tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts at lines 218-279,
define MP4_BYTES as a Buffer with non-UTF-8-safe bytes and compare the response
bytes using arrayBuffer/Buffer.compare instead of text.
🤖 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 1255-1261: Wrap the stream produced by
crate::stream_timeout::with_read_timeout_bytes in
crate::request_id::in_request_span before passing it to Body::from_stream, while
still within the handler. Preserve the existing timeout and response behavior so
each poll_next re-enters the current request span for chunk correlation.

---

Nitpick comments:
In `@crates/aisix-proxy/src/videos.rs`:
- Around line 996-1015: Update parse_provider_error_message so a string-valued
JSON error field is returned as the provider message instead of falling back to
"upstream error". Preserve the existing code/message extraction for
object-valued errors and the generic fallback when no usable error detail
exists.

In `@tests/e2e/src/harness/upstream-openai.ts`:
- Around line 148-158: The binary-streaming fixture currently re-encodes string
data as UTF-8, so it cannot test arbitrary bytes. In
tests/e2e/src/harness/upstream-openai.ts at lines 33-43 and 68-71, widen
OpenAiUpstreamOptions.rawBody and OpenAiUpstreamStep.rawBody to string | Buffer;
at lines 148-158, send Buffer/Uint8Array payloads directly while retaining
string conversion for strings. In
tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts at lines 218-279,
define MP4_BYTES as a Buffer with non-UTF-8-safe bytes and compare the response
bytes using arrayBuffer/Buffer.compare instead of text.
🪄 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: 5ddb4e2b-99e3-4bb3-b594-cc67fd4a1318

📥 Commits

Reviewing files that changed from the base of the PR and between 3295d56 and 350cef0.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/videos-sora-content-proxy-e2e.test.ts
  • tests/e2e/src/harness/upstream-openai.ts

Comment thread crates/aisix-proxy/src/videos.rs
…tion notes

Audit follow-ups on the content-streaming proxy:
- the non-2xx content-proxy branch read the upstream error body with no
  time bound; a stalled error body could hang the handler (the success
  path is already per-chunk bounded). Wrap the read in the same
  per-request stream budget.
- document that a mid-stream read timeout surfaces to the client as a
  short read against the relayed Content-Length (intended incomplete-
  download signal).
- e2e now also asserts the client response carries no Authorization
  header (structural no-leak, complements the body-bytes assertion).
@moonming

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Every other streaming path in the crate (chat, messages x2, responses,
responses_bridge) wraps its stream in request_id::in_request_span so
each poll_next re-enters the handler's request span. The new content
proxy did not, so its per-chunk read-timeout warnings would lose
request-id correlation. Wrap it the same way.
@moonming
moonming merged commit b369062 into main Jul 26, 2026
12 checks passed
@moonming
moonming deleted the feat/videos-content-proxy-sora branch July 26, 2026 09:13
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