Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

12
extensions/qwen/README.md Normal file
View File

@@ -0,0 +1,12 @@
# OpenClaw Qwen Cloud Provider
Official OpenClaw provider plugin for Qwen Cloud.
Install from OpenClaw:
```bash
openclaw plugins install @openclaw/qwen-provider
openclaw gateway restart
```
See <https://docs.openclaw.ai/providers/qwen> for setup and configuration.

35
extensions/qwen/api.ts Normal file
View File

@@ -0,0 +1,35 @@
// Qwen API module exposes the plugin public contract.
export {
applyQwenNativeStreamingUsageCompat,
buildQwenDefaultModelDefinition,
buildQwenModelDefinition,
buildQwenModelCatalogForBaseUrl,
isNativeQwenBaseUrl,
isQwen36PlusSupportedBaseUrl,
isQwenCodingPlanBaseUrl,
QWEN_36_PLUS_MODEL_ID,
QWEN_BASE_URL,
QWEN_CN_BASE_URL,
QWEN_DEFAULT_COST,
QWEN_DEFAULT_MODEL_ID,
QWEN_DEFAULT_MODEL_REF,
QWEN_GLOBAL_BASE_URL,
QWEN_STANDARD_CN_BASE_URL,
QWEN_STANDARD_GLOBAL_BASE_URL,
QWEN_MODEL_CATALOG,
applyModelStudioNativeStreamingUsageCompat,
buildModelStudioDefaultModelDefinition,
buildModelStudioModelDefinition,
isNativeModelStudioBaseUrl,
MODELSTUDIO_BASE_URL,
MODELSTUDIO_CN_BASE_URL,
MODELSTUDIO_DEFAULT_COST,
MODELSTUDIO_DEFAULT_MODEL_ID,
MODELSTUDIO_DEFAULT_MODEL_REF,
MODELSTUDIO_GLOBAL_BASE_URL,
MODELSTUDIO_STANDARD_CN_BASE_URL,
MODELSTUDIO_STANDARD_GLOBAL_BASE_URL,
MODELSTUDIO_MODEL_CATALOG,
} from "./models.js";
export { buildModelStudioProvider, buildQwenProvider } from "./provider-catalog.js";
export { createQwenThinkingWrapper, wrapQwenProviderStream } from "./stream.js";

View File

