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:
parent
00c6442e1a
commit
ddff956a91
7 changed files with 301 additions and 0 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export type KnownProvider =
|
|||
| "opencode-go"
|
||||
| "kimi-coding"
|
||||
| "alibaba-coding-plan"
|
||||
| "alibaba-dashscope"
|
||||
| "ollama"
|
||||
| "ollama-cloud";
|
||||
export type Provider = KnownProvider | string;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue