fix(vydra): apply request policy to media generation requests (#105884)

* fix(vydra): apply request policy to media generation requests

* fix(vydra): normalize body deadline aborts
This commit is contained in:
Alix-007
2026-07-22 12:49:40 +08:00
committed by GitHub
parent 17006d7de5
commit ce905af286
8 changed files with 270 additions and 62 deletions
@@ -116,13 +116,17 @@ describe("vydra image-generation provider", () => {
).rejects.toThrow("vydra.image-generation: JSON response exceeds 16777216 bytes");
});
it("passes request SSRF policy to the image creation request", async () => {
it("passes request SSRF policy through image creation, polling, and download", async () => {
stubVydraApiKey();
const fetchMock = stubFetch(
jsonResponse({
jobId: "job-123",
status: "queued",
}),
jsonResponse({
jobId: "job-123",
status: "completed",
imageUrl: "https://cdn.vydra.ai/generated/test.png",
imageUrl: "https://198.18.0.11/generated/test.png",
}),
binaryResponse("png-data", "image/png"),
);
@@ -137,6 +141,7 @@ describe("vydra image-generation provider", () => {
providers: {
vydra: {
baseUrl: "https://198.18.0.10/api/v1",
request: { headers: { "X-Vydra-Policy": "cross-origin" } },
},
},
},
@@ -147,6 +152,15 @@ describe("vydra image-generation provider", () => {
const createCall = fetchCall(fetchMock);
expect(createCall[0]).toBe("https://198.18.0.10/api/v1/models/grok-imagine");
expect(createCall[1].method).toBe("POST");
expect(new Headers(createCall[1].headers).get("x-vydra-policy")).toBe("cross-origin");
const pollCall = fetchCall(fetchMock, 1);
expect(pollCall[0]).toBe("https://198.18.0.10/api/v1/jobs/job-123");
expect(new Headers(pollCall[1].headers).get("x-vydra-policy")).toBe("cross-origin");
const downloadCall = fetchCall(fetchMock, 2);
expect(downloadCall[0]).toBe("https://198.18.0.11/generated/test.png");
const downloadHeaders = new Headers(downloadCall[1].headers);
expect(downloadHeaders.get("authorization")).toBeNull();
expect(downloadHeaders.get("x-vydra-policy")).toBeNull();
});
it("polls jobs when the create response is not completed yet", async () => {
@@ -156,7 +170,7 @@ describe("vydra image-generation provider", () => {
jsonResponse({
jobId: "job-456",
status: "completed",
resultUrls: ["https://cdn.vydra.ai/generated/polled.png"],
resultUrls: ["https://www.vydra.ai/generated/polled.png"],
}),
binaryResponse("png-data", "image/png"),
);
@@ -166,12 +180,26 @@ describe("vydra image-generation provider", () => {
provider: "vydra",
model: "grok-imagine",
prompt: "draw a cat",
cfg: {},
cfg: {
models: {
providers: {
vydra: {
baseUrl: "https://www.vydra.ai/api/v1",
models: [],
request: { headers: { "X-Vydra-Policy": "same-origin" } },
},
},
},
},
});
const pollCall = fetchCall(fetchMock, 1);
expect(pollCall[0]).toBe("https://www.vydra.ai/api/v1/jobs/job-456");
expect(pollCall[1].method).toBe("GET");
expect(new Headers(pollCall[1].headers).get("x-vydra-policy")).toBe("same-origin");
const downloadHeaders = new Headers(fetchCall(fetchMock, 2)[1].headers);
expect(downloadHeaders.get("authorization")).toBe("Bearer vydra-test-key");
expect(downloadHeaders.get("x-vydra-policy")).toBe("same-origin");
});
it("rejects job poll JSON responses that exceed the provider cap", async () => {
+13 -12
View File
@@ -54,27 +54,27 @@ export function buildVydraImageGenerationProvider(): ImageGenerationProvider {
throw new Error("Vydra image generation supports at most one image per request.");
}
const { fetchFn, baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
await resolveVydraRequestContext({
cfg: req.cfg,
agentDir: req.agentDir,
authStore: req.authStore,
capability: "image",
});
const { fetchFn, baseUrl, requestPolicy } = await resolveVydraRequestContext({
cfg: req.cfg,
agentDir: req.agentDir,
authStore: req.authStore,
capability: "image",
ssrfPolicy: req.ssrfPolicy,
});
const model = req.model?.trim() || DEFAULT_VYDRA_IMAGE_MODEL;
const { response, release } = await postJsonRequest({
url: `${baseUrl}/models/${model}`,
headers,
headers: requestPolicy.headers,
body: {
prompt: req.prompt,
model: "text-to-image",
},
timeoutMs: req.timeoutMs,
fetchFn,
allowPrivateNetwork,
ssrfPolicy: req.ssrfPolicy,
dispatcherPolicy,
allowPrivateNetwork: requestPolicy.allowPrivateNetwork,
ssrfPolicy: requestPolicy.ssrfPolicy,
dispatcherPolicy: requestPolicy.dispatcherPolicy,
});
try {
@@ -83,11 +83,11 @@ export function buildVydraImageGenerationProvider(): ImageGenerationProvider {
const completedPayload = await resolveCompletedVydraPayload({
submitted,
baseUrl,
headers,
timeoutMs: req.timeoutMs,
fetchFn,
kind: "image",
missingJobIdMessage: "Vydra image generation response missing job id",
requestPolicy,
});
const imageUrl = extractVydraResultUrls(completedPayload, "image")[0];
if (!imageUrl) {
@@ -99,6 +99,7 @@ export function buildVydraImageGenerationProvider(): ImageGenerationProvider {
timeoutMs: req.timeoutMs,
fetchFn,
maxBytes: resolveVydraGeneratedMediaMaxBytes({ cfg: req.cfg, kind: "image" }),
requestPolicy,
});
return {
images: [
+36
View File
@@ -2,13 +2,22 @@
import { once } from "node:events";
import http from "node:http";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import { installPinnedHostnameTestHooks } from "openclaw/plugin-sdk/test-media-understanding";
import { afterEach, describe, expect, it } from "vitest";
import { downloadVydraAsset } from "./shared.js";
describe("downloadVydraAsset", () => {
installPinnedHostnameTestHooks();
let server: http.Server | undefined;
const dripTimers = new Set<ReturnType<typeof setTimeout>>();
const requestPolicyFor = (url: string, allowPrivateNetwork = false) => ({
allowPrivateNetwork,
headers: new Headers(),
headerOrigin: new URL(url).origin,
});
afterEach(async () => {
for (const timer of dripTimers) {
clearTimeout(timer);
@@ -71,6 +80,7 @@ describe("downloadVydraAsset", () => {
timeoutMs,
fetchFn: fetch,
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor(`http://127.0.0.1:${port}`, true),
}),
).rejects.toThrow(`Vydra image download timed out after ${timeoutMs}ms`);
const elapsedMs = performance.now() - startedAt;
@@ -95,6 +105,7 @@ describe("downloadVydraAsset", () => {
timeoutMs,
fetchFn: fetch,
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor(`http://127.0.0.1:${port}`, true),
}),
).rejects.toThrow(`Vydra image download timed out after ${timeoutMs}ms`);
const elapsedMs = performance.now() - startedAt;
@@ -117,6 +128,7 @@ describe("downloadVydraAsset", () => {
},
),
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor("https://cdn.vydra.example"),
}).catch((error: unknown) => error);
expect(result).toMatchObject({
@@ -137,6 +149,7 @@ describe("downloadVydraAsset", () => {
timeoutMs: 250,
fetchFn: async () => new Response(null, { status: 304 }),
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor("https://cdn.vydra.example"),
}).catch((error: unknown) => error);
expect(result).toMatchObject({ name: "ProviderHttpError", status: 304, statusCode: 304 });
@@ -160,6 +173,7 @@ describe("downloadVydraAsset", () => {
},
),
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor("https://cdn.vydra.example"),
}).catch((error: unknown) => error);
expect(result).toMatchObject({
@@ -170,6 +184,28 @@ describe("downloadVydraAsset", () => {
});
});
it("preserves successful response body errors before the deadline", async () => {
const result = await downloadVydraAsset({
url: "https://cdn.vydra.example/generated/test.png",
kind: "image",
timeoutMs: 250,
fetchFn: async () =>
new Response(
new ReadableStream({
start(controller) {
controller.error(new Error("broken success body"));
},
}),
{ status: 200 },
),
maxBytes: 1024 * 1024,
requestPolicy: requestPolicyFor("https://cdn.vydra.example"),
}).catch((error: unknown) => error);
expect(result).toBeInstanceOf(Error);
expect(result).toMatchObject({ message: "broken success body" });
});
it("does not bound a dripping body when only chunk idle timeout is used", async () => {
// Negative control: chunkTimeoutMs resets on every drip, so idle alone never fires.
const port = await listenDripServer({
+102 -34
View File
@@ -6,13 +6,15 @@ import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
createProviderOperationTimeoutResolver,
fetchWithTimeout,
fetchWithTimeoutGuarded,
pollProviderOperationJson,
resolveProviderHttpRequestConfig,
sanitizeConfiguredModelProviderRequest,
type ProviderOperationDeadline,
type ProviderOperationTimeoutMs,
} from "openclaw/plugin-sdk/provider-http";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
@@ -31,6 +33,14 @@ const POLL_INTERVAL_MS = 2_500;
const MAX_POLL_ATTEMPTS = 120;
type VydraAuthStore = Parameters<typeof resolveApiKeyForProvider>[0]["store"];
type VydraRequestPolicy = Pick<
ReturnType<typeof resolveProviderHttpRequestConfig>,
"allowPrivateNetwork" | "dispatcherPolicy" | "headers"
> & {
headerOrigin: string;
ssrfPolicy?: SsrFPolicy;
};
type VydraMediaKind = Extract<MediaKind, "audio" | "image" | "video">;
type VydraJobPayload = {
@@ -99,12 +109,11 @@ export async function resolveVydraRequestContext(params: {
agentDir?: string;
authStore?: VydraAuthStore;
capability: "image" | "video";
ssrfPolicy?: SsrFPolicy;
}): Promise<{
fetchFn: typeof fetch;
baseUrl: string;
allowPrivateNetwork: boolean;
headers: Headers;
dispatcherPolicy: ReturnType<typeof resolveProviderHttpRequestConfig>["dispatcherPolicy"];
requestPolicy: VydraRequestPolicy;
}> {
const auth = await resolveApiKeyForProvider({
provider: "vydra",
@@ -116,11 +125,11 @@ export async function resolveVydraRequestContext(params: {
throw new Error("Vydra API key missing");
}
const fetchFn = fetch;
const providerConfig = params.cfg.models?.providers?.vydra;
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolveVydraBaseUrlFromConfig(params.cfg),
defaultBaseUrl: DEFAULT_VYDRA_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
@@ -128,13 +137,18 @@ export async function resolveVydraRequestContext(params: {
provider: "vydra",
capability: params.capability,
transport: "http",
request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
});
return {
fetchFn,
baseUrl,
allowPrivateNetwork,
headers,
dispatcherPolicy,
requestPolicy: {
allowPrivateNetwork,
dispatcherPolicy,
headers,
headerOrigin: new URL(baseUrl).origin,
...(params.ssrfPolicy ? { ssrfPolicy: params.ssrfPolicy } : {}),
},
};
}
@@ -221,6 +235,32 @@ function createVydraTimeoutError(deadline: ProviderOperationDeadline): Error {
return new Error(`${deadline.label} timed out${timeoutLabel}`);
}
function resolveVydraGuardedRequestOptions(
policy: VydraRequestPolicy,
): NonNullable<Parameters<typeof fetchWithTimeoutGuarded>[4]> {
const ssrfPolicy = policy.allowPrivateNetwork
? { ...policy.ssrfPolicy, allowPrivateNetwork: true }
: policy.ssrfPolicy;
return {
...(ssrfPolicy ? { ssrfPolicy } : {}),
...(policy.dispatcherPolicy ? { dispatcherPolicy: policy.dispatcherPolicy } : {}),
auditContext: "vydra-media-download",
};
}
function resolveVydraAssetRequestHeaders(
url: string,
policy: VydraRequestPolicy,
): Headers | undefined {
try {
// Same-origin assets may need the configured provider headers. Cross-origin
// result URLs must not receive the Vydra API credential or custom headers.
return new URL(url).origin === policy.headerOrigin ? policy.headers : undefined;
} catch {
return undefined;
}
}
export function resolveVydraGeneratedMediaMaxBytes(params: {
cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };
kind: VydraMediaKind;
@@ -244,6 +284,7 @@ export async function downloadVydraAsset(params: {
timeoutMs?: ProviderOperationTimeoutMs;
fetchFn: typeof fetch;
maxBytes: number;
requestPolicy: VydraRequestPolicy;
}): Promise<{ buffer: Buffer; mimeType: string; fileName: string }> {
const timeoutMs = resolveVydraHttpTimeoutMs(params.timeoutMs);
const deadline = createProviderOperationDeadline({
@@ -254,42 +295,65 @@ export async function downloadVydraAsset(params: {
deadline,
defaultTimeoutMs: timeoutMs,
});
const response = await fetchWithTimeout(
const headers = resolveVydraAssetRequestHeaders(params.url, params.requestPolicy);
const result = await fetchWithTimeoutGuarded(
params.url,
{ method: "GET" },
{
method: "GET",
...(headers ? { headers } : {}),
},
resolveTimeoutMs(),
params.fetchFn,
resolveVydraGuardedRequestOptions(params.requestPolicy),
);
await assertOkOrThrowHttpError(response, `Vydra ${params.kind} download failed`, {
bodyTimeoutMs: resolveTimeoutMs,
onBodyTimeout: () => createVydraTimeoutError(deadline),
});
const mimeType =
response.headers.get("content-type")?.trim() ||
(params.kind === "image" ? "image/png" : params.kind === "audio" ? "audio/mpeg" : "video/mp4");
const buffer = await readResponseWithLimit(response, params.maxBytes, {
timeoutMs: resolveTimeoutMs,
onTimeout: () => createVydraTimeoutError(deadline),
onOverflow: ({ maxBytes }) =>
new Error(`Vydra ${params.kind} download exceeds ${maxBytes} bytes`),
});
const extension = resolveVydraFileExtension(params.kind, mimeType);
const fileStem = params.kind === "image" ? "image" : params.kind === "audio" ? "audio" : "video";
return {
buffer,
mimeType,
fileName: `${fileStem}-1.${extension}`,
};
try {
try {
await assertOkOrThrowHttpError(result.response, `Vydra ${params.kind} download failed`, {
bodyTimeoutMs: resolveTimeoutMs,
onBodyTimeout: () => createVydraTimeoutError(deadline),
});
const mimeType =
result.response.headers.get("content-type")?.trim() ||
(params.kind === "image"
? "image/png"
: params.kind === "audio"
? "audio/mpeg"
: "video/mp4");
const buffer = await readResponseWithLimit(result.response, params.maxBytes, {
timeoutMs: resolveTimeoutMs,
onTimeout: () => createVydraTimeoutError(deadline),
onOverflow: ({ maxBytes }) =>
new Error(`Vydra ${params.kind} download exceeds ${maxBytes} bytes`),
});
const extension = resolveVydraFileExtension(params.kind, mimeType);
const fileStem =
params.kind === "image" ? "image" : params.kind === "audio" ? "audio" : "video";
return {
buffer,
mimeType,
fileName: `${fileStem}-1.${extension}`,
};
} catch (error) {
// The guarded request signal remains active through body consumption and
// can win the same absolute-deadline race. Keep timeout precedence stable.
if (typeof deadline.deadlineAtMs === "number" && Date.now() >= deadline.deadlineAtMs) {
throw createVydraTimeoutError(deadline);
}
throw error;
}
} finally {
await result.release();
}
}
async function waitForVydraJob(params: {
baseUrl: string;
jobId: string;
headers: Headers;
timeoutMs?: number;
deadline?: ProviderOperationDeadline;
fetchFn: typeof fetch;
kind: VydraMediaKind;
requestPolicy: VydraRequestPolicy;
}): Promise<unknown> {
const deadline =
params.deadline ??
@@ -299,7 +363,7 @@ async function waitForVydraJob(params: {
});
return await pollProviderOperationJson<unknown>({
url: `${params.baseUrl}/jobs/${params.jobId}`,
headers: params.headers,
headers: params.requestPolicy.headers,
deadline,
defaultTimeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
fetchFn: params.fetchFn,
@@ -307,6 +371,10 @@ async function waitForVydraJob(params: {
pollIntervalMs: POLL_INTERVAL_MS,
requestFailedMessage: "Vydra job status request failed",
timeoutMessage: `Vydra job ${params.jobId} did not finish in time`,
allowPrivateNetwork: params.requestPolicy.allowPrivateNetwork,
ssrfPolicy: params.requestPolicy.ssrfPolicy,
dispatcherPolicy: params.requestPolicy.dispatcherPolicy,
auditContext: "vydra-job-status",
isComplete: (payload) =>
resolveVydraResponseStatus(payload) === "completed" ||
extractVydraResultUrls(payload, params.kind).length > 0,
@@ -322,12 +390,12 @@ async function waitForVydraJob(params: {
export async function resolveCompletedVydraPayload(params: {
submitted: unknown;
baseUrl: string;
headers: Headers;
timeoutMs?: number;
deadline?: ProviderOperationDeadline;
fetchFn: typeof fetch;
kind: VydraMediaKind;
missingJobIdMessage: string;
requestPolicy: VydraRequestPolicy;
}): Promise<unknown> {
if (
resolveVydraResponseStatus(params.submitted) === "completed" ||
@@ -342,10 +410,10 @@ export async function resolveCompletedVydraPayload(params: {
return waitForVydraJob({
baseUrl: params.baseUrl,
jobId,
headers: params.headers,
timeoutMs: params.timeoutMs,
...(params.deadline ? { deadline: params.deadline } : {}),
fetchFn: params.fetchFn,
kind: params.kind,
requestPolicy: params.requestPolicy,
});
}
+3 -1
View File
@@ -42,7 +42,7 @@ describe("vydra speech provider", () => {
.mockResolvedValueOnce(
new Response(
JSON.stringify({
audioUrl: "https://cdn.vydra.ai/generated/test.mp3",
audioUrl: "https://www.vydra.ai/generated/test.mp3",
}),
{
status: 200,
@@ -78,6 +78,8 @@ describe("vydra speech provider", () => {
);
const headers = new Headers(init.headers);
expect(headers.get("authorization")).toBe("Bearer vydra-test-key");
const [, downloadInit] = fetchMock.mock.calls[1] as [string, RequestInit];
expect(new Headers(downloadInit.headers).get("authorization")).toBe("Bearer vydra-test-key");
expect(result.outputFormat).toBe("mp3");
expect(result.fileExtension).toBe(".mp3");
expect(result.audioBuffer).toEqual(Buffer.from("mp3-data"));
+6
View File
@@ -146,6 +146,12 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin {
timeoutMs: req.timeoutMs,
fetchFn,
maxBytes: resolveVydraGeneratedMediaMaxBytes({ cfg: req.cfg, kind: "audio" }),
requestPolicy: {
allowPrivateNetwork,
dispatcherPolicy,
headers,
headerOrigin: new URL(baseUrl).origin,
},
});
return {
audioBuffer: audio.buffer,
@@ -1,4 +1,5 @@
// Vydra tests cover video generation provider plugin behavior.
import * as providerHttp from "openclaw/plugin-sdk/provider-http";
import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
import { installPinnedHostnameTestHooks } from "openclaw/plugin-sdk/test-media-understanding";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -80,6 +81,72 @@ describe("vydra video-generation provider", () => {
});
});
it("carries configured request policy through video submit, poll, and download", async () => {
stubVydraApiKey();
const postJsonRequestSpy = vi.spyOn(providerHttp, "postJsonRequest");
const pollProviderOperationJsonSpy = vi.spyOn(providerHttp, "pollProviderOperationJson");
const fetchWithTimeoutGuardedSpy = vi.spyOn(providerHttp, "fetchWithTimeoutGuarded");
const fetchMock = stubFetch(
jsonResponse({ jobId: "job-policy", status: "processing" }),
jsonResponse({
jobId: "job-policy",
status: "completed",
videoUrl: "https://198.18.0.10/generated/policy.mp4",
}),
binaryResponse("mp4-data", "video/mp4"),
);
const provider = buildVydraVideoGenerationProvider();
await provider.generateVideo({
provider: "vydra",
model: "veo3",
prompt: "policy proof",
cfg: {
models: {
providers: {
vydra: {
baseUrl: "https://198.18.0.10/api/v1",
models: [],
request: {
allowPrivateNetwork: true,
headers: { "X-Vydra-Policy": "video-policy" },
proxy: { mode: "env-proxy" },
},
},
},
},
},
});
const submitRequest = postJsonRequestSpy.mock.calls[0]?.[0];
expect(submitRequest?.allowPrivateNetwork).toBe(true);
expect(submitRequest?.dispatcherPolicy).toMatchObject({ mode: "env-proxy" });
expect(submitRequest?.headers.get("x-vydra-policy")).toBe("video-policy");
const pollRequest = pollProviderOperationJsonSpy.mock.calls[0]?.[0];
expect(pollRequest?.allowPrivateNetwork).toBe(true);
expect(pollRequest?.dispatcherPolicy).toBe(submitRequest?.dispatcherPolicy);
const pollHeaders = new Headers(
typeof pollRequest?.headers === "function" ? pollRequest.headers() : pollRequest?.headers,
);
expect(pollHeaders.get("x-vydra-policy")).toBe("video-policy");
const downloadRequest = fetchWithTimeoutGuardedSpy.mock.calls.find(
([url]) => url === "https://198.18.0.10/generated/policy.mp4",
);
expect(downloadRequest?.[4]).toMatchObject({
ssrfPolicy: { allowPrivateNetwork: true },
dispatcherPolicy: submitRequest?.dispatcherPolicy,
auditContext: "vydra-media-download",
});
for (const index of [0, 1, 2]) {
const headers = new Headers((fetchCall(fetchMock, index)[1] as RequestInit).headers);
expect(headers.get("authorization")).toBe("Bearer vydra-test-key");
expect(headers.get("x-vydra-policy")).toBe("video-policy");
}
});
it("rejects generated video downloads that exceed the configured media cap", async () => {
stubVydraApiKey();
stubFetch(
+11 -11
View File
@@ -85,13 +85,12 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
throw new Error("Vydra video generation does not support video reference inputs.");
}
const { fetchFn, baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
await resolveVydraRequestContext({
cfg: req.cfg,
agentDir: req.agentDir,
authStore: req.authStore,
capability: "video",
});
const { fetchFn, baseUrl, requestPolicy } = await resolveVydraRequestContext({
cfg: req.cfg,
agentDir: req.agentDir,
authStore: req.authStore,
capability: "video",
});
const deadline = createProviderOperationDeadline({
timeoutMs: req.timeoutMs ?? DEFAULT_VYDRA_VIDEO_TIMEOUT_MS,
label: "Vydra video generation",
@@ -99,15 +98,15 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
const { model, body } = resolveVydraVideoRequestBody(req);
const { response, release } = await postJsonRequest({
url: `${baseUrl}/models/${model}`,
headers,
headers: requestPolicy.headers,
body,
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: DEFAULT_VYDRA_VIDEO_TIMEOUT_MS,
}),
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
allowPrivateNetwork: requestPolicy.allowPrivateNetwork,
dispatcherPolicy: requestPolicy.dispatcherPolicy,
});
try {
@@ -119,11 +118,11 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
const completedPayload = await resolveCompletedVydraPayload({
submitted,
baseUrl,
headers,
deadline,
fetchFn,
kind: "video",
missingJobIdMessage: "Vydra video generation response missing job id",
requestPolicy,
});
const videoUrl = extractVydraResultUrls(completedPayload, "video")[0];
if (!videoUrl) {
@@ -138,6 +137,7 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
}),
fetchFn,
maxBytes: resolveVydraGeneratedMediaMaxBytes({ cfg: req.cfg, kind: "video" }),
requestPolicy,
});
return {
videos: [