@@ -0,0 +1,148 @@
// Qwen tests cover index plugin behavior.
import {
registerProviderPlugin,
requireRegisteredProvider,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import type { ProviderCatalogResult } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { describe, expect, it } from "vitest";
import { QWEN_36_PLUS_MODEL_ID, QWEN_BASE_URL } from "./api.js";
import qwenPlugin from "./index.js";
import { wrapQwenProviderStream } from "./stream.js";
function requireCatalogProvider(result: ProviderCatalogResult): ModelProviderConfig {
if (!result || !("provider" in result)) {
throw new Error("single provider catalog result missing");
}
return result.provider;
}
async function registerQwenProvider() {
const { providers } = await registerProviderPlugin({
plugin: qwenPlugin,
id: "qwen",
name: "Qwen Provider",
});
return requireRegisteredProvider(providers, "qwen");
}
describe("qwen provider plugin", () => {
it("keeps qwen3.6-plus out of Coding Plan normalized catalogs", async () => {
const provider = await registerQwenProvider();
const normalized = provider.normalizeConfig?.({
provider: "qwen",
providerConfig: {
baseUrl: QWEN_BASE_URL,
models: [{ id: "qwen3.5-plus" }, { id: QWEN_36_PLUS_MODEL_ID }],
},
} as never);
expect(normalized?.models?.map((model) => model.id)).toEqual(["qwen3.5-plus"]);
});
it("does not expose runtime model suppression hooks", async () => {
const provider = await registerQwenProvider();
expect(provider.suppressBuiltInModel).toBeUndefined();
});
it("registers qwen-oauth as a portal provider", async () => {
const { providers } = await registerProviderPlugin({
plugin: qwenPlugin,
id: "qwen",
name: "Qwen Provider",
});
const provider = requireRegisteredProvider(providers, "qwen-oauth");
expect(provider.aliases).toEqual(["qwen-portal", "qwen-cli"]);
expect(provider.envVars).toEqual(["QWEN_API_KEY"]);
expect(provider.auth?.map((method) => method.id)).toEqual(["api-key"]);
const result = await provider.staticCatalog?.run({
config: {},
env: {},
resolveProviderApiKey: () => ({}),
} as never);
const catalogProvider = requireCatalogProvider(result);
expect(catalogProvider.baseUrl).toBe("https://portal.qwen.ai/v1");
expect(catalogProvider.models?.map((model) => model.id)).toContain("qwen3.5-plus");
});
it("reuses legacy qwen portal auth profiles for qwen-oauth catalog", async () => {
const { providers } = await registerProviderPlugin({
plugin: qwenPlugin,
id: "qwen",
name: "Qwen Provider",
});
const provider = requireRegisteredProvider(providers, "qwen-oauth");
const result = await provider.catalog?.run({
config: {},
env: {},
resolveProviderApiKey: (providerId: string) =>
providerId === "qwen-portal" ? { apiKey: "portal-token" } : {},
} as never);
const catalogProvider = requireCatalogProvider(result);
expect(catalogProvider.apiKey).toBe("portal-token");
expect(catalogProvider.baseUrl).toBe("https://portal.qwen.ai/v1");
});
it.each([["qwen-oauth"], ["qwen-portal"], ["qwen-cli"]])(
"patches %s message payloads for portal compatibility",
async (providerId) => {
let patchedPayload: Record<string, unknown> | undefined;
const streamFn = wrapQwenProviderStream({
provider: providerId,
thinkingLevel: "off",
streamFn: ((
_model: unknown,
_context: unknown,
options?: {
onPayload?: (payload: Record<string, unknown>, model: unknown) => void;
},
) => {
const payload = {
messages: [
{ role: "system", content: "system text" },
{ role: "user", content: ["hello", { type: "text", text: "world" }] },
],
};
options?.onPayload?.(payload, _model);
patchedPayload = payload;
return (async function* () {})();
}) as never,
} as never);
const stream = streamFn!(
{
provider: providerId,
api: "openai-completions",
id: "qwen3.5-plus",
} as never,
{} as never,
{},
) as AsyncIterable<unknown>;
for await (const event of stream) {
void event;
// Drain stream so the payload hook runs.
}
expect(patchedPayload?.vl_high_resolution_images).toBe(true);
expect(patchedPayload?.messages).toEqual([
{
role: "system",
content: [{ type: "text", text: "system text", cache_control: { type: "ephemeral" } }],
},
{
role: "user",
content: [
{ type: "text", text: "hello" },
{ type: "text", text: "world" },
],
},
]);
},
);
});

243
extensions/qwen/index.ts Normal file
View File

@@ -0,0 +1,243 @@
// Qwen plugin entrypoint registers its OpenClaw integration.
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyQwenNativeStreamingUsageCompat } from "./api.js";
import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js";
import {
isQwenCodingPlanBaseUrl,
QWEN_36_PLUS_MODEL_ID,
QWEN_BASE_URL,
QWEN_DEFAULT_MODEL_REF,
QWEN_OAUTH_DEFAULT_MODEL_REF,
QWEN_OAUTH_PROVIDER_ID,
} from "./models.js";
import {
applyQwenConfig,
applyQwenConfigCn,
applyQwenOAuthConfig,
applyQwenStandardConfig,
applyQwenStandardConfigCn,
} from "./onboard.js";
import { buildQwenOAuthProvider, buildQwenProvider } from "./provider-catalog.js";
import { wrapQwenProviderStream } from "./stream.js";
import { buildQwenVideoGenerationProvider } from "./video-generation-provider.js";
const PROVIDER_ID = "qwen";
const LEGACY_PROVIDER_ID = "modelstudio";
const QWEN_OAUTH_AUTH_PROVIDER_IDS = [QWEN_OAUTH_PROVIDER_ID, "qwen-portal", "qwen-cli"] as const;
function normalizeProviderId(value: string): string {
return value.trim().toLowerCase();
}
function resolveConfiguredQwenBaseUrl(
config: { models?: { providers?: Record<string, { baseUrl?: string } | undefined> } } | undefined,
): string | undefined {
const providers = config?.models?.providers;
if (!providers) {
return undefined;
}
for (const [providerId, provider] of Object.entries(providers)) {
const normalized = normalizeProviderId(providerId);
if (normalized !== PROVIDER_ID && normalized !== LEGACY_PROVIDER_ID) {
continue;
}
const baseUrl = provider?.baseUrl?.trim();
if (baseUrl) {
return baseUrl;
}
}
return undefined;
}
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Qwen Provider",
description: "Bundled Qwen Cloud provider plugin",
provider: {
label: "Qwen Cloud",
docsPath: "/providers/qwen",
aliases: ["modelstudio", "qwencloud"],
auth: [
{
methodId: "standard-api-key-cn",
label: "Standard API Key for China (pay-as-you-go)",
hint: "Endpoint: dashscope.aliyuncs.com",
optionKey: "modelstudioStandardApiKeyCn",
flagName: "--modelstudio-standard-api-key-cn",
envVar: "QWEN_API_KEY",
promptMessage: "Enter Qwen Cloud API key (China standard endpoint)",
defaultModel: QWEN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQwenStandardConfigCn(cfg),
noteMessage: [
"Manage API keys: https://home.qwencloud.com/api-keys",
"Docs: https://docs.qwencloud.com/",
"Endpoint: dashscope.aliyuncs.com/compatible-mode/v1",
"Models: qwen3.6-plus, qwen3.5-plus, qwen3-coder-plus, etc.",
].join("\n"),
noteTitle: "Qwen Cloud Standard (China)",
wizard: {
choiceHint: "Endpoint: dashscope.aliyuncs.com",
groupLabel: "Qwen Cloud",
groupHint: "Standard / Coding Plan (CN / Global) + multimodal roadmap",
},
},
{
methodId: "standard-api-key",
label: "Standard API Key for Global/Intl (pay-as-you-go)",
hint: "Endpoint: dashscope-intl.aliyuncs.com",
optionKey: "modelstudioStandardApiKey",
flagName: "--modelstudio-standard-api-key",
envVar: "QWEN_API_KEY",
promptMessage: "Enter Qwen Cloud API key (Global/Intl standard endpoint)",
defaultModel: QWEN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQwenStandardConfig(cfg),
noteMessage: [
"Manage API keys: https://home.qwencloud.com/api-keys",
"Docs: https://docs.qwencloud.com/",
"Endpoint: dashscope-intl.aliyuncs.com/compatible-mode/v1",
"Models: qwen3.6-plus, qwen3.5-plus, qwen3-coder-plus, etc.",
].join("\n"),
noteTitle: "Qwen Cloud Standard (Global/Intl)",
wizard: {
choiceHint: "Endpoint: dashscope-intl.aliyuncs.com",
groupLabel: "Qwen Cloud",
groupHint: "Standard / Coding Plan (CN / Global) + multimodal roadmap",
},
},
{
methodId: "api-key-cn",
label: "Coding Plan API Key for China (subscription)",
hint: "Endpoint: coding.dashscope.aliyuncs.com",
optionKey: "modelstudioApiKeyCn",
flagName: "--modelstudio-api-key-cn",
envVar: "QWEN_API_KEY",
promptMessage: "Enter Qwen Cloud Coding Plan API key (China)",
defaultModel: QWEN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQwenConfigCn(cfg),
noteMessage: [
"Manage API keys: https://home.qwencloud.com/api-keys",
"Docs: https://docs.qwencloud.com/",
"Endpoint: coding.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Qwen Cloud Coding Plan (China)",
wizard: {
choiceHint: "Endpoint: coding.dashscope.aliyuncs.com",
groupLabel: "Qwen Cloud",
groupHint: "Standard / Coding Plan (CN / Global) + multimodal roadmap",
},
},
{
methodId: "api-key",
label: "Coding Plan API Key for Global/Intl (subscription)",
hint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
optionKey: "modelstudioApiKey",
flagName: "--modelstudio-api-key",
envVar: "QWEN_API_KEY",
promptMessage: "Enter Qwen Cloud Coding Plan API key (Global/Intl)",
defaultModel: QWEN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQwenConfig(cfg),
noteMessage: [
"Manage API keys: https://home.qwencloud.com/api-keys",
"Docs: https://docs.qwencloud.com/",
"Endpoint: coding-intl.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Qwen Cloud Coding Plan (Global/Intl)",
wizard: {
choiceHint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
groupLabel: "Qwen Cloud",
groupHint: "Standard / Coding Plan (CN / Global) + multimodal roadmap",
},
},
],
catalog: {
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
const baseUrl = resolveConfiguredQwenBaseUrl(ctx.config) ?? QWEN_BASE_URL;
return {
provider: {
...buildQwenProvider({ baseUrl }),
apiKey,
},
};
},
},
applyNativeStreamingUsageCompat: ({ providerConfig }) =>
applyQwenNativeStreamingUsageCompat(providerConfig),
wrapStreamFn: wrapQwenProviderStream,
normalizeConfig: ({ providerConfig }) => {
if (!isQwenCodingPlanBaseUrl(providerConfig.baseUrl)) {
return undefined;
}
const models = providerConfig.models?.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID);
return models && models.length !== providerConfig.models?.length
? { ...providerConfig, models }
: undefined;
},
},
register(api) {
api.registerProvider({
id: QWEN_OAUTH_PROVIDER_ID,
label: "Qwen OAuth",
docsPath: "/providers/qwen",
aliases: ["qwen-portal", "qwen-cli"],
envVars: ["QWEN_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: QWEN_OAUTH_PROVIDER_ID,
methodId: "api-key",
label: "Qwen OAuth token",
hint: "Portal token for portal.qwen.ai",
optionKey: "qwenOauthToken",
flagName: "--qwen-oauth-token",
envVar: "QWEN_API_KEY",
promptMessage: "Enter Qwen OAuth token",
defaultModel: QWEN_OAUTH_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQwenOAuthConfig(cfg),
wizard: {
choiceId: QWEN_OAUTH_PROVIDER_ID,
choiceLabel: "Qwen OAuth",
choiceHint: "Portal token for portal.qwen.ai",
groupId: "qwen",
groupLabel: "Qwen Cloud",
groupHint: "Standard / Coding Plan / OAuth",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = QWEN_OAUTH_AUTH_PROVIDER_IDS.map(
(providerId) => ctx.resolveProviderApiKey(providerId).apiKey,
).find(
(candidate): candidate is string =>
typeof candidate === "string" && candidate.length > 0,
);
if (!apiKey) {
return null;
}
return {
provider: {
...buildQwenOAuthProvider(),
apiKey,
},
};
},
},
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildQwenOAuthProvider(),
}),
},
wrapStreamFn: wrapQwenProviderStream,
});
api.registerMediaUnderstandingProvider(buildQwenMediaUnderstandingProvider());
api.registerVideoGenerationProvider(buildQwenVideoGenerationProvider());
},
});

View File

