diff --git a/extensions/vydra/image-generation-provider.test.ts b/extensions/vydra/image-generation-provider.test.ts index 47298210a7fd..2143b1b8d6b1 100644 --- a/extensions/vydra/image-generation-provider.test.ts +++ b/extensions/vydra/image-generation-provider.test.ts @@ -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 () => { diff --git a/extensions/vydra/image-generation-provider.ts b/extensions/vydra/image-generation-provider.ts index f583bcb3dc29..9f24cdffc581 100644 --- a/extensions/vydra/image-generation-provider.ts +++ b/extensions/vydra/image-generation-provider.ts @@ -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: [ diff --git a/extensions/vydra/shared.test.ts b/extensions/vydra/shared.test.ts index 035f51b6e7a7..1e395949c6bf 100644 --- a/extensions/vydra/shared.test.ts +++ b/extensions/vydra/shared.test.ts @@ -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>(); + 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({ diff --git a/extensions/vydra/shared.ts b/extensions/vydra/shared.ts index 70da22d0a24a..1e9e08dd3c74 100644 --- a/extensions/vydra/shared.ts +++ b/extensions/vydra/shared.ts @@ -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[0]["store"]; +type VydraRequestPolicy = Pick< + ReturnType, + "allowPrivateNetwork" | "dispatcherPolicy" | "headers" +> & { + headerOrigin: string; + ssrfPolicy?: SsrFPolicy; +}; + type VydraMediaKind = Extract; 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["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[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 { const deadline = params.deadline ?? @@ -299,7 +363,7 @@ async function waitForVydraJob(params: { }); return await pollProviderOperationJson({ 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 { 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, }); } diff --git a/extensions/vydra/speech-provider.test.ts b/extensions/vydra/speech-provider.test.ts index f6422843cd4d..8ae30b4b48a8 100644 --- a/extensions/vydra/speech-provider.test.ts +++ b/extensions/vydra/speech-provider.test.ts @@ -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")); diff --git a/extensions/vydra/speech-provider.ts b/extensions/vydra/speech-provider.ts index bc24b856d0ad..0ad1c33468c0 100644 --- a/extensions/vydra/speech-provider.ts +++ b/extensions/vydra/speech-provider.ts @@ -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, diff --git a/extensions/vydra/video-generation-provider.test.ts b/extensions/vydra/video-generation-provider.test.ts index beebda92a9b8..f788290638e9 100644 --- a/extensions/vydra/video-generation-provider.test.ts +++ b/extensions/vydra/video-generation-provider.test.ts @@ -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( diff --git a/extensions/vydra/video-generation-provider.ts b/extensions/vydra/video-generation-provider.ts index 02310b558b0a..01bdaab0b1c6 100644 --- a/extensions/vydra/video-generation-provider.ts +++ b/extensions/vydra/video-generation-provider.ts @@ -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: [