Files
9router/open-sse/handlers/videoProviders/openrouter.js
coozgan 3288bbc47e feat(video): add OpenRouter and Vertex AI (Veo) video generation
Video generation was xAI-only. Adds an adapter layer under
open-sse/handlers/videoProviders/ so /v1/videos/* can target OpenRouter or
Google Cloud credentials. A provider with no adapter keeps the exact previous
behaviour (raw body to {baseUrl}/{action}, poll {baseUrl}/{id}, verbatim
passthrough), so the xAI path is unchanged.

- openrouter: async job shape identical to xAI; creation POSTs to the /videos
  collection root (no /generations suffix) and the registry HTTP-Referer /
  X-Title headers are applied. Bodies pass through verbatim.
- vertex: two-way translation, since Veo does not speak the OpenAI-ish videos
  shape. create -> :predictLongRunning { instances[], parameters{} }, poll ->
  :fetchPredictOperation (Veo has no REST GET poll). The operation resource
  name is base64url-encoded into the job id so GET /v1/videos/{id} stays a
  flat path. Access tokens are minted from Service Account JSON via the
  existing refreshVertexToken; raw API keys are rejected up front. The
  operation response maps back onto the { id, status, video, videos } shape
  clients already poll.
- videoCore: the request plan is rebuilt per attempt, so the 401 -> refresh
  once -> retry once path picks up the refreshed token. Adapter validation
  errors return 400 before any upstream call, so a malformed request can never
  create a billable job.
- videoGeneration: GET /v1/videos/{id} resolves the provider from the pinned
  x-connection-id connection, then ?provider=, then falls back to the xAI
  default.
- registry: openrouter and vertex gain videoConfig, the video serviceKind and
  video-kind models (Veo 3.1 / 3 / 2, Sora 2 Pro, Seedance 2.0).
2026-09-10 22:05:22 +07:00

40 lines
1.4 KiB
JavaScript

// OpenRouter video jobs — https://openrouter.ai/docs/api/api-reference/videos
//
// Same async shape as xAI (POST → { id, status }, GET → status/unsigned_urls),
// two differences only: creation POSTs to the collection root (no `/generations`
// suffix) and the account headers come from the registry entry.
// Response bodies are passed through verbatim.
// ponytail: generations only — OpenRouter has no edits/extensions endpoint today.
const SUPPORTED_ACTIONS = new Set(["generations"]);
function headers(config, token) {
return {
Accept: "application/json",
...(config.headers || {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export default {
buildRequest({ config, action, requestId, rawBody, contentType, token }) {
const base = config.baseUrl.replace(/\/$/, "");
if (requestId) {
return { method: "GET", url: `${base}/${encodeURIComponent(requestId)}`, headers: headers(config, token) };
}
if (!SUPPORTED_ACTIONS.has(action)) {
return { error: `OpenRouter video supports 'generations' only (got '${action}')` };
}
if (contentType && !contentType.includes("application/json")) {
return { error: "OpenRouter video requires an application/json body" };
}
return {
method: "POST",
url: base,
headers: { ...headers(config, token), "Content-Type": "application/json" },
body: rawBody,
};
},
};