@@ -0,0 +1,148 @@
// Qwen tests cover media understanding provider plugin behavior.
import {
createRequestCaptureJsonFetch,
installPinnedHostnameTestHooks,
} from "openclaw/plugin-sdk/test-env";
import { describe, expect, it } from "vitest";
import { describeQwenVideo } from "./media-understanding-provider.js";
installPinnedHostnameTestHooks();
function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
response: Response;
getReadCount: () => number;
wasCanceled: () => boolean;
} {
const chunk = new Uint8Array(params.chunkSize);
let readCount = 0;
let canceled = false;
return {
response: new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (readCount >= params.chunkCount) {
controller.close();
return;
}
readCount += 1;
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
getReadCount: () => readCount,
wasCanceled: () => canceled,
};
}
describe("describeQwenVideo", () => {
it("builds the expected OpenAI-compatible video payload", async () => {
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({
choices: [
{
message: {
content: [{ text: " first " }, { text: "second" }],
},
},
],
});
const result = await describeQwenVideo({
buffer: Buffer.from("video-bytes"),
fileName: "clip.mp4",
mime: "video/mp4",
apiKey: "test-key",
timeoutMs: 1500,
baseUrl: "https://example.com/v1",
model: "qwen-vl-max",
prompt: "summarize the clip",
headers: { "X-Other": "1" },
fetchFn,
});
const { url, init } = getRequest();
expect(result.model).toBe("qwen-vl-max");
expect(result.text).toBe("first\nsecond");
expect(url).toBe("https://example.com/v1/chat/completions");
if (!init) {
throw new Error("expected Qwen request init");
}
expect(init.method).toBe("POST");
expect(init.signal).toBeInstanceOf(AbortSignal);
const headers = new Headers(init.headers);
expect(headers.get("authorization")).toBe("Bearer test-key");
expect(headers.get("content-type")).toBe("application/json");
expect(headers.get("x-other")).toBe("1");
const bodyText =
typeof init.body === "string"
? init.body
: Buffer.isBuffer(init.body)
? init.body.toString("utf8")
: "";
expect(bodyText).not.toBe("");
const body = JSON.parse(bodyText);
expect(body.model).toBe("qwen-vl-max");
const content = body.messages?.[0]?.content;
if (!content) {
throw new Error("expected Qwen user content");
}
expect(content[0]?.text).toBe("summarize the clip");
const videoContent = content[1];
if (!videoContent) {
throw new Error("expected Qwen video content");
}
expect(videoContent.type).toBe("video_url");
if (!videoContent.video_url) {
throw new Error("expected Qwen video URL payload");
}
expect(videoContent.video_url.url).toBe(
`data:video/mp4;base64,${Buffer.from("video-bytes").toString("base64")}`,
);
});
it("bounds successful Qwen video JSON bodies instead of buffering the whole response", async () => {
const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 });
await expect(
describeQwenVideo({
buffer: Buffer.from("video-bytes"),
fileName: "clip.mp4",
mime: "video/mp4",
apiKey: "test-key",
timeoutMs: 1500,
baseUrl: "https://example.com/v1",
fetchFn: async () => streamed.response,
}),
).rejects.toThrow("Qwen video description failed: JSON response exceeds 16777216 bytes");
expect(streamed.getReadCount()).toBeLessThan(64);
expect(streamed.wasCanceled()).toBe(true);
});
it("reports malformed Qwen video JSON with a provider-owned error", async () => {
const response = new Response("not-json{", {
status: 200,
headers: { "Content-Type": "application/json" },
});
await expect(
describeQwenVideo({
buffer: Buffer.from("video-bytes"),
fileName: "clip.mp4",
mime: "video/mp4",
apiKey: "test-key",
timeoutMs: 1500,
baseUrl: "https://example.com/v1",
fetchFn: async () => response,
}),
).rejects.toThrow("Qwen video description failed: malformed JSON response");
});
});

View File

@@ -0,0 +1,97 @@
// Qwen provider module implements model/runtime integration.
import {
buildOpenAiCompatibleVideoRequestBody,
coerceOpenAiCompatibleVideoText,
describeImageWithModel,
describeImagesWithModel,
resolveMediaUnderstandingString,
type MediaUnderstandingProvider,
type OpenAiCompatibleVideoPayload,
type VideoDescriptionRequest,
type VideoDescriptionResult,
} from "openclaw/plugin-sdk/media-understanding";
import {
assertOkOrThrowHttpError,
postJsonRequest,
readProviderJsonResponse,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { QWEN_STANDARD_GLOBAL_BASE_URL } from "./models.js";
const DEFAULT_QWEN_VIDEO_MODEL = "qwen-vl-max-latest";
const DEFAULT_QWEN_VIDEO_PROMPT = "Describe the video in detail.";
export async function describeQwenVideo(
params: VideoDescriptionRequest,
): Promise<VideoDescriptionResult> {
const fetchFn = params.fetchFn ?? fetch;
const model = resolveMediaUnderstandingString(params.model, DEFAULT_QWEN_VIDEO_MODEL);
const mime = resolveMediaUnderstandingString(params.mime, "video/mp4");
const prompt = resolveMediaUnderstandingString(params.prompt, DEFAULT_QWEN_VIDEO_PROMPT);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: params.baseUrl,
defaultBaseUrl: QWEN_STANDARD_GLOBAL_BASE_URL,
headers: params.headers,
request: params.request,
defaultHeaders: {
"content-type": "application/json",
authorization: `Bearer ${params.apiKey}`,
},
provider: "qwen",
api: "openai-completions",
capability: "video",
transport: "media-understanding",
});
const { response: res, release } = await postJsonRequest({
url: `${baseUrl}/chat/completions`,
headers,
body: buildOpenAiCompatibleVideoRequestBody({
model,
prompt,
mime,
buffer: params.buffer,
}),
timeoutMs: params.timeoutMs,
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
});
try {
await assertOkOrThrowHttpError(res, "Qwen video description failed");
// Read the success body through the shared byte-bounded JSON reader (16 MiB cap +
// stream cancel on overflow) so a hostile or buggy endpoint cannot force the runtime
// to buffer an unbounded body. Malformed JSON keeps the
// `Qwen video description failed: malformed JSON response` wrapping.
const payload = await readProviderJsonResponse<OpenAiCompatibleVideoPayload>(
res,
"Qwen video description failed",
);
const text = coerceOpenAiCompatibleVideoText(payload);
if (!text) {
throw new Error("Qwen video description response missing content");
}
return { text, model };
} finally {
await release();
}
}
export function buildQwenMediaUnderstandingProvider(): MediaUnderstandingProvider {
return {
id: "qwen",
capabilities: ["image", "video"],
defaultModels: {
image: "qwen-vl-max-latest",
video: DEFAULT_QWEN_VIDEO_MODEL,
},
autoPriority: {
video: 15,
},
describeImage: describeImageWithModel,
describeImages: describeImagesWithModel,
describeVideo: describeQwenVideo,
};
}

210
extensions/qwen/models.ts Normal file
View File

