diff --git a/packages/pi-ai/src/env-api-keys.ts b/packages/pi-ai/src/env-api-keys.ts index 1036c4b28..48edd5575 100644 --- a/packages/pi-ai/src/env-api-keys.ts +++ b/packages/pi-ai/src/env-api-keys.ts @@ -137,6 +137,7 @@ export function getEnvApiKey(provider: any): string | undefined { "opencode-go": "OPENCODE_API_KEY", "kimi-coding": "KIMI_API_KEY", "alibaba-coding-plan": "ALIBABA_API_KEY", + "alibaba-dashscope": "DASHSCOPE_API_KEY", ollama: "OLLAMA_API_KEY", "ollama-cloud": "OLLAMA_API_KEY", "custom-openai": "CUSTOM_OPENAI_API_KEY", diff --git a/packages/pi-ai/src/models.custom.ts b/packages/pi-ai/src/models.custom.ts index c3cc5ac04..37bccc97a 100644 --- a/packages/pi-ai/src/models.custom.ts +++ b/packages/pi-ai/src/models.custom.ts @@ -170,6 +170,104 @@ export const CUSTOM_MODELS = { } satisfies Model<"openai-completions">, }, + // ─── Alibaba DashScope ─────────────────────────────────────────────── + // Regular DashScope API for users without the Coding Plan. + // Uses the international OpenAI-compatible endpoint. + // Requires DASHSCOPE_API_KEY from: dashscope.console.aliyun.com + // Pricing: https://www.alibabacloud.com/help/en/model-studio/model-pricing + "alibaba-dashscope": { + "qwen3-max": { + id: "qwen3-max", + name: "Qwen3 Max", + api: "openai-completions", + provider: "alibaba-dashscope", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32768, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3.5-plus": { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + api: "openai-completions", + provider: "alibaba-dashscope", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 1.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3.5-flash": { + id: "qwen3.5-flash", + name: "Qwen3.5 Flash", + api: "openai-completions", + provider: "alibaba-dashscope", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.1, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 32768, + compat: { supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3-coder-plus": { + id: "qwen3-coder-plus", + name: "Qwen3 Coder Plus", + api: "openai-completions", + provider: "alibaba-dashscope", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + reasoning: false, + input: ["text"], + cost: { + input: 1.0, + output: 5.0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + compat: { supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + "qwen3.6-plus": { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + api: "openai-completions", + provider: "alibaba-dashscope", + baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 3.0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 65536, + compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, + } satisfies Model<"openai-completions">, + }, + // ─── Z.AI (GLM-5.1) ──────────────────────────────────────────────── // GLM-5.1 is the latest GLM model from Zhipu AI, not yet in models.dev. // Uses the Z.AI Coding Plan endpoint (OpenAI-compatible). diff --git a/packages/pi-ai/src/models.test.ts b/packages/pi-ai/src/models.test.ts index 068004ad3..d8a3a20af 100644 --- a/packages/pi-ai/src/models.test.ts +++ b/packages/pi-ai/src/models.test.ts @@ -109,6 +109,141 @@ describe("model registry — custom zai provider (GLM-5.1)", () => { }); }); +// ═══════════════════════════════════════════════════════════════════════════ +// New provider: alibaba-dashscope (feat: #3891) +// +// Regular DashScope API for users without the Coding Plan. +// Separate from alibaba-coding-plan — different endpoint, auth, and pricing. +// ═══════════════════════════════════════════════════════════════════════════ + +describe("model registry — alibaba-dashscope provider", () => { + it("alibaba-dashscope is a registered provider", () => { + const providers = getProviders(); + assert.ok( + providers.includes("alibaba-dashscope"), + `Expected "alibaba-dashscope" in providers, got: ${providers.join(", ")}`, + ); + }); + + it("alibaba-dashscope has all expected models", () => { + const models = getModels("alibaba-dashscope"); + const ids = models.map((m) => m.id).sort(); + const expected = [ + "qwen3-coder-plus", + "qwen3-max", + "qwen3.5-flash", + "qwen3.5-plus", + "qwen3.6-plus", + ]; + assert.deepEqual(ids, expected); + }); + + it("alibaba-dashscope models use the international DashScope base URL", () => { + const models = getModels("alibaba-dashscope"); + for (const model of models) { + assert.equal( + model.baseUrl, + "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + `Model ${model.id} has wrong baseUrl: ${model.baseUrl}`, + ); + } + }); + + it("alibaba-dashscope models use openai-completions API", () => { + const models = getModels("alibaba-dashscope"); + for (const model of models) { + assert.equal(model.api, "openai-completions", `Model ${model.id} has wrong api: ${model.api}`); + } + }); + + it("alibaba-dashscope models have provider set correctly", () => { + const models = getModels("alibaba-dashscope"); + for (const model of models) { + assert.equal( + model.provider, + "alibaba-dashscope", + `Model ${model.id} has wrong provider: ${model.provider}`, + ); + } + }); + + it("alibaba-dashscope models all have 1M context window", () => { + const models = getModels("alibaba-dashscope"); + for (const model of models) { + assert.equal(model.contextWindow, 1_000_000, `Model ${model.id} has wrong contextWindow: ${model.contextWindow}`); + } + }); + + it("alibaba-dashscope models have positive paid costs (not free-tier)", () => { + const models = getModels("alibaba-dashscope"); + for (const model of models) { + assert.ok(model.cost.input > 0, `${model.id}: input cost should be > 0 (paid tier)`); + assert.ok(model.cost.output > 0, `${model.id}: output cost should be > 0 (paid tier)`); + } + }); + + it("qwen3-max is a reasoning model with correct pricing", () => { + const model = getModel("alibaba-dashscope" as any, "qwen3-max" as any); + assert.ok(model, "Expected getModel to return qwen3-max for alibaba-dashscope"); + assert.equal(model.reasoning, true); + assert.equal(model.cost.input, 1.2); + assert.equal(model.cost.output, 6); + assert.equal(model.maxTokens, 32768); + }); + + it("qwen3.5-plus is a reasoning model with correct pricing", () => { + const model = getModel("alibaba-dashscope" as any, "qwen3.5-plus" as any); + assert.ok(model, "Expected getModel to return qwen3.5-plus for alibaba-dashscope"); + assert.equal(model.reasoning, true); + assert.equal(model.cost.input, 0.4); + assert.equal(model.cost.output, 1.2); + assert.equal(model.maxTokens, 65536); + }); + + it("qwen3.5-flash is not a reasoning model", () => { + const model = getModel("alibaba-dashscope" as any, "qwen3.5-flash" as any); + assert.ok(model, "Expected getModel to return qwen3.5-flash for alibaba-dashscope"); + assert.equal(model.reasoning, false); + assert.equal(model.cost.input, 0.1); + assert.equal(model.cost.output, 0.4); + }); + + it("qwen3-coder-plus is not a reasoning model", () => { + const model = getModel("alibaba-dashscope" as any, "qwen3-coder-plus" as any); + assert.ok(model, "Expected getModel to return qwen3-coder-plus for alibaba-dashscope"); + assert.equal(model.reasoning, false); + assert.equal(model.cost.input, 1.0); + assert.equal(model.cost.output, 5.0); + }); + + it("qwen3.6-plus is a reasoning model", () => { + const model = getModel("alibaba-dashscope" as any, "qwen3.6-plus" as any); + assert.ok(model, "Expected getModel to return qwen3.6-plus for alibaba-dashscope"); + assert.equal(model.reasoning, true); + assert.equal(model.cost.input, 0.5); + assert.equal(model.cost.output, 3.0); + }); + + it("alibaba-dashscope is independent of alibaba-coding-plan (different endpoint)", () => { + const dashscope = getModels("alibaba-dashscope"); + const codingPlan = getModels("alibaba-coding-plan"); + for (const m of dashscope) { + assert.notEqual( + m.baseUrl, + "https://coding-intl.dashscope.aliyuncs.com/v1", + `${m.id} must not use the Coding Plan endpoint`, + ); + } + // Both providers must coexist — coding-plan must not have been overwritten + assert.ok(codingPlan.length > 0, "alibaba-coding-plan must still have models"); + }); + + it("getModel returns undefined for unknown model in alibaba-dashscope (failure path)", () => { + const model = getModel("alibaba-dashscope" as any, "does-not-exist" as any); + assert.equal(model, undefined); + }); +}); + describe("model registry — custom models do not collide with generated models", () => { it("generated providers still exist alongside custom providers", () => { const providers = getProviders(); diff --git a/packages/pi-ai/src/types.ts b/packages/pi-ai/src/types.ts index 661b58b57..9ec6ad85f 100644 --- a/packages/pi-ai/src/types.ts +++ b/packages/pi-ai/src/types.ts @@ -44,6 +44,7 @@ export type KnownProvider = | "opencode-go" | "kimi-coding" | "alibaba-coding-plan" + | "alibaba-dashscope" | "ollama" | "ollama-cloud"; export type Provider = KnownProvider | string; diff --git a/packages/pi-coding-agent/src/core/model-resolver.ts b/packages/pi-coding-agent/src/core/model-resolver.ts index 1a85c2bfa..39841303e 100644 --- a/packages/pi-coding-agent/src/core/model-resolver.ts +++ b/packages/pi-coding-agent/src/core/model-resolver.ts @@ -37,6 +37,7 @@ const defaultModelPerProvider: Record = { "opencode-go": "kimi-k2.5", "kimi-coding": "kimi-k2-thinking", "alibaba-coding-plan": "qwen3.5-plus", + "alibaba-dashscope": "qwen3.5-plus", ollama: "llama3.1:8b", "ollama-cloud": "qwen3:32b", }; diff --git a/src/resources/extensions/gsd/key-manager.ts b/src/resources/extensions/gsd/key-manager.ts index 17bd3cb31..a4699202b 100644 --- a/src/resources/extensions/gsd/key-manager.ts +++ b/src/resources/extensions/gsd/key-manager.ts @@ -49,6 +49,8 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [ { id: "custom-openai", label: "Custom (OpenAI-compat)", category: "llm", envVar: "CUSTOM_OPENAI_API_KEY" }, { id: "cerebras", label: "Cerebras", category: "llm", envVar: "CEREBRAS_API_KEY" }, { id: "azure-openai-responses", label: "Azure OpenAI", category: "llm", envVar: "AZURE_OPENAI_API_KEY" }, + { id: "alibaba-coding-plan", label: "Alibaba Coding Plan", category: "llm", envVar: "ALIBABA_API_KEY", dashboardUrl: "bailian.console.aliyun.com" }, + { id: "alibaba-dashscope", label: "Alibaba DashScope", category: "llm", envVar: "DASHSCOPE_API_KEY", dashboardUrl: "dashscope.console.aliyun.com" }, // Tool Keys { id: "context7", label: "Context7 Docs", category: "tool", envVar: "CONTEXT7_API_KEY", dashboardUrl: "context7.com/dashboard" }, diff --git a/src/resources/extensions/gsd/tests/key-manager.test.ts b/src/resources/extensions/gsd/tests/key-manager.test.ts index 785c34945..a7614b092 100644 --- a/src/resources/extensions/gsd/tests/key-manager.test.ts +++ b/src/resources/extensions/gsd/tests/key-manager.test.ts @@ -427,3 +427,66 @@ test("formatDoctorFindings shows findings with appropriate icons", () => { assert.ok(output.includes("1 warning")); assert.ok(output.includes("1 fixed")); }); + +// ─── Regression #3891 — alibaba-coding-plan missing from PROVIDER_REGISTRY ─────── +// +// Before this fix, `alibaba-coding-plan` was not in PROVIDER_REGISTRY, causing +// `/gsd keys add alibaba-coding-plan` to silently fail (provider not found). +// alibaba-dashscope is the new standalone provider added in the same PR. + +test("regression #3891 — alibaba-coding-plan is in PROVIDER_REGISTRY", () => { + const provider = findProvider("alibaba-coding-plan"); + assert.ok(provider, "alibaba-coding-plan must be in PROVIDER_REGISTRY for /gsd keys add to work"); + assert.equal(provider.id, "alibaba-coding-plan"); + assert.equal(provider.category, "llm"); + assert.equal(provider.envVar, "ALIBABA_API_KEY"); +}); + +test("alibaba-dashscope is in PROVIDER_REGISTRY", () => { + const provider = findProvider("alibaba-dashscope"); + assert.ok(provider, "alibaba-dashscope must be in PROVIDER_REGISTRY for /gsd keys add to work"); + assert.equal(provider.id, "alibaba-dashscope"); + assert.equal(provider.category, "llm"); + assert.equal(provider.envVar, "DASHSCOPE_API_KEY"); +}); + +test("alibaba-coding-plan and alibaba-dashscope are separate providers (different env vars)", () => { + const codingPlan = findProvider("alibaba-coding-plan"); + const dashscope = findProvider("alibaba-dashscope"); + assert.ok(codingPlan, "alibaba-coding-plan must exist"); + assert.ok(dashscope, "alibaba-dashscope must exist"); + assert.notEqual( + codingPlan.envVar, + dashscope.envVar, + "alibaba-coding-plan and alibaba-dashscope must use different env vars", + ); +}); + +test("getAllKeyStatuses includes alibaba-coding-plan", () => { + const auth = makeAuth(); + const statuses = getAllKeyStatuses(auth); + const found = statuses.find((s) => s.provider.id === "alibaba-coding-plan"); + assert.ok(found, "getAllKeyStatuses must include alibaba-coding-plan"); +}); + +test("getAllKeyStatuses includes alibaba-dashscope", () => { + const auth = makeAuth(); + const statuses = getAllKeyStatuses(auth); + const found = statuses.find((s) => s.provider.id === "alibaba-dashscope"); + assert.ok(found, "getAllKeyStatuses must include alibaba-dashscope"); +}); + +test("getAllKeyStatuses detects DASHSCOPE_API_KEY for alibaba-dashscope (failure path: missing key shows not configured)", () => { + const saved = process.env.DASHSCOPE_API_KEY; + delete process.env.DASHSCOPE_API_KEY; + try { + const auth = makeAuth(); + const statuses = getAllKeyStatuses(auth); + const found = statuses.find((s) => s.provider.id === "alibaba-dashscope"); + assert.ok(found); + assert.equal(found.configured, false); + assert.equal(found.source, "none"); + } finally { + if (saved !== undefined) process.env.DASHSCOPE_API_KEY = saved; + } +});