From 4dc80fd2e11325a2d8df167054bdccf298aa09a0 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 16 Jul 2026 16:46:49 +0800 Subject: [PATCH] feat(providers): add Alibaba Coding Plan (China + global) --- .../icon-governance-contract.test.ts | 5 +- .../settings/provider-brand-marks.tsx | 2 + .../settings/provider-display-copy.ts | 8 ++ .../src/__tests__/connection-target.test.ts | 28 +++++++ .../src/__tests__/llm-connections.test.ts | 75 +++++++++++++++++++ .../core/src/__tests__/model-catalog.test.ts | 33 ++++++++ .../core/src/__tests__/provider-auth.test.ts | 26 +++++++ packages/core/src/model-metadata.generated.ts | 38 +++++++++- packages/core/src/provider-registry.ts | 71 ++++++++++++++++++ .../src/__tests__/harbor-cell.test.ts | 54 +++++++++++++ .../src/__tests__/provider-env.test.ts | 16 ++++ .../src/__tests__/connection-store.test.ts | 22 ++++++ scripts/sync-model-metadata.mjs | 2 + scripts/sync-model-metadata.test.mjs | 2 +- 14 files changed, 377 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts index 1dfec8c1ec9..be816b9082e 100644 --- a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts +++ b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts @@ -363,7 +363,10 @@ describe('icon + typography governance contract', () => { assert.match(marks, /license: MIT/); assert.match(marks, /73ea97baf919edb3d888a77cd45b21574e190a80124a59e199ea6953a28a959c/); assert.match(marks, /import alibabaBrandMark from '\.\.\/assets\/provider-brands\/alibabacloud\.svg';/); - assert.match(marks, /case 'alibaba':\s*return /); + assert.match( + marks, + /case 'alibaba':\s*case 'alibaba-coding-plan-cn':\s*case 'alibaba-coding-plan':\s*return /, + ); assert.match( notices, /apps\/desktop\/src\/renderer\/assets\/provider-brands\/alibabacloud\.svg[\s\S]*e4302041fbb3039608d25f9f618bd462783b875e[\s\S]*packages\/static-svg\/icons\/alibabacloud-color\.svg[\s\S]*73ea97baf919edb3d888a77cd45b21574e190a80124a59e199ea6953a28a959c/, diff --git a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx index 5194fd93a7b..7d02b1873b3 100644 --- a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx +++ b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx @@ -374,6 +374,8 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen case 'openrouter': return ; case 'alibaba': + case 'alibaba-coding-plan-cn': + case 'alibaba-coding-plan': return ; case 'cloudflare-workers-ai': return ; diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index d0fac52b25a..6ebf82b7024 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -207,6 +207,14 @@ export const PROVIDER_DISPLAY_COPY = { zh: { name: 'Alibaba', description: '阿里云百炼接入,通义千问 Qwen 模型', badge: 'API' }, en: { name: 'Alibaba', description: 'Alibaba Cloud API access for Qwen models.', badge: 'API' }, }, + 'alibaba-coding-plan-cn': { + zh: { name: 'Alibaba Coding Plan 中国站', description: '阿里云百炼 Coding Plan 订阅 · 中国站', badge: 'Plan' }, + en: { name: 'Alibaba Coding Plan (China)', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools · China.', badge: 'Plan' }, + }, + 'alibaba-coding-plan': { + zh: { name: 'Alibaba Coding Plan 国际站', description: '阿里云百炼 Coding Plan 订阅 · 国际站', badge: 'Plan' }, + en: { name: 'Alibaba Coding Plan', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools.', badge: 'Plan' }, + }, // OAuth account providers (not in CATALOG_PROVIDER_TYPES; shown in the // accounts section and on connection rows). 'github-copilot': { diff --git a/packages/cli/src/__tests__/connection-target.test.ts b/packages/cli/src/__tests__/connection-target.test.ts index a2a40696fa6..4ee06ce0897 100644 --- a/packages/cli/src/__tests__/connection-target.test.ts +++ b/packages/cli/src/__tests__/connection-target.test.ts @@ -258,6 +258,34 @@ describe('default session target resolver', () => { assert.equal(target.model, 'deepseek-v4-pro-202606'); }); + test('resolves both Alibaba Coding Plan regions without rewriting the selected model id', async () => { + for (const [providerType, defaultModel] of [ + ['alibaba-coding-plan-cn', 'qwen3.7-plus'], + ['alibaba-coding-plan', 'qwen3-coder-plus'], + ] as const) { + const connection = makeConnection({ + slug: providerType, + name: providerType, + providerType, + defaultModel, + }); + + const target = await resolveDefaultSessionTarget({ + connectionStore: { + getDefault: async () => providerType, + get: async (slug) => slug === providerType ? connection : null, + }, + credentialStore: { + getSecret: async (_slug, kind) => kind === 'api_key' ? `${providerType}-test-key` : null, + }, + }); + + assert.equal(target.connection.providerType, providerType); + assert.equal(target.apiKey, `${providerType}-test-key`); + assert.equal(target.model, defaultModel); + } + }); + test('resolves LM Studio without reading a credential or rewriting the selected model id', async () => { const connection = makeConnection({ slug: 'lm-studio', diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts index 762aa437ba8..2e6bebe75f9 100644 --- a/packages/core/src/__tests__/llm-connections.test.ts +++ b/packages/core/src/__tests__/llm-connections.test.ts @@ -72,6 +72,8 @@ describe('provider compatibility contract', () => { 'groq', 'openrouter', 'alibaba', + 'alibaba-coding-plan-cn', + 'alibaba-coding-plan', 'cloudflare-workers-ai', 'ollama-cloud', 'ollama', @@ -128,6 +130,8 @@ describe('provider compatibility contract', () => { 'groq', 'openrouter', 'alibaba', + 'alibaba-coding-plan-cn', + 'alibaba-coding-plan', ]); assert.deepEqual(CATALOG_PROVIDER_TYPES, [ 'kimi-coding-plan', @@ -174,6 +178,8 @@ describe('provider compatibility contract', () => { 'groq', 'openrouter', 'alibaba', + 'alibaba-coding-plan-cn', + 'alibaba-coding-plan', ]); for (const orderField of ['readyOrder', 'catalogOrder', 'recommendedOrder'] as const) { @@ -767,6 +773,75 @@ describe('provider compatibility contract', () => { ]); }); + const ALIBABA_CODING_PLAN_MODELS = [ + 'qwen3.7-plus', + 'qwen3.7-max', + 'qwen3.6-plus', + 'qwen3.6-flash', + 'qwen3.5-plus', + 'qwen3-max-2026-01-23', + 'qwen3-coder-next', + 'qwen3-coder-plus', + 'glm-5', + 'glm-4.7', + 'kimi-k2.5', + 'MiniMax-M2.5', + ]; + + it('owns Alibaba Coding Plan (China) behavior under its independent persisted id', () => { + const plan = (PROVIDER_REGISTRY as Partial>)['alibaba-coding-plan-cn']; + + assert.ok(plan, 'Alibaba Coding Plan (China) must be available through the shared provider registry'); + assert.equal(plan.label, 'Alibaba Coding Plan (China)'); + assert.equal(plan.baseUrl, 'https://coding.dashscope.aliyuncs.com/v1'); + assert.equal(plan.authKind, 'api_key'); + assert.equal(plan.backendKind, 'ai-sdk'); + assert.equal(plan.status, 'ready'); + assert.equal(plan.protocol, 'openai'); + assert.deepEqual(plan.runtimeAdapter, { kind: 'openai-compatible', name: 'provider' }); + assert.deepEqual(plan.modelDiscovery, { kind: 'fallback' }); + assert.equal(plan.category, 'domestic'); + assert.equal(plan.catalogGroup, 'plans'); + assert.equal(plan.catalogBadge, 'Plan'); + assert.equal(plan.signupUrl, 'https://www.aliyun.com/benefit/scene/codingplan'); + assert.equal(plan.modelsDevId, 'alibaba-coding-plan-cn'); + assert.deepEqual(plan.fallbackModels, ALIBABA_CODING_PLAN_MODELS); + }); + + it('owns Alibaba Coding Plan (global) behavior under its independent persisted id', () => { + const plan = (PROVIDER_REGISTRY as Partial>)['alibaba-coding-plan']; + + assert.ok(plan, 'Alibaba Coding Plan must be available through the shared provider registry'); + assert.equal(plan.label, 'Alibaba Coding Plan'); + assert.equal(plan.baseUrl, 'https://coding-intl.dashscope.aliyuncs.com/v1'); + assert.equal(plan.authKind, 'api_key'); + assert.equal(plan.backendKind, 'ai-sdk'); + assert.equal(plan.status, 'ready'); + assert.equal(plan.protocol, 'openai'); + assert.deepEqual(plan.runtimeAdapter, { kind: 'openai-compatible', name: 'provider' }); + assert.deepEqual(plan.modelDiscovery, { kind: 'fallback' }); + assert.equal(plan.category, 'overseas'); + assert.equal(plan.catalogGroup, 'plans'); + assert.equal(plan.catalogBadge, 'Plan'); + assert.equal(plan.signupUrl, 'https://www.alibabacloud.com/help/en/model-studio/coding-plan'); + assert.equal(plan.modelsDevId, 'alibaba-coding-plan'); + assert.deepEqual(plan.fallbackModels, ALIBABA_CODING_PLAN_MODELS); + }); + + it('separates the two Alibaba Coding Plan regions from the Alibaba direct API path', () => { + const registry = PROVIDER_REGISTRY as Partial>; + const cn = registry['alibaba-coding-plan-cn']; + const global = registry['alibaba-coding-plan']; + const direct = registry.alibaba; + + assert.ok(cn && global && direct); + const baseUrls = new Set([cn.baseUrl, global.baseUrl, direct.baseUrl]); + assert.equal(baseUrls.size, 3, 'each Alibaba access path must have a distinct endpoint'); + assert.equal(direct.catalogGroup, 'api'); + assert.equal(cn.catalogGroup, 'plans'); + assert.equal(global.catalogGroup, 'plans'); + }); + it('owns StepFun China direct API behavior under the stable stepfun id', () => { const stepfun = (PROVIDER_REGISTRY as Partial>).stepfun; diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts index d5f7b4049bf..88f197ba45e 100644 --- a/packages/core/src/__tests__/model-catalog.test.ts +++ b/packages/core/src/__tests__/model-catalog.test.ts @@ -64,6 +64,39 @@ describe('ModelCatalogEntry', () => { }); }); + it('uses the official Alibaba Coding Plan snapshot for both regions without inventing live discovery', () => { + const expectedIds = [ + 'qwen3.7-plus', + 'qwen3.7-max', + 'qwen3.6-plus', + 'qwen3.6-flash', + 'qwen3.5-plus', + 'qwen3-max-2026-01-23', + 'qwen3-coder-next', + 'qwen3-coder-plus', + 'glm-5', + 'glm-4.7', + 'kimi-k2.5', + 'MiniMax-M2.5', + ]; + for (const providerType of ['alibaba-coding-plan-cn', 'alibaba-coding-plan'] as const) { + const entries = buildConnectionModelCatalogEntries({ + connection: { slug: providerType, providerType, defaultModel: 'qwen3.7-plus' }, + }); + + assert.deepEqual(entries.map((entry) => entry.id), expectedIds); + assert.equal(entries[0]?.source, 'static_catalog'); + assert.equal(entries[0]?.provenance.modelSource, 'fallback'); + assert.equal(entries[0]?.contextWindow, 1_000_000); + assert.equal(entries[0]?.maxOutputTokens, 64_000); + assert.deepEqual(entries[0]?.capabilities, { + vision: true, + reasoning: true, + functionCalling: true, + }); + } + }); + it('uses the checked-in StepFun Global snapshot until account discovery succeeds', () => { const entries = buildConnectionModelCatalogEntries({ connection: { diff --git a/packages/core/src/__tests__/provider-auth.test.ts b/packages/core/src/__tests__/provider-auth.test.ts index 489b2bd2646..439a09ef6c4 100644 --- a/packages/core/src/__tests__/provider-auth.test.ts +++ b/packages/core/src/__tests__/provider-auth.test.ts @@ -118,6 +118,32 @@ describe('ProviderAuth contract', () => { expect(contract.actionAvailability.fetch_models).toBe('hidden'); }); + test('Alibaba Coding Plan (China) uses an independent API key and fallback-model flow', () => { + const contract = deriveProviderAuthContract({ + providerType: 'alibaba-coding-plan-cn', + hasSecret: true, + }); + + expect(contract.providerType).toBe('alibaba-coding-plan-cn'); + expect(contract.setupMode).toBe('api_key'); + expect(contract.requiresSecret).toBe(true); + expect(contract.actionAvailability.test_credentials).toBe('available'); + expect(contract.actionAvailability.fetch_models).toBe('hidden'); + }); + + test('Alibaba Coding Plan (global) uses an independent API key and fallback-model flow', () => { + const contract = deriveProviderAuthContract({ + providerType: 'alibaba-coding-plan', + hasSecret: true, + }); + + expect(contract.providerType).toBe('alibaba-coding-plan'); + expect(contract.setupMode).toBe('api_key'); + expect(contract.requiresSecret).toBe(true); + expect(contract.actionAvailability.test_credentials).toBe('available'); + expect(contract.actionAvailability.fetch_models).toBe('hidden'); + }); + test('StepFun Global uses an independent API-key credential and model-discovery flow', () => { const contract = deriveProviderAuthContract({ providerType: 'stepfun-ai', diff --git a/packages/core/src/model-metadata.generated.ts b/packages/core/src/model-metadata.generated.ts index 49f8b9814e9..e39618fbe9c 100644 --- a/packages/core/src/model-metadata.generated.ts +++ b/packages/core/src/model-metadata.generated.ts @@ -2,7 +2,7 @@ // Do not edit by hand; put access-path-specific facts in model-metadata.ts. import type { ModelMetadata } from './model-metadata.js'; -export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "alibaba" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", Record> = { +export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "alibaba" | "alibaba-coding-plan-cn" | "alibaba-coding-plan" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", Record> = { "anthropic": { "claude-fable-5": {"displayName":"Claude Fable 5","lifecycle":"active","docsUrl":"https://docs.anthropic.com/en/docs/about-claude/models","contextWindow":1000000,"maxOutputTokens":128000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, "claude-haiku-4-5": {"displayName":"Claude Haiku 4.5 (latest)","lifecycle":"active","docsUrl":"https://docs.anthropic.com/en/docs/about-claude/models","contextWindow":200000,"maxOutputTokens":64000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, @@ -72,6 +72,34 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "alibaba" | "ce "qwen3.7-plus": {"displayName":"Qwen3.7 Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/models","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, "qwq-plus": {"displayName":"QwQ Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/models","contextWindow":131072,"maxOutputTokens":8192,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, }, + "alibaba-coding-plan-cn": { + "glm-4.7": {"displayName":"GLM-4.7","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":202752,"maxOutputTokens":16384,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "glm-5": {"displayName":"GLM-5","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":202752,"maxOutputTokens":16384,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "kimi-k2.5": {"displayName":"Kimi K2.5","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "MiniMax-M2.5": {"displayName":"MiniMax-M2.5","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":196608,"maxOutputTokens":24576,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "qwen3-coder-next": {"displayName":"Qwen3 Coder Next","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3-coder-plus": {"displayName":"Qwen3 Coder Plus","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3-max-2026-01-23": {"displayName":"Qwen3 Max","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3.5-plus": {"displayName":"Qwen3.5 Plus","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.6-flash": {"displayName":"Qwen3.6 Flash","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.6-plus": {"displayName":"Qwen3.6 Plus","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.7-max": {"displayName":"Qwen3.7 Max","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "qwen3.7-plus": {"displayName":"Qwen3.7 Plus","lifecycle":"active","docsUrl":"https://help.aliyun.com/zh/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":64000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + }, + "alibaba-coding-plan": { + "glm-4.7": {"displayName":"GLM-4.7","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":202752,"maxOutputTokens":16384,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "glm-5": {"displayName":"GLM-5","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":202752,"maxOutputTokens":16384,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "kimi-k2.5": {"displayName":"Kimi K2.5","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "MiniMax-M2.5": {"displayName":"MiniMax-M2.5","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":196608,"maxOutputTokens":24576,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "qwen3-coder-next": {"displayName":"Qwen3 Coder Next","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3-coder-plus": {"displayName":"Qwen3 Coder Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3-max-2026-01-23": {"displayName":"Qwen3 Max","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}}, + "qwen3.5-plus": {"displayName":"Qwen3.5 Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.6-flash": {"displayName":"Qwen3.6 Flash","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.6-plus": {"displayName":"Qwen3.6 Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + "qwen3.7-max": {"displayName":"Qwen3.7 Max","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, + "qwen3.7-plus": {"displayName":"Qwen3.7 Plus","lifecycle":"active","docsUrl":"https://www.alibabacloud.com/help/en/model-studio/coding-plan","contextWindow":1000000,"maxOutputTokens":64000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, + }, "cerebras": { "gemma-4-31b": {"displayName":"Gemma 4 31B IT","lifecycle":"active","docsUrl":"https://inference-docs.cerebras.ai/models/overview","contextWindow":131072,"maxOutputTokens":40960,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}}, "gpt-oss-120b": {"displayName":"GPT OSS 120B","lifecycle":"active","docsUrl":"https://inference-docs.cerebras.ai/models/overview","contextWindow":131072,"maxOutputTokens":40960,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}}, @@ -1627,9 +1655,11 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "alibaba" | "ce }, }; -export const GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES: Record<"anthropic" | "alibaba" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", Record> = { +export const GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES: Record<"anthropic" | "alibaba" | "alibaba-coding-plan-cn" | "alibaba-coding-plan" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", Record> = { "anthropic": {}, "alibaba": {}, + "alibaba-coding-plan-cn": {}, + "alibaba-coding-plan": {}, "cerebras": {}, "cohere": {"north-mini-code-1-0":{"npm":"@ai-sdk/openai-compatible","api":"https://api.cohere.ai/compatibility/v1"}}, "cloudflare-workers-ai": {}, @@ -1667,9 +1697,11 @@ export const GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES: Record<"anthropic" | "zenmux": {"anthropic/claude-3.5-haiku":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-3.7-sonnet":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-fable-5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-haiku-4.5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4.1":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4.5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4.6":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4.7":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-opus-4.8":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-sonnet-4":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-sonnet-4.5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-sonnet-4.6":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-sonnet-5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"anthropic/claude-sonnet-5-free":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2.1":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2.5":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2.5-lightning":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2.7":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m2.7-highspeed":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"minimax/minimax-m3":{"npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1"},"openai/gpt-5":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5-codex":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.1":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.1-chat":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.1-codex":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.1-codex-mini":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.2":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.2-codex":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.2-pro":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.3-chat":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.3-codex":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.4":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.4-mini":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.4-nano":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"},"openai/gpt-5.4-pro":{"npm":"@ai-sdk/openai","api":"https://zenmux.ai/api/v1"}}, }; -export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "alibaba" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", { id: string; name: string; api?: string; doc: string }> = { +export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "alibaba" | "alibaba-coding-plan-cn" | "alibaba-coding-plan" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "github-copilot" | "google" | "gemini-cli" | "groq" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "opencode" | "opencode-go" | "openrouter" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "xiaomi" | "zai" | "zai-coding-plan" | "zenmux", { id: string; name: string; api?: string; doc: string }> = { "anthropic": {"id":"anthropic","name":"Anthropic","doc":"https://docs.anthropic.com/en/docs/about-claude/models"}, "alibaba": {"id":"alibaba","name":"Alibaba","api":"https://dashscope-intl.aliyuncs.com/compatible-mode/v1","doc":"https://www.alibabacloud.com/help/en/model-studio/models"}, + "alibaba-coding-plan-cn": {"id":"alibaba-coding-plan-cn","name":"Alibaba Coding Plan (China)","api":"https://coding.dashscope.aliyuncs.com/v1","doc":"https://help.aliyun.com/zh/model-studio/coding-plan"}, + "alibaba-coding-plan": {"id":"alibaba-coding-plan","name":"Alibaba Coding Plan","api":"https://coding-intl.dashscope.aliyuncs.com/v1","doc":"https://www.alibabacloud.com/help/en/model-studio/coding-plan"}, "cerebras": {"id":"cerebras","name":"Cerebras","doc":"https://inference-docs.cerebras.ai/models/overview"}, "cohere": {"id":"cohere","name":"Cohere","doc":"https://docs.cohere.com/docs/models"}, "cloudflare-workers-ai": {"id":"cloudflare-workers-ai","name":"Cloudflare Workers AI","api":"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1","doc":"https://developers.cloudflare.com/workers-ai/models/"}, diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 841bceec481..4d880eb3465 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -320,6 +320,39 @@ const alibabaModelIds = toolCallingModelIds( GENERATED_MODELS_DEV_METADATA.alibaba, ['qwen3.7-plus'], ); +const alibabaCodingPlanCn = GENERATED_MODELS_DEV_PROVIDER_FACTS['alibaba-coding-plan-cn']; +if (alibabaCodingPlanCn.id !== 'alibaba-coding-plan-cn' || alibabaCodingPlanCn.api !== 'https://coding.dashscope.aliyuncs.com/v1') { + throw new Error('models.dev Alibaba Coding Plan (China) provider facts are missing the stable id or API'); +} +const alibabaCodingPlanGlobal = GENERATED_MODELS_DEV_PROVIDER_FACTS['alibaba-coding-plan']; +if (alibabaCodingPlanGlobal.id !== 'alibaba-coding-plan' || alibabaCodingPlanGlobal.api !== 'https://coding-intl.dashscope.aliyuncs.com/v1') { + throw new Error('models.dev Alibaba Coding Plan provider facts are missing the stable id or API'); +} +// Alibaba's Coding Plan docs are authoritative for this subscription allowlist; the +// plan endpoint does not publish a /models discovery contract. China and global share +// an identical tool-calling text-model snapshot (image models are excluded). +const alibabaCodingPlanModelIds = [ + 'qwen3.7-plus', + 'qwen3.7-max', + 'qwen3.6-plus', + 'qwen3.6-flash', + 'qwen3.5-plus', + 'qwen3-max-2026-01-23', + 'qwen3-coder-next', + 'qwen3-coder-plus', + 'glm-5', + 'glm-4.7', + 'kimi-k2.5', + 'MiniMax-M2.5', +] as const; +for (const id of alibabaCodingPlanModelIds) { + if (!GENERATED_MODELS_DEV_METADATA['alibaba-coding-plan-cn'][id]?.capabilities?.functionCalling) { + throw new Error(`models.dev Alibaba Coding Plan (China) snapshot is missing tool-capable model ${id}`); + } + if (!GENERATED_MODELS_DEV_METADATA['alibaba-coding-plan'][id]?.capabilities?.functionCalling) { + throw new Error(`models.dev Alibaba Coding Plan snapshot is missing tool-capable model ${id}`); + } +} const vercel = GENERATED_MODELS_DEV_PROVIDER_FACTS.vercel; if (vercel.id !== 'vercel') { throw new Error('models.dev Vercel AI Gateway provider facts are missing stable id vercel'); @@ -1127,6 +1160,44 @@ const providerRegistry = { readyOrder: 41, catalogOrder: 41, }, + 'alibaba-coding-plan-cn': { + label: alibabaCodingPlanCn.name, + description: 'Alibaba Cloud Model Studio Coding Plan (China) for interactive AI coding tools.', + baseUrl: alibabaCodingPlanCn.api, + authKind: 'api_key', + backendKind: 'ai-sdk', + fallbackModels: [...alibabaCodingPlanModelIds], + status: 'ready', + protocol: 'openai', + runtimeAdapter: { kind: 'openai-compatible', name: 'provider' }, + modelDiscovery: { kind: 'fallback' }, + category: 'domestic', + catalogGroup: 'plans', + catalogBadge: 'Plan', + signupUrl: 'https://www.aliyun.com/benefit/scene/codingplan', + modelsDevId: alibabaCodingPlanCn.id, + readyOrder: 41.1, + catalogOrder: 41.1, + }, + 'alibaba-coding-plan': { + label: alibabaCodingPlanGlobal.name, + description: 'Alibaba Cloud Model Studio Coding Plan for interactive AI coding tools.', + baseUrl: alibabaCodingPlanGlobal.api, + authKind: 'api_key', + backendKind: 'ai-sdk', + fallbackModels: [...alibabaCodingPlanModelIds], + status: 'ready', + protocol: 'openai', + runtimeAdapter: { kind: 'openai-compatible', name: 'provider' }, + modelDiscovery: { kind: 'fallback' }, + category: 'overseas', + catalogGroup: 'plans', + catalogBadge: 'Plan', + signupUrl: 'https://www.alibabacloud.com/help/en/model-studio/coding-plan', + modelsDevId: alibabaCodingPlanGlobal.id, + readyOrder: 41.2, + catalogOrder: 41.2, + }, 'cloudflare-workers-ai': { label: cloudflareWorkersAi.name, description: 'Cloudflare-hosted models over the account-scoped Workers AI API.', diff --git a/packages/headless/src/__tests__/harbor-cell.test.ts b/packages/headless/src/__tests__/harbor-cell.test.ts index 2e38333a68d..ffe268cc5d1 100644 --- a/packages/headless/src/__tests__/harbor-cell.test.ts +++ b/packages/headless/src/__tests__/harbor-cell.test.ts @@ -2932,6 +2932,60 @@ setTimeout(() => { } }); + test('does not load Alibaba Coding Plan (China) credentials in non-interactive Harbor runs', async () => { + const dir = await mkdtemp(join(tmpdir(), 'maka-cell-alibaba-coding-plan-cn-')); + try { + const credentialsPath = join(dir, 'credentials.json'); + await writeFile(credentialsPath, `${JSON.stringify({ + version: 1, + values: { 'alibaba-coding-plan-cn:apiKey': 'must-not-load-in-headless' }, + })}\n`, 'utf8'); + + const resolved = resolveHarborCellAiSdkEnv({ + provider: 'alibaba-coding-plan-cn', + model: 'qwen3.7-plus', + env: { + MAKA_CREDENTIALS_PATH: credentialsPath, + ALIBABA_CODING_PLAN_API_KEY: 'must-also-not-load-in-headless', + }, + ts: 1, + }); + + assert.equal(resolved.apiKey, ''); + assert.equal(resolved.connection.providerType, 'alibaba-coding-plan-cn'); + assert.equal(resolved.connection.defaultModel, 'qwen3.7-plus'); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test('does not load Alibaba Coding Plan (global) credentials in non-interactive Harbor runs', async () => { + const dir = await mkdtemp(join(tmpdir(), 'maka-cell-alibaba-coding-plan-')); + try { + const credentialsPath = join(dir, 'credentials.json'); + await writeFile(credentialsPath, `${JSON.stringify({ + version: 1, + values: { 'alibaba-coding-plan:apiKey': 'must-not-load-in-headless' }, + })}\n`, 'utf8'); + + const resolved = resolveHarborCellAiSdkEnv({ + provider: 'alibaba-coding-plan', + model: 'qwen3.7-plus', + env: { + MAKA_CREDENTIALS_PATH: credentialsPath, + ALIBABA_CODING_PLAN_API_KEY: 'must-also-not-load-in-headless', + }, + ts: 1, + }); + + assert.equal(resolved.apiKey, ''); + assert.equal(resolved.connection.providerType, 'alibaba-coding-plan'); + assert.equal(resolved.connection.defaultModel, 'qwen3.7-plus'); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + test('resolves StepFun China only from its direct API credential env', () => { const resolved = resolveHarborCellAiSdkEnv({ provider: 'stepfun', diff --git a/packages/headless/src/__tests__/provider-env.test.ts b/packages/headless/src/__tests__/provider-env.test.ts index 9612a62811d..b9c306bf6ea 100644 --- a/packages/headless/src/__tests__/provider-env.test.ts +++ b/packages/headless/src/__tests__/provider-env.test.ts @@ -194,6 +194,22 @@ test('Tencent Token Plan is unavailable to non-interactive headless credential l ); }); +test('Alibaba Coding Plan (China) is unavailable to non-interactive headless credential loading', () => { + assert.equal(providerCredentialEnv('alibaba-coding-plan-cn'), undefined); + assert.throws( + () => requireProviderCredentialEnv('alibaba-coding-plan-cn'), + /provider does not support API key files: alibaba-coding-plan-cn/, + ); +}); + +test('Alibaba Coding Plan (global) is unavailable to non-interactive headless credential loading', () => { + assert.equal(providerCredentialEnv('alibaba-coding-plan'), undefined); + assert.throws( + () => requireProviderCredentialEnv('alibaba-coding-plan'), + /provider does not support API key files: alibaba-coding-plan/, + ); +}); + test('StepFun China keeps direct API credentials separate from global and plan identities', () => { assert.deepEqual(providerCredentialEnv('stepfun'), { apiKeys: ['STEPFUN_API_KEY'], diff --git a/packages/storage/src/__tests__/connection-store.test.ts b/packages/storage/src/__tests__/connection-store.test.ts index 4637de5d021..f0ea14b095b 100644 --- a/packages/storage/src/__tests__/connection-store.test.ts +++ b/packages/storage/src/__tests__/connection-store.test.ts @@ -405,6 +405,28 @@ describe('FileConnectionStore', () => { }); }); + test('persists both Alibaba Coding Plan region ids and exact default models', async () => { + for (const [slug, providerType, defaultModel] of [ + ['alibaba-coding-plan-cn', 'alibaba-coding-plan-cn', 'qwen3.7-plus'], + ['alibaba-coding-plan', 'alibaba-coding-plan', 'qwen3-coder-plus'], + ] as const) { + await withConnectionStore(async (store, dir) => { + await store.create({ + slug, + name: slug, + providerType, + defaultModel, + }); + + const persisted = JSON.parse(await readFile(join(dir, 'llm-connections.json'), 'utf8')) as { + connections: Array<{ providerType: string; defaultModel: string }>; + }; + assert.equal(persisted.connections[0]?.providerType, providerType); + assert.equal(persisted.connections[0]?.defaultModel, defaultModel); + }); + } + }); + test('persists the LM Studio provider id and exact local model id', async () => { await withConnectionStore(async (store, dir) => { await store.create({ diff --git a/scripts/sync-model-metadata.mjs b/scripts/sync-model-metadata.mjs index 681da9f59af..cf6192fd5ec 100644 --- a/scripts/sync-model-metadata.mjs +++ b/scripts/sync-model-metadata.mjs @@ -5,6 +5,8 @@ const DEFAULT_OUTPUT = 'packages/core/src/model-metadata.generated.ts'; const PROVIDERS = { anthropic: 'anthropic', alibaba: 'alibaba', + 'alibaba-coding-plan-cn': 'alibaba-coding-plan-cn', + 'alibaba-coding-plan': 'alibaba-coding-plan', cerebras: 'cerebras', cohere: 'cohere', 'cloudflare-workers-ai': 'cloudflare-workers-ai', diff --git a/scripts/sync-model-metadata.test.mjs b/scripts/sync-model-metadata.test.mjs index ef60815a776..bf8d1f099f5 100644 --- a/scripts/sync-model-metadata.test.mjs +++ b/scripts/sync-model-metadata.test.mjs @@ -7,7 +7,7 @@ import { promisify } from 'node:util'; import test from 'node:test'; const execFileAsync = promisify(execFile); -const PROVIDER_IDS = ['alibaba', 'anthropic', 'cerebras', 'cloudflare-workers-ai', 'cohere', 'deepinfra', 'deepseek', 'fireworks-ai', 'github-copilot', 'google', 'groq', 'huggingface', 'minimax', 'minimax-cn', 'mistral', 'moonshotai-cn', 'nvidia', 'ollama-cloud', 'openai', 'opencode', 'opencode-go', 'openrouter', 'siliconflow', 'stepfun', 'stepfun-ai', 'stepfun-ai-step-plan', 'tencent-coding-plan', 'tencent-token-plan', 'tencent-tokenhub', 'togetherai', 'vercel', 'xai', 'xiaomi', 'zai', 'zai-coding-plan', 'zenmux']; +const PROVIDER_IDS = ['alibaba', 'alibaba-coding-plan', 'alibaba-coding-plan-cn', 'anthropic', 'cerebras', 'cloudflare-workers-ai', 'cohere', 'deepinfra', 'deepseek', 'fireworks-ai', 'github-copilot', 'google', 'groq', 'huggingface', 'minimax', 'minimax-cn', 'mistral', 'moonshotai-cn', 'nvidia', 'ollama-cloud', 'openai', 'opencode', 'opencode-go', 'openrouter', 'siliconflow', 'stepfun', 'stepfun-ai', 'stepfun-ai-step-plan', 'tencent-coding-plan', 'tencent-token-plan', 'tencent-tokenhub', 'togetherai', 'vercel', 'xai', 'xiaomi', 'zai', 'zai-coding-plan', 'zenmux']; function withRequiredProviders(openai) { return Object.fromEntries(PROVIDER_IDS.map((id) => {