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
This commit is contained in:
Alix-007
2026-07-12 11:11:15 -04:00
committed by GitHub
parent eaedcdcd4e
commit bbac74685d
6 changed files with 275 additions and 32 deletions
@@ -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();
@@ -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;
@@ -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: {
+6 -1
View File
@@ -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;
@@ -30,7 +30,7 @@ type ResolveProviderHttpRequestConfigResult = {
baseUrl: string;
allowPrivateNetwork: boolean;
headers: Headers;
dispatcherPolicy: undefined;
dispatcherPolicy: ReturnType<typeof resolveProviderHttpRequestConfig>["dispatcherPolicy"];
};
type AnyMock = Mock<(...args: unknown[]) => unknown>;
+84 -27
View File
@@ -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<typeof postJsonRequest>[0]["dispatcherPolicy"];
defaultTimeoutMs?: number;
}): Promise<DashscopeVideoGenerationResponse> {
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<DashscopeVideoGenerationResponse>(
response,
`${params.providerLabel} video-generation task poll`,
);
let payload: DashscopeVideoGenerationResponse;
try {
payload = await readProviderJsonResponse<DashscopeVideoGenerationResponse>(
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<typeof postJsonRequest>[0]["dispatcherPolicy"];
defaultTimeoutMs?: number;
maxBytes: number;
}): Promise<GeneratedVideoAsset[]> {
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 },
});