@@ -0,0 +1,210 @@
// Qwen plugin module implements models behavior.
import {
applyProviderNativeStreamingUsageCompat,
supportsNativeStreamingUsageCompat,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import type {
ModelDefinitionConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
export const QWEN_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
export const QWEN_GLOBAL_BASE_URL = QWEN_BASE_URL;
export const QWEN_CN_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1";
export const QWEN_STANDARD_CN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
export const QWEN_STANDARD_GLOBAL_BASE_URL =
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
export const QWEN_OAUTH_PROVIDER_ID = "qwen-oauth";
export const QWEN_OAUTH_BASE_URL = "https://portal.qwen.ai/v1";
export const QWEN_DEFAULT_MODEL_ID = "qwen3.5-plus";
export const QWEN_36_PLUS_MODEL_ID = "qwen3.6-plus";
export const QWEN_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
export const QWEN_DEFAULT_MODEL_REF = `qwen/${QWEN_DEFAULT_MODEL_ID}`;
export const QWEN_OAUTH_DEFAULT_MODEL_REF = `qwen-oauth/${QWEN_DEFAULT_MODEL_ID}`;
export const QWEN_MODEL_CATALOG: ReadonlyArray<ModelDefinitionConfig> = [
{
id: "qwen3.5-plus",
name: "qwen3.5-plus",
reasoning: false,
input: ["text", "image"],
cost: QWEN_DEFAULT_COST,
contextWindow: 1_000_000,
maxTokens: 65_536,
},
{
id: QWEN_36_PLUS_MODEL_ID,
name: QWEN_36_PLUS_MODEL_ID,
reasoning: false,
input: ["text", "image"],
cost: QWEN_DEFAULT_COST,
contextWindow: 1_000_000,
maxTokens: 65_536,
},
{
id: "qwen3-max-2026-01-23",
name: "qwen3-max-2026-01-23",
reasoning: false,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 262_144,
maxTokens: 65_536,
},
{
id: "qwen3-coder-next",
name: "qwen3-coder-next",
reasoning: false,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 262_144,
maxTokens: 65_536,
},
{
id: "qwen3-coder-plus",
name: "qwen3-coder-plus",
reasoning: false,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 1_000_000,
maxTokens: 65_536,
},
{
id: "MiniMax-M2.5",
name: "MiniMax-M2.5",
reasoning: true,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 1_000_000,
maxTokens: 65_536,
},
{
id: "glm-5",
name: "glm-5",
reasoning: false,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 202_752,
maxTokens: 16_384,
},
{
id: "glm-4.7",
name: "glm-4.7",
reasoning: false,
input: ["text"],
cost: QWEN_DEFAULT_COST,
contextWindow: 202_752,
maxTokens: 16_384,
},
{
id: "kimi-k2.5",
name: "kimi-k2.5",
reasoning: false,
input: ["text", "image"],
cost: QWEN_DEFAULT_COST,
contextWindow: 262_144,
maxTokens: 32_768,
},
];
export function isQwenCodingPlanBaseUrl(baseUrl: string | undefined): boolean {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return false;
}
try {
const hostname = new URL(trimmed).hostname.toLowerCase().replace(/\.+$/, "");
return (
hostname === "coding.dashscope.aliyuncs.com" ||
hostname === "coding-intl.dashscope.aliyuncs.com"
);
} catch {
return false;
}
}
export function isQwen36PlusSupportedBaseUrl(baseUrl: string | undefined): boolean {
return !isQwenCodingPlanBaseUrl(baseUrl);
}
export function buildQwenModelCatalogForBaseUrl(
baseUrl: string | undefined,
): ReadonlyArray<ModelDefinitionConfig> {
return isQwen36PlusSupportedBaseUrl(baseUrl)
? QWEN_MODEL_CATALOG
: QWEN_MODEL_CATALOG.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID);
}
export function isNativeQwenBaseUrl(baseUrl: string | undefined): boolean {
return supportsNativeStreamingUsageCompat({
providerId: "qwen",
baseUrl,
});
}
export function applyQwenNativeStreamingUsageCompat(
provider: ModelProviderConfig,
): ModelProviderConfig {
return applyProviderNativeStreamingUsageCompat({
providerId: "qwen",
providerConfig: provider,
});
}
export function buildQwenModelDefinition(params: {
id: string;
name?: string;
reasoning?: boolean;
input?: string[];
cost?: ModelDefinitionConfig["cost"];
contextWindow?: number;
maxTokens?: number;
}): ModelDefinitionConfig {
const catalog = QWEN_MODEL_CATALOG.find((model) => model.id === params.id);
return {
id: params.id,
name: params.name ?? catalog?.name ?? params.id,
reasoning: params.reasoning ?? catalog?.reasoning ?? false,
input:
(params.input as ("text" | "image")[]) ?? (catalog?.input ? [...catalog.input] : ["text"]),
cost: params.cost ?? catalog?.cost ?? QWEN_DEFAULT_COST,
contextWindow: params.contextWindow ?? catalog?.contextWindow ?? 262_144,
maxTokens: params.maxTokens ?? catalog?.maxTokens ?? 65_536,
};
}
export function buildQwenDefaultModelDefinition(): ModelDefinitionConfig {
return buildQwenModelDefinition({ id: QWEN_DEFAULT_MODEL_ID });
}
export function buildQwenOAuthModelCatalog(): ReadonlyArray<ModelDefinitionConfig> {
return QWEN_MODEL_CATALOG.map((model) => ({ ...model, maxTokens: 65_536 }));
}
/** @deprecated Use QWEN_BASE_URL. */
export const MODELSTUDIO_BASE_URL = QWEN_BASE_URL;
/** @deprecated Use QWEN_GLOBAL_BASE_URL. */
export const MODELSTUDIO_GLOBAL_BASE_URL = QWEN_GLOBAL_BASE_URL;
/** @deprecated Use QWEN_CN_BASE_URL. */
export const MODELSTUDIO_CN_BASE_URL = QWEN_CN_BASE_URL;
/** @deprecated Use QWEN_STANDARD_CN_BASE_URL. */
export const MODELSTUDIO_STANDARD_CN_BASE_URL = QWEN_STANDARD_CN_BASE_URL;
/** @deprecated Use QWEN_STANDARD_GLOBAL_BASE_URL. */
export const MODELSTUDIO_STANDARD_GLOBAL_BASE_URL = QWEN_STANDARD_GLOBAL_BASE_URL;
/** @deprecated Use QWEN_DEFAULT_MODEL_ID. */
export const MODELSTUDIO_DEFAULT_MODEL_ID = QWEN_DEFAULT_MODEL_ID;
/** @deprecated Use QWEN_DEFAULT_COST. */
export const MODELSTUDIO_DEFAULT_COST = QWEN_DEFAULT_COST;
/** @deprecated Use qwen/${QWEN_DEFAULT_MODEL_ID}. */
export const MODELSTUDIO_DEFAULT_MODEL_REF = `modelstudio/${QWEN_DEFAULT_MODEL_ID}`;
/** @deprecated Use QWEN_MODEL_CATALOG. */
export const MODELSTUDIO_MODEL_CATALOG = QWEN_MODEL_CATALOG;
export const isNativeModelStudioBaseUrl = isNativeQwenBaseUrl;
export const applyModelStudioNativeStreamingUsageCompat = applyQwenNativeStreamingUsageCompat;
export const buildModelStudioModelDefinition = buildQwenModelDefinition;
export const buildModelStudioDefaultModelDefinition = buildQwenDefaultModelDefinition;

12
extensions/qwen/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/qwen-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/qwen-provider",
"version": "2026.6.11"
}
}
}

View File

@@ -0,0 +1,72 @@
// Qwen setup module handles plugin onboarding behavior.
import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
QWEN_CN_BASE_URL,
QWEN_DEFAULT_MODEL_REF,
QWEN_GLOBAL_BASE_URL,
QWEN_OAUTH_DEFAULT_MODEL_REF,
QWEN_OAUTH_PROVIDER_ID,
QWEN_STANDARD_CN_BASE_URL,
QWEN_STANDARD_GLOBAL_BASE_URL,
} from "./models.js";
import { buildQwenOAuthProvider, buildQwenProvider } from "./provider-catalog.js";
const qwenPresetAppliers = createModelCatalogPresetAppliers<[string]>({
primaryModelRef: QWEN_DEFAULT_MODEL_REF,
resolveParams: (_cfg: OpenClawConfig, baseUrl: string) => {
const provider = buildQwenProvider({ baseUrl });
return {
providerId: "qwen",
api: provider.api ?? "openai-completions",
baseUrl,
catalogModels: provider.models ?? [],
aliases: [
...(provider.models ?? []).flatMap((model) => [
`qwen/${model.id}`,
`modelstudio/${model.id}`,
]),
{ modelRef: QWEN_DEFAULT_MODEL_REF, alias: "Qwen" },
],
};
},
});
const qwenOAuthPresetAppliers = createModelCatalogPresetAppliers<[]>({
primaryModelRef: QWEN_OAUTH_DEFAULT_MODEL_REF,
resolveParams: () => {
const provider = buildQwenOAuthProvider();
return {
providerId: QWEN_OAUTH_PROVIDER_ID,
api: provider.api ?? "openai-completions",
baseUrl: provider.baseUrl,
catalogModels: provider.models ?? [],
aliases: [
...(provider.models ?? []).map((model) => `qwen-oauth/${model.id}`),
{ modelRef: QWEN_OAUTH_DEFAULT_MODEL_REF, alias: "Qwen OAuth" },
],
};
},
});
export function applyQwenConfig(cfg: OpenClawConfig): OpenClawConfig {
return qwenPresetAppliers.applyConfig(cfg, QWEN_GLOBAL_BASE_URL);
}
export function applyQwenConfigCn(cfg: OpenClawConfig): OpenClawConfig {
return qwenPresetAppliers.applyConfig(cfg, QWEN_CN_BASE_URL);
}
export function applyQwenStandardConfig(cfg: OpenClawConfig): OpenClawConfig {
return qwenPresetAppliers.applyConfig(cfg, QWEN_STANDARD_GLOBAL_BASE_URL);
}
export function applyQwenStandardConfigCn(cfg: OpenClawConfig): OpenClawConfig {
return qwenPresetAppliers.applyConfig(cfg, QWEN_STANDARD_CN_BASE_URL);
}
export function applyQwenOAuthConfig(cfg: OpenClawConfig): OpenClawConfig {
return qwenOAuthPresetAppliers.applyConfig(cfg);
}

View File

