feat(pi-ai): add Alibaba DashScope as standalone provider (#3891)

* feat(pi-ai): add Alibaba DashScope as standalone provider

Adds `alibaba-dashscope` for users with a regular DashScope API key,
separate from the existing `alibaba-coding-plan` free-tier provider.

- types.ts: register `alibaba-dashscope` as KnownProvider
- env-api-keys.ts: map to DASHSCOPE_API_KEY
- models.custom.ts: add qwen3-max, qwen3.5-plus, qwen3.5-flash,
  qwen3-coder-plus with international endpoint and real pricing
- model-resolver.ts: default model qwen3.5-plus
- key-manager.ts: add alibaba-coding-plan and alibaba-dashscope
  to PROVIDER_REGISTRY so /gsd keys add works for both

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(pi-ai): add qwen3.6-plus to alibaba-dashscope provider

qwen3.6-plus is available on DashScope international endpoint.
Pricing: $0.5/M input, $3/M output (base tier, 0-256K tokens).
Supports thinking mode (reasoning: true).

Source: https://www.alibabacloud.com/help/en/model-studio/model-pricing

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test(pi-ai): add tests for alibaba-dashscope provider and key-manager regression

- packages/pi-ai/src/models.test.ts: add describe block covering all 5
  alibaba-dashscope models (presence, base URL, API, provider field,
  context window, paid pricing, per-model reasoning/cost assertions,
  independence from alibaba-coding-plan, failure path for unknown model)
- src/resources/extensions/gsd/tests/key-manager.test.ts: add regression
  tests for #3891 — alibaba-coding-plan was missing from PROVIDER_REGISTRY,
  causing /gsd keys add alibaba-coding-plan to fail silently; also covers
  alibaba-dashscope registration, env var separation, and getAllKeyStatuses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
NilsR0711 2026-04-13 14:04:39 +02:00 committed by GitHub
parent 00c6442e1a
commit ddff956a91
7 changed files with 301 additions and 0 deletions

View file

@ -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",

View file

@ -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).

View file

@ -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();

View file

@ -44,6 +44,7 @@ export type KnownProvider =
| "opencode-go"
| "kimi-coding"
| "alibaba-coding-plan"
| "alibaba-dashscope"
| "ollama"
| "ollama-cloud";
export type Provider = KnownProvider | string;

View file

@ -37,6 +37,7 @@ const defaultModelPerProvider: Record<KnownProvider, string> = {
"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",
};

View file

@ -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" },

View file

@ -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;
}
});