feat(qwen): add qwen provider and video generation

This commit is contained in:
Peter Steinberger
2026-04-04 17:43:15 +01:00
parent 759373e887
commit e3ac0f43df
104 changed files with 2477 additions and 483 deletions

View File

@@ -14,8 +14,8 @@ describe("formatAuthDoctorHint", () => {
provider: "qwen-portal",
});
expect(hint).toContain("openclaw onboard --auth-choice modelstudio-api-key");
expect(hint).toContain("modelstudio-api-key-cn");
expect(hint).not.toContain("--provider modelstudio");
expect(hint).toContain("openclaw onboard --auth-choice qwen-api-key");
expect(hint).toContain("qwen-api-key-cn");
expect(hint).not.toContain("--provider qwen");
});
});

View File

@@ -13,7 +13,15 @@ import {
} from "./model-auth.js";
vi.mock("../plugins/provider-runtime.js", () => ({
buildProviderMissingAuthMessageWithPlugin: () => undefined,
buildProviderMissingAuthMessageWithPlugin: (params: {
provider: string;
context: { listProfileIds: (providerId: string) => string[] };
}) => {
if (params.provider === "openai" && params.context.listProfileIds("openai-codex").length > 0) {
return 'No API key found for provider "openai". Use openai-codex/gpt-5.4.';
}
return undefined;
},
formatProviderAuthProfileApiKeyWithPlugin: async () => undefined,
refreshProviderOAuthCredentialWithPlugin: async () => null,
resolveProviderSyntheticAuthWithPlugin: (params: {
@@ -311,13 +319,13 @@ describe("getApiKeyForModel", () => {
});
});
it("resolves Model Studio API key from env", async () => {
it("resolves Qwen API key from env", async () => {
await withEnvAsync(
{ [envVar("MODELSTUDIO", "API", "KEY")]: "modelstudio-test-key" },
async () => {
// pragma: allowlist secret
const resolved = await resolveApiKeyForProvider({
provider: "modelstudio",
provider: "qwen",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("modelstudio-test-key");

View File

@@ -157,10 +157,10 @@ describe("normalizeModelCompat", () => {
});
});
it("keeps supportsUsageInStreaming on for native ModelStudio endpoints", () => {
it("keeps supportsUsageInStreaming on for native Qwen endpoints", () => {
const model = {
...baseModel(),
provider: "modelstudio",
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
};
delete (model as { compat?: unknown }).compat;

View File

@@ -419,7 +419,7 @@ describe("model-selection", () => {
"qwen-dashscope": {
models: [{ id: "qwen-max" }],
},
modelstudio: {
qwen: {
models: [{ id: "qwen-max" }],
},
},

View File

@@ -834,13 +834,13 @@ describe("openai transport stream", () => {
expect(params.messages?.[0]?.content).toBe("Stable prefix\nDynamic suffix");
});
it("uses system role and streaming usage compat for native ModelStudio completions providers", () => {
it("uses system role and streaming usage compat for native Qwen completions providers", () => {
const params = buildOpenAICompletionsParams(
{
id: "qwen3.6-plus",
name: "Qwen 3.6 Plus",
api: "openai-completions",
provider: "modelstudio",
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
reasoning: true,
input: ["text"],

View File

@@ -561,7 +561,7 @@ describe("provider attribution", () => {
expect(
resolveProviderRequestCapabilities({
provider: "modelstudio",
provider: "qwen",
api: "openai-completions",
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
capability: "llm",
@@ -745,9 +745,9 @@ describe("provider attribution", () => {
},
},
{
name: "native ModelStudio completions",
name: "native Qwen completions",
input: {
provider: "modelstudio",
provider: "qwen",
api: "openai-completions",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
capability: "llm" as const,

View File

@@ -295,6 +295,8 @@ function resolveKnownProviderFamily(provider: string | undefined): string {
case "moonshot":
case "kimi":
return "moonshot";
case "qwen":
case "qwencloud":
case "modelstudio":
case "dashscope":
return "modelstudio";

View File

@@ -1,5 +1,8 @@
export function normalizeProviderId(provider: string): string {
const normalized = provider.trim().toLowerCase();
if (normalized === "modelstudio" || normalized === "qwencloud") {
return "qwen";
}
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}