@@ -0,0 +1,326 @@
{
"id": "qwen",
"icon": "https://cdn.simpleicons.org/qwen",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["qwen", "qwencloud", "modelstudio", "dashscope", "qwen-oauth", "qwen-portal", "qwen-cli"],
"providerAuthAliases": {
"qwen-portal": "qwen-oauth",
"qwen-cli": "qwen-oauth"
},
"providerEndpoints": [
{
"endpointClass": "modelstudio-native",
"baseUrls": [
"https://coding-intl.dashscope.aliyuncs.com/v1",
"https://coding.dashscope.aliyuncs.com/v1",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
]
},
{
"endpointClass": "qwen-portal-native",
"baseUrls": ["https://portal.qwen.ai/v1"]
}
],
"providerRequest": {
"providers": {
"qwen": {
"family": "modelstudio"
},
"qwencloud": {
"family": "modelstudio"
},
"modelstudio": {
"family": "modelstudio"
},
"dashscope": {
"family": "modelstudio"
},
"qwen-oauth": {
"family": "qwen-portal"
},
"qwen-portal": {
"family": "qwen-portal"
},
"qwen-cli": {
"family": "qwen-portal"
}
}
},
"modelCatalog": {
"aliases": {
"qwen-portal": {
"provider": "qwen-oauth"
},
"qwen-cli": {
"provider": "qwen-oauth"
}
},
"suppressions": [
{
"provider": "qwen",
"model": "qwen3.6-plus",
"reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
"when": {
"baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"],
"providerConfigApiIn": ["qwen", "modelstudio"]
}
},
{
"provider": "modelstudio",
"model": "qwen3.6-plus",
"reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
"when": {
"baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"],
"providerConfigApiIn": ["qwen", "modelstudio"]
}
}
],
"providers": {
"qwen-oauth": {
"baseUrl": "https://portal.qwen.ai/v1",
"api": "openai-completions",
"models": [
{
"id": "qwen3.5-plus",
"name": "qwen3.5-plus",
"reasoning": false,
"input": ["text", "image"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 1000000,
"maxTokens": 65536
},
{
"id": "qwen3.6-plus",
"name": "qwen3.6-plus",
"reasoning": false,
"input": ["text", "image"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 1000000,
"maxTokens": 65536
},
{
"id": "qwen3-max-2026-01-23",
"name": "qwen3-max-2026-01-23",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 262144,
"maxTokens": 65536
},
{
"id": "qwen3-coder-next",
"name": "qwen3-coder-next",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 262144,
"maxTokens": 65536
},
{
"id": "qwen3-coder-plus",
"name": "qwen3-coder-plus",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 1000000,
"maxTokens": 65536
},
{
"id": "MiniMax-M2.5",
"name": "MiniMax-M2.5",
"reasoning": true,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 1000000,
"maxTokens": 65536
},
{
"id": "glm-5",
"name": "glm-5",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 202752,
"maxTokens": 65536
},
{
"id": "glm-4.7",
"name": "glm-4.7",
"reasoning": false,
"input": ["text"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 202752,
"maxTokens": 65536
},
{
"id": "kimi-k2.5",
"name": "kimi-k2.5",
"reasoning": false,
"input": ["text", "image"],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 262144,
"maxTokens": 65536
}
]
}
}
},
"contracts": {
"mediaUnderstandingProviders": ["qwen"],
"videoGenerationProviders": ["qwen"]
},
"mediaUnderstandingProviderMetadata": {
"qwen": {
"capabilities": ["image", "video"],
"defaultModels": {
"image": "qwen-vl-max-latest",
"video": "qwen-vl-max-latest"
},
"autoPriority": {
"video": 15
}
}
},
"setup": {
"providers": [
{
"id": "qwen",
"envVars": ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"]
},
{
"id": "qwen-oauth",
"envVars": ["QWEN_API_KEY"]
}
]
},
"providerAuthChoices": [
{
"provider": "qwen",
"method": "standard-api-key-cn",
"choiceId": "qwen-standard-api-key-cn",
"deprecatedChoiceIds": ["modelstudio-standard-api-key-cn"],
"choiceLabel": "Standard API Key for China (pay-as-you-go)",
"choiceHint": "Endpoint: dashscope.aliyuncs.com",
"groupId": "qwen",
"groupLabel": "Qwen Cloud",
"groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap",
"optionKey": "modelstudioStandardApiKeyCn",
"cliFlag": "--modelstudio-standard-api-key-cn",
"cliOption": "--modelstudio-standard-api-key-cn <key>",
"cliDescription": "Qwen Cloud standard API key (China)"
},
{
"provider": "qwen",
"method": "standard-api-key",
"choiceId": "qwen-standard-api-key",
"deprecatedChoiceIds": ["modelstudio-standard-api-key"],
"choiceLabel": "Standard API Key for Global/Intl (pay-as-you-go)",
"choiceHint": "Endpoint: dashscope-intl.aliyuncs.com",
"groupId": "qwen",
"groupLabel": "Qwen Cloud",
"groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap",
"optionKey": "modelstudioStandardApiKey",
"cliFlag": "--modelstudio-standard-api-key",
"cliOption": "--modelstudio-standard-api-key <key>",
"cliDescription": "Qwen Cloud standard API key (Global/Intl)"
},
{
"provider": "qwen",
"method": "api-key-cn",
"choiceId": "qwen-api-key-cn",
"deprecatedChoiceIds": ["modelstudio-api-key-cn"],
"choiceLabel": "Coding Plan API Key for China (subscription)",
"choiceHint": "Endpoint: coding.dashscope.aliyuncs.com",
"groupId": "qwen",
"groupLabel": "Qwen Cloud",
"groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap",
"optionKey": "modelstudioApiKeyCn",
"cliFlag": "--modelstudio-api-key-cn",
"cliOption": "--modelstudio-api-key-cn <key>",
"cliDescription": "Qwen Cloud Coding Plan API key (China)"
},
{
"provider": "qwen",
"method": "api-key",
"choiceId": "qwen-api-key",
"deprecatedChoiceIds": ["modelstudio-api-key"],
"choiceLabel": "Coding Plan API Key for Global/Intl (subscription)",
"choiceHint": "Endpoint: coding-intl.dashscope.aliyuncs.com",
"groupId": "qwen",
"groupLabel": "Qwen Cloud",
"groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap",
"optionKey": "modelstudioApiKey",
"cliFlag": "--modelstudio-api-key",
"cliOption": "--modelstudio-api-key <key>",
"cliDescription": "Qwen Cloud Coding Plan API key (Global/Intl)"
},
{
"provider": "qwen-oauth",
"method": "api-key",
"choiceId": "qwen-oauth",
"choiceLabel": "Qwen OAuth",
"choiceHint": "Portal token for portal.qwen.ai",
"groupId": "qwen",
"groupLabel": "Qwen Cloud",
"groupHint": "Standard / Coding Plan / OAuth",
"optionKey": "qwenOauthToken",
"cliFlag": "--qwen-oauth-token",
"cliOption": "--qwen-oauth-token <token>",
"cliDescription": "Qwen OAuth token"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "@openclaw/qwen-provider",
"version": "2026.6.11",
"description": "OpenClaw Qwen Cloud provider plugin.",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/qwen-provider",
"npmSpec": "@openclaw/qwen-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.8"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -0,0 +1,63 @@
// Qwen tests cover provider catalog plugin behavior.
import { describe, expect, it } from "vitest";
import {
applyQwenNativeStreamingUsageCompat,
buildQwenProvider,
QWEN_BASE_URL,
QWEN_STANDARD_GLOBAL_BASE_URL,
QWEN_DEFAULT_MODEL_ID,
} from "./api.js";
type QwenProvider = ReturnType<typeof buildQwenProvider>;
function getQwenModelIds(provider: QwenProvider): string[] {
return provider.models.map((model) => model.id);
}
describe("qwen provider catalog", () => {
it("builds the bundled Qwen provider defaults", () => {
const provider = buildQwenProvider();
expect(provider.baseUrl).toBe(QWEN_BASE_URL);
expect(provider.api).toBe("openai-completions");
const modelIds = getQwenModelIds(provider);
expect(modelIds.length).toBeGreaterThan(0);
expect(modelIds).toContain(QWEN_DEFAULT_MODEL_ID);
expect(modelIds).not.toContain("qwen3.6-plus");
});
it("only advertises qwen3.6-plus on Standard endpoints", () => {
const coding = buildQwenProvider({ baseUrl: QWEN_BASE_URL });
const codingTrailingDot = buildQwenProvider({
baseUrl: " https://coding-intl.dashscope.aliyuncs.com./v1 ",
});
const standard = buildQwenProvider({ baseUrl: QWEN_STANDARD_GLOBAL_BASE_URL });
expect(getQwenModelIds(coding)).not.toContain("qwen3.6-plus");
expect(getQwenModelIds(codingTrailingDot)).not.toContain("qwen3.6-plus");
expect(getQwenModelIds(standard)).toContain("qwen3.6-plus");
});
it("opts native Qwen baseUrls into streaming usage only inside the extension", () => {
const nativeProvider = applyQwenNativeStreamingUsageCompat(buildQwenProvider());
expect(nativeProvider.models.length).toBeGreaterThan(0);
expect(
nativeProvider.models.every((model) => {
if (!model.compat) {
throw new Error(`expected Qwen model ${model.id} compat`);
}
return model.compat.supportsUsageInStreaming === true;
}),
).toBe(true);
const customProvider = applyQwenNativeStreamingUsageCompat({
...buildQwenProvider(),
baseUrl: "https://proxy.example.com/v1",
});
expect(
customProvider.models.some(
(model) => model.compat && model.compat.supportsUsageInStreaming === true,
),
).toBe(false);
});
});

View File

@@ -0,0 +1,27 @@
// Qwen provider module implements model/runtime integration.
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildQwenModelCatalogForBaseUrl,
buildQwenOAuthModelCatalog,
QWEN_BASE_URL,
QWEN_OAUTH_BASE_URL,
} from "./models.js";
export function buildQwenProvider(params?: { baseUrl?: string }): ModelProviderConfig {
const baseUrl = params?.baseUrl ?? QWEN_BASE_URL;
return {
baseUrl,
api: "openai-completions",
models: buildQwenModelCatalogForBaseUrl(baseUrl).map((model) => Object.assign({}, model)),
};
}
export function buildQwenOAuthProvider(): ModelProviderConfig {
return {
baseUrl: QWEN_OAUTH_BASE_URL,
api: "openai-completions",
models: buildQwenOAuthModelCatalog().map((model) => Object.assign({}, model)),
};
}
export const buildModelStudioProvider = buildQwenProvider;

View File

@@ -0,0 +1,4 @@
// Qwen tests cover provider discovery.contract plugin behavior.
import { describeModelStudioProviderDiscoveryContract } from "openclaw/plugin-sdk/provider-test-contracts";
describeModelStudioProviderDiscoveryContract(() => import("./index.js"));

View File

@@ -0,0 +1,172 @@
// Qwen tests cover stream plugin behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import { describe, expect, it } from "vitest";
import { createQwenThinkingWrapper, wrapQwenProviderStream } from "./stream.js";
function capturePayload(params: {
thinkingLevel?: "off" | "low" | "medium" | "high" | "xhigh" | "max";
thinkingFormat?: string;
reasoning?: unknown;
initialPayload?: Record<string, unknown>;
model?: Partial<Model<"openai-completions">>;
}): Record<string, unknown> {
let captured: Record<string, unknown> = {};
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload = { ...params.initialPayload };
options?.onPayload?.(payload, _model);
captured = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = createQwenThinkingWrapper(
baseStreamFn,
params.thinkingLevel ?? "high",
params.thinkingFormat,
);
void wrapped(
{
api: "openai-completions",
provider: "qwen",
id: "qwen3.6-plus",
reasoning: true,
...params.model,
} as Model<"openai-completions">,
{ messages: [] } as Context,
params.reasoning === undefined ? {} : ({ reasoning: params.reasoning } as never),
);
return captured;
}
describe("createQwenThinkingWrapper", () => {
it("maps disabled thinking to Qwen top-level enable_thinking", () => {
const payload = capturePayload({
reasoning: "none",
initialPayload: {
reasoning_effort: "high",
reasoning: { effort: "high" },
reasoningEffort: "high",
},
});
expect(payload).toEqual({ enable_thinking: false });
});
it("maps enabled thinking to Qwen top-level enable_thinking", () => {
expect(capturePayload({ reasoning: "medium" })).toEqual({ enable_thinking: true });
});
it("falls back to the session thinking level", () => {
expect(capturePayload({ thinkingLevel: "off" })).toEqual({ enable_thinking: false });
expect(capturePayload({ thinkingLevel: "high" })).toEqual({ enable_thinking: true });
});
it("overrides qwen-chat-template thinking with the session level", () => {
expect(
capturePayload({
thinkingFormat: "qwen-chat-template",
thinkingLevel: "off",
initialPayload: {
chat_template_kwargs: { enable_thinking: true, preserve_thinking: true },
enable_thinking: true,
reasoning_effort: "high",
},
}),
).toEqual({
chat_template_kwargs: { enable_thinking: false, preserve_thinking: true },
});
});
it("uses the runtime model qwen-chat-template format when the wrapper context omits it", () => {
expect(
capturePayload({
thinkingLevel: "off",
model: { compat: { thinkingFormat: "qwen-chat-template" } },
initialPayload: {
chat_template_kwargs: { enable_thinking: true },
enable_thinking: true,
},
}),
).toEqual({
chat_template_kwargs: { enable_thinking: false, preserve_thinking: true },
});
});
it("skips non-reasoning and non-completions models", () => {
expect(capturePayload({ model: { reasoning: false } })).toStrictEqual({});
expect(capturePayload({ model: { api: "openai-responses" as never } })).toStrictEqual({});
});
});
describe("wrapQwenProviderStream", () => {
it("only registers for Qwen-family OpenAI-compatible providers", () => {
const streamFn = wrapQwenProviderStream({
provider: "qwencloud",
modelId: "qwen3.6-plus",
model: {
api: "openai-completions",
provider: "qwen",
id: "qwen3.6-plus",
reasoning: true,
} as Model<"openai-completions">,
streamFn: undefined,
} as never);
expect(streamFn).toBeTypeOf("function");
expect(
wrapQwenProviderStream({
provider: "openai",
modelId: "gpt-5.4",
model: {
api: "openai-completions",
provider: "openai",
id: "gpt-5.4",
} as Model<"openai-completions">,
streamFn: undefined,
} as never),
).toBeUndefined();
});
it("passes qwen-chat-template format to the Qwen wrapper", () => {
let captured: Record<string, unknown> = {};
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload = {
chat_template_kwargs: { enable_thinking: true },
enable_thinking: true,
};
options?.onPayload?.(payload, _model);
captured = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = wrapQwenProviderStream({
provider: "qwen",
modelId: "qwen3.6-plus",
model: {
api: "openai-completions",
provider: "qwen",
id: "qwen3.6-plus",
reasoning: true,
compat: { thinkingFormat: "qwen-chat-template" },
} as Model<"openai-completions">,
streamFn: baseStreamFn,
thinkingLevel: "off",
} as never);
void wrapped?.(
{
api: "openai-completions",
provider: "qwen",
id: "qwen3.6-plus",
reasoning: true,
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(captured).toStrictEqual({
chat_template_kwargs: { enable_thinking: false, preserve_thinking: true },
});
});
});

126
extensions/qwen/stream.ts Normal file
View File

@@ -0,0 +1,126 @@
// Qwen plugin module implements stream behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import {
createPayloadPatchStreamWrapper,
isOpenAICompatibleThinkingEnabled,
setQwenChatTemplateThinking,
} from "openclaw/plugin-sdk/provider-stream-shared";
type QwenThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"];
type QwenThinkingFormat = string | undefined;
function isQwenProviderId(providerId: string): boolean {
const normalized = normalizeProviderId(providerId);
return (
normalized === "qwen" ||
normalized === "qwen-oauth" ||
normalized === "qwen-portal" ||
normalized === "qwen-cli" ||
normalized === "modelstudio" ||
normalized === "qwencloud" ||
normalized === "dashscope"
);
}
function isQwenOAuthProviderId(providerId: string): boolean {
const normalized = normalizeProviderId(providerId);
return normalized === "qwen-oauth" || normalized === "qwen-portal" || normalized === "qwen-cli";
}
function normalizeQwenOAuthContent(content: unknown): unknown {
if (typeof content === "string") {
return [{ type: "text", text: content }];
}
if (!Array.isArray(content)) {
return content;
}
const normalized = content
.map((part) => {
if (typeof part === "string") {
return { type: "text", text: part };
}
return part && typeof part === "object" ? part : undefined;
})
.filter((part): part is Record<string, unknown> => Boolean(part));
return normalized.length > 0 ? normalized : content;
}
function patchQwenOAuthPayload(payload: Record<string, unknown>): void {
const messages = payload.messages;
if (!Array.isArray(messages)) {
return;
}
for (const message of messages) {
if (!message || typeof message !== "object") {
continue;
}
const record = message as Record<string, unknown>;
record.content = normalizeQwenOAuthContent(record.content);
if (record.role !== "system" || !Array.isArray(record.content) || record.content.length === 0) {
continue;
}
const last = record.content[record.content.length - 1];
if (last && typeof last === "object") {
(last as Record<string, unknown>).cache_control = { type: "ephemeral" };
}
}
payload.vl_high_resolution_images = true;
}
function readQwenThinkingFormatFromModel(model: Parameters<StreamFn>[0]): QwenThinkingFormat {
if (model.api !== "openai-completions") {
return undefined;
}
const compat =
model.compat && typeof model.compat === "object"
? (model.compat as { thinkingFormat?: unknown })
: undefined;
return typeof compat?.thinkingFormat === "string" ? compat.thinkingFormat : undefined;
}
export function createQwenThinkingWrapper(
baseStreamFn: StreamFn | undefined,
thinkingLevel: QwenThinkingLevel,
thinkingFormat?: QwenThinkingFormat,
): StreamFn {
return createPayloadPatchStreamWrapper(
baseStreamFn,
({ payload: payloadObj, model, options }) => {
const enableThinking = isOpenAICompatibleThinkingEnabled({ thinkingLevel, options });
const effectiveThinkingFormat = thinkingFormat ?? readQwenThinkingFormatFromModel(model);
if (effectiveThinkingFormat === "qwen-chat-template") {
setQwenChatTemplateThinking(payloadObj, enableThinking);
delete payloadObj.enable_thinking;
} else {
payloadObj.enable_thinking = enableThinking;
}
delete payloadObj.reasoning_effort;
delete payloadObj.reasoningEffort;
delete payloadObj.reasoning;
},
{
shouldPatch: ({ model }) => model.api === "openai-completions" && model.reasoning,
},
);
}
export function wrapQwenProviderStream(ctx: ProviderWrapStreamFnContext): StreamFn | undefined {
if (!isQwenProviderId(ctx.provider) || (ctx.model && ctx.model.api !== "openai-completions")) {
return undefined;
}
const streamFn = createQwenThinkingWrapper(
ctx.streamFn,
ctx.thinkingLevel,
ctx.model ? readQwenThinkingFormatFromModel(ctx.model) : undefined,
);
if (!isQwenOAuthProviderId(ctx.provider)) {
return streamFn;
}
return createPayloadPatchStreamWrapper(streamFn, ({ payload, model }) => {
if (model.api === "openai-completions") {
patchQwenOAuthPayload(payload);
}
});
}

View File

@@ -0,0 +1,3 @@
// Qwen API module exposes the plugin public contract.
export { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js";
export { buildQwenVideoGenerationProvider } from "./video-generation-provider.js";

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}

View File

@@ -0,0 +1,201 @@
// Qwen tests cover video generation provider plugin behavior.
import {
getProviderHttpMocks,
installProviderHttpMockCleanup,
} from "openclaw/plugin-sdk/provider-http-test-mocks";
import {
expectDashscopeVideoTaskPoll,
expectExplicitVideoGenerationCapabilities,
expectSuccessfulDashscopeVideoResult,
mockSuccessfulDashscopeVideoTask,
} from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it } from "vitest";
const { postJsonRequestMock, fetchWithTimeoutMock } = getProviderHttpMocks();
let buildQwenVideoGenerationProvider: typeof import("./video-generation-provider.js").buildQwenVideoGenerationProvider;
beforeAll(async () => {
({ buildQwenVideoGenerationProvider } = await import("./video-generation-provider.js"));
});
installProviderHttpMockCleanup();
function expectPostJsonRequest(
call: unknown,
expected: {
url: string;
body: Record<string, unknown>;
},
) {
if (!call || typeof call !== "object") {
throw new Error("expected postJsonRequest call object");
}
const request = call as {
url?: unknown;
headers?: unknown;
body?: unknown;
timeoutMs?: unknown;
fetchFn?: unknown;
allowPrivateNetwork?: unknown;
dispatcherPolicy?: unknown;
};
expect(request.url).toBe(expected.url);
expect(request.body).toEqual(expected.body);
expect(request.timeoutMs).toBe(120_000);
expect(request.fetchFn).toBe(globalThis.fetch);
expect(request.allowPrivateNetwork).toBe(false);
expect(request.dispatcherPolicy).toBeUndefined();
expect(request.headers).toBeInstanceOf(Headers);
expect(Array.from((request.headers as Headers).entries())).toEqual([
["authorization", "Bearer provider-key"],
["content-type", "application/json"],
["x-dashscope-async", "enable"],
]);
}
function streamedVideoResponse(bytes: string): Response {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(bytes));
controller.close();
},
}),
{ headers: { "content-type": "video/mp4" } },
);
}
describe("qwen video generation provider", () => {
it("declares explicit mode capabilities", () => {
expectExplicitVideoGenerationCapabilities(buildQwenVideoGenerationProvider());
});
it("submits async Wan generation, polls task status, and downloads the resulting video", async () => {
mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
const provider = buildQwenVideoGenerationProvider();
const result = await provider.generateVideo({
provider: "qwen",
model: "wan2.6-r2v-flash",
prompt: "animate this shot",
cfg: {},
inputImages: [{ url: "https://example.com/ref.png" }],
durationSeconds: 6,
audio: true,
});
expect(postJsonRequestMock).toHaveBeenCalledTimes(1);
expectPostJsonRequest(postJsonRequestMock.mock.calls[0]?.[0], {
url: "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
body: {
model: "wan2.6-r2v-flash",
input: {
prompt: "animate this shot",
img_url: "https://example.com/ref.png",
},
parameters: {
duration: 6,
enable_audio: true,
},
},
});
expectDashscopeVideoTaskPoll(fetchWithTimeoutMock);
expectSuccessfulDashscopeVideoResult(result);
});
it("rejects DashScope video downloads that exceed the configured media cap", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
json: async () => ({
request_id: "req-too-large",
output: { task_id: "task-too-large" },
}),
},
release: async () => {},
});
fetchWithTimeoutMock
.mockResolvedValueOnce({
json: async () => ({
output: {
task_status: "SUCCEEDED",
results: [{ video_url: "https://example.com/too-large.mp4" }],
},
}),
headers: new Headers(),
})
.mockResolvedValueOnce(streamedVideoResponse("too-large"));
const provider = buildQwenVideoGenerationProvider();
await expect(
provider.generateVideo({
provider: "qwen",
model: "wan2.6-r2v-flash",
prompt: "short video",
cfg: { agents: { defaults: { mediaMaxMb: 0.000001 } } },
}),
).rejects.toThrow("Qwen generated video download exceeds 1 bytes");
});
it("fails fast when reference inputs are local buffers instead of remote URLs", async () => {
const provider = buildQwenVideoGenerationProvider();
await expect(
provider.generateVideo({
provider: "qwen",
model: "wan2.6-i2v",
prompt: "animate this local frame",
cfg: {},
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
}),
).rejects.toThrow(
"Qwen video generation currently requires remote http(s) URLs for reference images/videos.",
);
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("preserves dedicated coding endpoints for dedicated API keys", async () => {
mockSuccessfulDashscopeVideoTask(
{
postJsonRequestMock,
fetchWithTimeoutMock,
},
{ requestId: "req-2", taskId: "task-2" },
);
const provider = buildQwenVideoGenerationProvider();
await provider.generateVideo({
provider: "qwen",
model: "wan2.6-t2v",
prompt: "animate this shot",
cfg: {
models: {
providers: {
qwen: {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
models: [],
},
},
},
},
});
expect(postJsonRequestMock).toHaveBeenCalledTimes(1);
expectPostJsonRequest(postJsonRequestMock.mock.calls[0]?.[0], {
url: "https://coding-intl.dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
body: {
model: "wan2.6-t2v",
input: {
prompt: "animate this shot",
},
parameters: {
duration: 5,
},
},
});
expectDashscopeVideoTaskPoll(fetchWithTimeoutMock, {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com",
taskId: "task-2",
});
});
});

