Skip to content

fix(providers): poll OpenRouter video jobs and serve the clip safely - #16372

Merged
diegosouzapw merged 7 commits into
cheaper-inference:release/v3.8.52from
LuisAlejandro:fix/16369-openrouter-video-job
Oct 18, 2026
Merged

diegosouzapw merged 7 commits into
cheaper-inference:release/v3.8.52from
LuisAlejandro:fix/16369-openrouter-video-job

Conversation

@LuisAlejandro

@LuisAlejandro LuisAlejandro commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • OpenRouter video models are now callable. openrouter joins the video registry against a submit→poll preset, because POST /api/v1/videos answers 202 with a job id rather than a finished clip. The synchronous openai-video handler reported that 202 as success with an empty data array, so a combo stopped instead of trying its next target.
  • The finished clip is fetched with the provider credential and re-served from GET /api/v1/media/{id}, because unsigned_urls points at an endpoint that 401s without the OpenRouter key. An expired job is now terminal instead of polling until a misleading 504.
  • Media URLs are built from the caller's own forwarded host (X-Forwarded-Host/X-Forwarded-Proto, then Host), not the bind address or BASE_URL. Served media is scoped to the key that generated it, and files expire on the call-log retention window.

Related Issues

Validation

  • Change type: provider
  • Focused tests and category gates from the golden path
  • npm run lint
  • Reconciled with the current active release base; focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR
node --import tsx/esm --test tests/unit/services/openrouter-video-job.test.ts tests/unit/services/resolve-request-origin.test.ts   # 13/13 pass
node --import tsx/esm --test tests/unit/registry-direct-exports.test.ts tests/unit/media-provider-ordering.test.ts tests/unit/model-id-path-injection.test.ts tests/unit/local-catalog-import-media-api-format.test.ts   # pass
make typecheck        # clean
make lint             # clean
npm run check:provider-assets   # passed
  • npm run check:provider-consistency did not run locally: it is a bun script and bun is not installed in this environment (npm suggests cd node_modules/bun && node install.js). CI-only for this branch.
  • The provider.ts translate-path golden snapshot fails locally on zyloai, which this diff does not touch. release/v3.8.52 is already red — see 🔴 Release branch not green: release/v3.8.52 #15306.

⚠️ base-red inherited: #15306

Tests Added Or Updated

  • tests/unit/services/openrouter-video-job.test.ts — extended: registry→preset wiring, submit→poll completion served as a URL, owner-tag digesting, retention prune, api-key-context owner, malformed-id traversal, cancelled job.
  • tests/unit/services/resolve-request-origin.test.ts — new (forwarded host/proto precedence, comma-separated headers, direct-call fallback).

Coverage Notes

  • open-sse/handlers/videoGeneration/mediaStore.ts and src/shared/utils/resolveRequestOrigin.ts are covered directly by the two test files above, as are the preset's auth-gated download and owner stamping in open-sse/handlers/videoGeneration/job.ts.
  • src/app/api/v1/media/[id]/route.ts is thin and mostly delegates — the id pattern, the owner digest and the file read all live in mediaStore and are asserted — but the route's own ownership comparison (unowned id → 404) has no route-level test. Correcting an earlier claim in this description that the route was covered by these tests. Worth a reviewer's eye; say the word and I will add one.

Reviewer Notes

  • GET /api/v1/media/{id} is new. The id is validated against the exact pattern mediaStore generates, so it cannot escape MEDIA_DIR; an unowned id returns 404 rather than 403 so the route does not confirm that it exists; session auth may read any id.
  • Retention deliberately reuses CALL_LOG_RETENTION_DAYS (default 7) through a prune-on-write sweep: one knob, no scheduler. Lowering it also shortens how long served media URLs stay valid, and nothing expires while no generation runs.
  • The video route establishes runWithCallLogApiKeyContext, the wrapper the images and chat paths already use. Here it is what makes the media owner attributable; the same identity feeds the usage/call-log rows in fix(api): record job-preset video calls and attribute them to the API key #16378.
  • Overlap with fix(api): record job-preset video calls and attribute them to the API key #16378: both PRs set that wrapper on the video route. The comment and the helper are byte-identical in both, so that block merges cleanly; the diffs meet at the handleVideoGeneration call site, where this PR threads mediaOrigin, and in job.ts, where fix(api): record job-preset video calls and attribute them to the API key #16378 splits the function head for recording. Whichever lands second needs a trivial rebase (keep the incoming wrapper, add the other change).
  • openrouter is added to the video registry only; the chat provider of the same name is untouched.

