From bbac74685dd3cc5a09769a2df94c93ec66303388 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Sun, 12 Jul 2026 23:11:15 +0800
Subject: [PATCH] fix(dashscope): apply request policy to video requests
(#102094)
* fix(dashscope): apply request policy to video requests
* fix(dashscope): apply request policy to task follow-ups
---
.../alibaba/video-generation-provider.test.ts | 87 +++++++++++++-
.../alibaba/video-generation-provider.ts | 7 +-
.../qwen/video-generation-provider.test.ts | 93 ++++++++++++++-
extensions/qwen/video-generation-provider.ts | 7 +-
.../test-helpers/provider-http-mocks.ts | 2 +-
src/video-generation/dashscope-compatible.ts | 111 +++++++++++++-----
6 files changed, 275 insertions(+), 32 deletions(-)
diff --git a/extensions/alibaba/video-generation-provider.test.ts b/extensions/alibaba/video-generation-provider.test.ts
index 10efa0b07cc4..487bcade2edc 100644
--- a/extensions/alibaba/video-generation-provider.test.ts
+++ b/extensions/alibaba/video-generation-provider.test.ts
@@ -11,7 +11,13 @@ import {
} from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it } from "vitest";
-const { postJsonRequestMock, fetchWithTimeoutMock } = getProviderHttpMocks();
+const {
+ postJsonRequestMock,
+ fetchWithTimeoutMock,
+ fetchWithTimeoutGuardedMock,
+ resolveProviderHttpRequestConfigMock,
+ sanitizeConfiguredModelProviderRequestMock,
+} = getProviderHttpMocks();
let buildAlibabaVideoGenerationProvider: typeof import("./video-generation-provider.js").buildAlibabaVideoGenerationProvider;
@@ -74,6 +80,85 @@ describe("alibaba video generation provider", () => {
expectSuccessfulDashscopeVideoResult(result);
});
+ it("applies configured request policy to DashScope video requests", async () => {
+ const requestPolicy = {
+ allowPrivateNetwork: true,
+ headers: { "X-DashScope-Route": "alibaba-policy" },
+ };
+ const dispatcherPolicy = { mode: "env-proxy" as const };
+ resolveProviderHttpRequestConfigMock.mockImplementationOnce((params) => {
+ const headers = new Headers(params.defaultHeaders);
+ for (const [key, value] of Object.entries(params.request?.headers ?? {})) {
+ headers.set(key, value);
+ }
+ return {
+ baseUrl: params.baseUrl ?? params.defaultBaseUrl,
+ allowPrivateNetwork: params.request?.allowPrivateNetwork === true,
+ headers,
+ dispatcherPolicy,
+ };
+ });
+ mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
+
+ const provider = buildAlibabaVideoGenerationProvider();
+ await provider.generateVideo({
+ provider: "alibaba",
+ model: "wan2.6-t2v",
+ prompt: "animate this shot",
+ cfg: {
+ models: {
+ providers: {
+ alibaba: {
+ baseUrl: "https://dashscope-intl.aliyuncs.com",
+ models: [],
+ request: requestPolicy,
+ },
+ },
+ },
+ },
+ });
+
+ expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith(requestPolicy);
+ expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ provider: "alibaba",
+ capability: "video",
+ transport: "http",
+ request: requestPolicy,
+ }),
+ );
+ const request = requireFirstPostJsonRequest("DashScope request with request policy");
+ expect(request.allowPrivateNetwork).toBe(true);
+ expect(request.dispatcherPolicy).toBe(dispatcherPolicy);
+ expect(request.headers).toBeInstanceOf(Headers);
+ expect((request.headers as Headers).get("x-dashscope-route")).toBe("alibaba-policy");
+ expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
+ 1,
+ "https://dashscope-intl.aliyuncs.com/api/v1/tasks/task-1",
+ expect.objectContaining({
+ method: "GET",
+ headers: expect.any(Headers),
+ }),
+ 120_000,
+ fetch,
+ {
+ ssrfPolicy: { allowPrivateNetwork: true },
+ dispatcherPolicy,
+ },
+ );
+ expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
+ 2,
+ "https://example.com/out.mp4",
+ { method: "GET" },
+ 120_000,
+ fetch,
+ {
+ ssrfPolicy: { allowPrivateNetwork: true },
+ dispatcherPolicy,
+ },
+ );
+ });
+
it("fails fast when reference inputs are local buffers instead of remote URLs", async () => {
const provider = buildAlibabaVideoGenerationProvider();
diff --git a/extensions/alibaba/video-generation-provider.ts b/extensions/alibaba/video-generation-provider.ts
index 3e4da50805ee..098c312fbf05 100644
--- a/extensions/alibaba/video-generation-provider.ts
+++ b/extensions/alibaba/video-generation-provider.ts
@@ -4,7 +4,10 @@
*/
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
-import { resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
+import {
+ resolveProviderHttpRequestConfig,
+ sanitizeConfiguredModelProviderRequest,
+} from "openclaw/plugin-sdk/provider-http";
import {
DASHSCOPE_WAN_VIDEO_CAPABILITIES,
DASHSCOPE_WAN_VIDEO_MODELS,
@@ -54,6 +57,7 @@ export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
throw new Error("Alibaba Model Studio API key missing");
}
+ const providerConfig = req.cfg?.models?.providers?.alibaba;
const requestBaseUrl = resolveAlibabaVideoBaseUrl(req);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
@@ -67,6 +71,7 @@ export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
provider: "alibaba",
capability: "video",
transport: "http",
+ request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
});
const model = req.model?.trim() || DEFAULT_ALIBABA_VIDEO_MODEL;
diff --git a/extensions/qwen/video-generation-provider.test.ts b/extensions/qwen/video-generation-provider.test.ts
index 3f39d49b21b8..9c359709b87a 100644
--- a/extensions/qwen/video-generation-provider.test.ts
+++ b/extensions/qwen/video-generation-provider.test.ts
@@ -11,7 +11,13 @@ import {
} from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it } from "vitest";
-const { postJsonRequestMock, fetchWithTimeoutMock } = getProviderHttpMocks();
+const {
+ postJsonRequestMock,
+ fetchWithTimeoutMock,
+ fetchWithTimeoutGuardedMock,
+ resolveProviderHttpRequestConfigMock,
+ sanitizeConfiguredModelProviderRequestMock,
+} = getProviderHttpMocks();
let buildQwenVideoGenerationProvider: typeof import("./video-generation-provider.js").buildQwenVideoGenerationProvider;
@@ -104,6 +110,91 @@ describe("qwen video generation provider", () => {
expectSuccessfulDashscopeVideoResult(result);
});
+ it("applies configured request policy to DashScope video requests", async () => {
+ const requestPolicy = {
+ allowPrivateNetwork: true,
+ headers: { "X-DashScope-Route": "qwen-policy" },
+ };
+ const dispatcherPolicy = { mode: "env-proxy" as const };
+ resolveProviderHttpRequestConfigMock.mockImplementationOnce((params) => {
+ const headers = new Headers(params.defaultHeaders);
+ for (const [key, value] of Object.entries(params.request?.headers ?? {})) {
+ headers.set(key, value);
+ }
+ return {
+ baseUrl: params.baseUrl ?? params.defaultBaseUrl,
+ allowPrivateNetwork: params.request?.allowPrivateNetwork === true,
+ headers,
+ dispatcherPolicy,
+ };
+ });
+ mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
+
+ const provider = buildQwenVideoGenerationProvider();
+ await provider.generateVideo({
+ provider: "qwen",
+ model: "wan2.6-t2v",
+ prompt: "animate this shot",
+ cfg: {
+ models: {
+ providers: {
+ qwen: {
+ baseUrl: "https://dashscope-intl.aliyuncs.com",
+ models: [],
+ request: requestPolicy,
+ },
+ },
+ },
+ },
+ });
+
+ expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith(requestPolicy);
+ expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ provider: "qwen",
+ capability: "video",
+ transport: "http",
+ request: requestPolicy,
+ }),
+ );
+ const request = postJsonRequestMock.mock.calls[0]?.[0] as
+ | {
+ allowPrivateNetwork?: unknown;
+ dispatcherPolicy?: unknown;
+ headers?: Headers;
+ }
+ | undefined;
+ expect(request?.allowPrivateNetwork).toBe(true);
+ expect(request?.dispatcherPolicy).toBe(dispatcherPolicy);
+ expect(request?.headers).toBeInstanceOf(Headers);
+ expect(request?.headers?.get("x-dashscope-route")).toBe("qwen-policy");
+ expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
+ 1,
+ "https://dashscope-intl.aliyuncs.com/api/v1/tasks/task-1",
+ expect.objectContaining({
+ method: "GET",
+ headers: expect.any(Headers),
+ }),
+ 120_000,
+ fetch,
+ {
+ ssrfPolicy: { allowPrivateNetwork: true },
+ dispatcherPolicy,
+ },
+ );
+ expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
+ 2,
+ "https://example.com/out.mp4",
+ { method: "GET" },
+ 120_000,
+ fetch,
+ {
+ ssrfPolicy: { allowPrivateNetwork: true },
+ dispatcherPolicy,
+ },
+ );
+ });
+
it("rejects DashScope video downloads that exceed the configured media cap", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
diff --git a/extensions/qwen/video-generation-provider.ts b/extensions/qwen/video-generation-provider.ts
index c4a5aa368cd9..8341cfa95fe1 100644
--- a/extensions/qwen/video-generation-provider.ts
+++ b/extensions/qwen/video-generation-provider.ts
@@ -1,7 +1,10 @@
// Qwen provider module implements model/runtime integration.
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
-import { resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
+import {
+ resolveProviderHttpRequestConfig,
+ sanitizeConfiguredModelProviderRequest,
+} from "openclaw/plugin-sdk/provider-http";
import {
DASHSCOPE_WAN_VIDEO_CAPABILITIES,
DASHSCOPE_WAN_VIDEO_MODELS,
@@ -78,6 +81,7 @@ export function buildQwenVideoGenerationProvider(): VideoGenerationProvider {
throw new Error("Qwen API key missing");
}
+ const providerConfig = req.cfg?.models?.providers?.qwen;
const requestBaseUrl = resolveQwenVideoBaseUrl(req);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
@@ -91,6 +95,7 @@ export function buildQwenVideoGenerationProvider(): VideoGenerationProvider {
provider: "qwen",
capability: "video",
transport: "http",
+ request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
});
const model = req.model?.trim() || DEFAULT_QWEN_VIDEO_MODEL;
diff --git a/src/plugin-sdk/test-helpers/provider-http-mocks.ts b/src/plugin-sdk/test-helpers/provider-http-mocks.ts
index 8eec0244242c..438af63168ac 100644
--- a/src/plugin-sdk/test-helpers/provider-http-mocks.ts
+++ b/src/plugin-sdk/test-helpers/provider-http-mocks.ts
@@ -30,7 +30,7 @@ type ResolveProviderHttpRequestConfigResult = {
baseUrl: string;
allowPrivateNetwork: boolean;
headers: Headers;
- dispatcherPolicy: undefined;
+ dispatcherPolicy: ReturnType["dispatcherPolicy"];
};
type AnyMock = Mock<(...args: unknown[]) => unknown>;
diff --git a/src/video-generation/dashscope-compatible.ts b/src/video-generation/dashscope-compatible.ts
index 6f22f2ac3d11..469ea2bfa632 100644
--- a/src/video-generation/dashscope-compatible.ts
+++ b/src/video-generation/dashscope-compatible.ts
@@ -4,8 +4,8 @@ import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
createProviderOperationTimeoutResolver,
- fetchProviderDownloadResponse,
- fetchProviderOperationResponse,
+ executeProviderOperationWithRetry,
+ fetchWithTimeoutGuarded,
postJsonRequest,
readProviderJsonResponse,
resolveProviderOperationTimeoutMs,
@@ -180,6 +180,8 @@ export async function pollDashscopeVideoTaskUntilComplete(params: {
timeoutMs?: number;
fetchFn: typeof fetch;
baseUrl: string;
+ allowPrivateNetwork?: boolean;
+ dispatcherPolicy?: Parameters[0]["dispatcherPolicy"];
defaultTimeoutMs?: number;
}): Promise {
const defaultTimeoutMs = params.defaultTimeoutMs ?? DEFAULT_VIDEO_GENERATION_TIMEOUT_MS;
@@ -188,22 +190,44 @@ export async function pollDashscopeVideoTaskUntilComplete(params: {
label: `${params.providerLabel} video generation task ${params.taskId}`,
});
for (let attempt = 0; attempt < DEFAULT_VIDEO_GENERATION_MAX_POLL_ATTEMPTS; attempt += 1) {
- const response = await fetchProviderOperationResponse({
- stage: "poll",
- url: `${params.baseUrl}/api/v1/tasks/${params.taskId}`,
- init: {
- method: "GET",
- headers: params.headers,
- },
- timeoutMs: createProviderOperationTimeoutResolver({ deadline, defaultTimeoutMs }),
- fetchFn: params.fetchFn,
+ const pollResult = await executeProviderOperationWithRetry({
provider: params.providerLabel,
- requestFailedMessage: `${params.providerLabel} video-generation task poll failed`,
+ stage: "poll",
+ operation: async () => {
+ const result = await fetchWithTimeoutGuarded(
+ `${params.baseUrl}/api/v1/tasks/${params.taskId}`,
+ {
+ method: "GET",
+ headers: params.headers,
+ },
+ createProviderOperationTimeoutResolver({ deadline, defaultTimeoutMs })(),
+ params.fetchFn,
+ {
+ ...(params.allowPrivateNetwork ? { ssrfPolicy: { allowPrivateNetwork: true } } : {}),
+ ...(params.dispatcherPolicy ? { dispatcherPolicy: params.dispatcherPolicy } : {}),
+ },
+ );
+ try {
+ await assertOkOrThrowHttpError(
+ result.response,
+ `${params.providerLabel} video-generation task poll failed`,
+ );
+ return result;
+ } catch (error) {
+ await result.release();
+ throw error;
+ }
+ },
});
- const payload = await readProviderJsonResponse(
- response,
- `${params.providerLabel} video-generation task poll`,
- );
+ let payload: DashscopeVideoGenerationResponse;
+ try {
+ payload = await readProviderJsonResponse(
+ pollResult.response,
+ `${params.providerLabel} video-generation task poll`,
+ );
+ } finally {
+ await pollResult.release();
+ }
const status = payload.output?.task_status?.trim().toUpperCase();
if (status === "SUCCEEDED") {
return payload;
@@ -285,6 +309,8 @@ export async function runDashscopeVideoGenerationTask(params: {
timeoutMs: resolveProviderOperationTimeoutMs({ deadline, defaultTimeoutMs }),
fetchFn: params.fetchFn,
baseUrl: params.baseUrl,
+ allowPrivateNetwork: params.allowPrivateNetwork,
+ dispatcherPolicy: params.dispatcherPolicy,
defaultTimeoutMs,
});
const urls = extractDashscopeVideoUrls(completed);
@@ -298,6 +324,8 @@ export async function runDashscopeVideoGenerationTask(params: {
urls,
timeoutMs: createProviderOperationTimeoutResolver({ deadline, defaultTimeoutMs }),
fetchFn: params.fetchFn,
+ allowPrivateNetwork: params.allowPrivateNetwork,
+ dispatcherPolicy: params.dispatcherPolicy,
defaultTimeoutMs,
maxBytes: resolveGeneratedMediaMaxBytes(params.req.cfg, "video"),
});
@@ -322,26 +350,55 @@ export async function downloadDashscopeGeneratedVideos(params: {
urls: string[];
timeoutMs?: ProviderOperationTimeoutMs;
fetchFn: typeof fetch;
+ allowPrivateNetwork?: boolean;
+ dispatcherPolicy?: Parameters[0]["dispatcherPolicy"];
defaultTimeoutMs?: number;
maxBytes: number;
}): Promise {
const videos: GeneratedVideoAsset[] = [];
for (const [index, url] of params.urls.entries()) {
- const response = await fetchProviderDownloadResponse({
- url,
- init: { method: "GET" },
- timeoutMs: params.timeoutMs ?? params.defaultTimeoutMs ?? DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
- fetchFn: params.fetchFn,
+ const result = await executeProviderOperationWithRetry({
provider: params.providerLabel,
- requestFailedMessage: `${params.providerLabel} generated video download failed`,
- });
- const buffer = await readResponseWithLimit(response, params.maxBytes, {
- onOverflow: ({ maxBytes }) =>
- new Error(`${params.providerLabel} generated video download exceeds ${maxBytes} bytes`),
+ stage: "download",
+ operation: async () => {
+ const guarded = await fetchWithTimeoutGuarded(
+ url,
+ { method: "GET" },
+ typeof params.timeoutMs === "function"
+ ? params.timeoutMs()
+ : (params.timeoutMs ?? params.defaultTimeoutMs ?? DEFAULT_VIDEO_GENERATION_TIMEOUT_MS),
+ params.fetchFn,
+ {
+ ...(params.allowPrivateNetwork ? { ssrfPolicy: { allowPrivateNetwork: true } } : {}),
+ ...(params.dispatcherPolicy ? { dispatcherPolicy: params.dispatcherPolicy } : {}),
+ },
+ );
+ try {
+ await assertOkOrThrowHttpError(
+ guarded.response,
+ `${params.providerLabel} generated video download failed`,
+ );
+ return guarded;
+ } catch (error) {
+ await guarded.release();
+ throw error;
+ }
+ },
});
+ let buffer: Buffer;
+ let mimeType: string;
+ try {
+ buffer = await readResponseWithLimit(result.response, params.maxBytes, {
+ onOverflow: ({ maxBytes }) =>
+ new Error(`${params.providerLabel} generated video download exceeds ${maxBytes} bytes`),
+ });
+ mimeType = result.response.headers.get("content-type")?.trim() || "video/mp4";
+ } finally {
+ await result.release();
+ }
videos.push({
buffer,
- mimeType: response.headers.get("content-type")?.trim() || "video/mp4",
+ mimeType,
fileName: `video-${index + 1}.mp4`,
metadata: { sourceUrl: url },
});