View File

@@ -0,0 +1,112 @@
// 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 {
DASHSCOPE_WAN_VIDEO_CAPABILITIES,
DASHSCOPE_WAN_VIDEO_MODELS,
DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL,
DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
runDashscopeVideoGenerationTask,
} from "openclaw/plugin-sdk/video-generation";
import type {
VideoGenerationProvider,
VideoGenerationRequest,
VideoGenerationResult,
} from "openclaw/plugin-sdk/video-generation";
import { QWEN_STANDARD_CN_BASE_URL, QWEN_STANDARD_GLOBAL_BASE_URL } from "./models.js";
const DEFAULT_QWEN_VIDEO_BASE_URL = "https://dashscope-intl.aliyuncs.com";
const DEFAULT_QWEN_VIDEO_MODEL = DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL;
function resolveQwenVideoBaseUrl(req: VideoGenerationRequest): string {
const direct = req.cfg?.models?.providers?.qwen?.baseUrl?.trim();
if (!direct) {
return DEFAULT_QWEN_VIDEO_BASE_URL;
}
try {
return new URL(direct).toString();
} catch {
return DEFAULT_QWEN_VIDEO_BASE_URL;
}
}
function resolveDashscopeAigcApiBaseUrl(baseUrl: string): string {
try {
const url = new URL(baseUrl);
if (
url.hostname === "coding-intl.dashscope.aliyuncs.com" ||
url.hostname === "coding.dashscope.aliyuncs.com" ||
url.hostname === "dashscope-intl.aliyuncs.com" ||
url.hostname === "dashscope.aliyuncs.com"
) {
return url.origin;
}
} catch {
// Fall through to legacy prefix handling for non-URL strings.
}
if (baseUrl.startsWith(QWEN_STANDARD_CN_BASE_URL)) {
return "https://dashscope.aliyuncs.com";
}
if (baseUrl.startsWith(QWEN_STANDARD_GLOBAL_BASE_URL)) {
return DEFAULT_QWEN_VIDEO_BASE_URL;
}
return baseUrl.replace(/\/+$/u, "");
}
export function buildQwenVideoGenerationProvider(): VideoGenerationProvider {
return {
id: "qwen",
label: "Qwen Cloud",
defaultModel: DEFAULT_QWEN_VIDEO_MODEL,
models: [...DASHSCOPE_WAN_VIDEO_MODELS],
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
provider: "qwen",
agentDir,
}),
capabilities: DASHSCOPE_WAN_VIDEO_CAPABILITIES,
async generateVideo(req): Promise<VideoGenerationResult> {
const fetchFn = fetch;
const auth = await resolveApiKeyForProvider({
provider: "qwen",
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
});
if (!auth.apiKey) {
throw new Error("Qwen API key missing");
}
const requestBaseUrl = resolveQwenVideoBaseUrl(req);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: requestBaseUrl,
defaultBaseUrl: DEFAULT_QWEN_VIDEO_BASE_URL,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
"X-DashScope-Async": "enable",
},
provider: "qwen",
capability: "video",
transport: "http",
});
const model = req.model?.trim() || DEFAULT_QWEN_VIDEO_MODEL;
return await runDashscopeVideoGenerationTask({
providerLabel: "Qwen",
model,
req,
url: `${resolveDashscopeAigcApiBaseUrl(baseUrl)}/api/v1/services/aigc/video-generation/video-synthesis`,
headers,
baseUrl: resolveDashscopeAigcApiBaseUrl(baseUrl),
timeoutMs: req.timeoutMs,
fetchFn,
allowPrivateNetwork,
dispatcherPolicy,
defaultTimeoutMs: DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
});
},
};
}