OpenRouter was absent from the video registry, so its models were unreachable.
Wired through the synchronous openai-video handler instead, POST /api/v1/videos
answers 202 with a job id and the handler reported that as success with an empty
`data` array, which stopped a combo from trying the next target. The finished
clip arrives in `unsigned_urls` pointing at an endpoint that 401s without the
OpenRouter key, and an `expired` job was polled until the timeout.

Register openrouter against a new submit->poll preset. It treats `expired` as
terminal, downloads the auth-gated result with the provider credential, and
re-serves it from GET /api/v1/media/{id} as a URL built from the caller's
forwarded host rather than the bind address or BASE_URL.

Served media is scoped to the key that generated it: the owner travels in the id
as a truncated digest of the api key id, so no side table is needed and a URL
does not leak which key produced it. A mismatch is a 404 rather than a 403, so
the route does not confirm that an unowned id exists. Files expire on the
call-log retention window (CALL_LOG_RETENTION_DAYS, default 7) via a
prune-on-write sweep, so there is one retention knob and no scheduler.

The video route now establishes runWithCallLogApiKeyContext, the wrapper the
images and chat paths already use, which is what makes the media owner
attributable. The usage/call-log rows for this path are a follow-up.

Closes cheaper-inference#16369
@LuisAlejandro
LuisAlejandro force-pushed the fix/16369-openrouter-video-job branch from 46b65b0 to e8b7b4f Compare October 11, 2026 19:56
… proxied origins, scope media like files

- Move the media store to src/lib/media and inject a per-request media sink
  (owner + public origin) into the open-sse video job pipeline, removing the
  four new open-sse -> src imports (dataPaths, logEnv, callLogApiKeyContext,
  resolveRequestOrigin).
- Media URLs use the configured public base URL, then forwarding headers only
  from a trusted proxy (resolvePublicOrigin), then the direct Host; an
  untrusted X-Forwarded-Host is ignored.
- Media ownership follows canAccessOwnedRecord: the generating key (or the
  anonymous-owner sentinel under REQUIRE_API_KEY=false), a manage-scope key
  or the dashboard session; anonymous media is no longer readable by any
  authenticated key, and session media needs the session.
- The media route validates the id with Zod, rejects an invalid key with 401
  and answers through buildErrorBody; OpenAPI entry added.
- The provider credential is only sent to a result URL on the provider's own
  origin.
@diegosouzapw
diegosouzapw merged commit d0ae778 into cheaper-inference:release/v3.8.52 Oct 18, 2026
6 of 8 checks passed
diegosouzapw added a commit to Kizuno18/OmniRoute that referenced this pull request Oct 27, 2026
Resolve the five conflicts:
- imageRegistryData.ts: keep the tip's muse-code import and the PR's
  Grok Build endpoint import.
- videoGeneration.ts / videoGeneration/job.ts / videoCombo.ts: keep the
  tip's recordCall, mediaSink (cheaper-inference#16372), provider and poll-retry backoff
  (cheaper-inference#15536) and thread the PR's AbortSignal through them: the job wrapper
  forwards signal to runVideoJobGeneration, the poll wait is
  sleep(nextDelay, signal) with an abort check before the next poll, and
  fetchJson carries both raw and signal.
- PROVIDER_REFERENCE.md: regenerated with npm run gen:provider-reference.

Co-authored-by: Kizuno18 <appdoleo@gmail.com>
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.

fix(providers): OpenRouter video reports success and returns a URL the caller cannot open

2 participants