mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
@@ -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: {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user