diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index fd0cb9b1f4c..901a178ac56 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -1234,7 +1234,12 @@ describe('AuthDialog', { timeout: 15000 }, () => { await pressEnterAndWaitFor( stdin, lastFrame, - 'Alibaba ModelStudio · Step 1/2 · API Key', + 'Alibaba ModelStudio · Step 1/3 · Region', + ); + await pressEnterAndWaitFor( + stdin, + lastFrame, + 'Alibaba ModelStudio · Step 2/3 · API Key', ); await typeText(stdin, 'sk-token-plan'); @@ -1242,7 +1247,7 @@ describe('AuthDialog', { timeout: 15000 }, () => { await pressEnterAndWaitFor( stdin, lastFrame, - 'Alibaba ModelStudio · Step 2/2 · Model IDs', + 'Alibaba ModelStudio · Step 3/3 · Model IDs', ); stdin.write('\r'); await vi.waitFor( @@ -1309,7 +1314,12 @@ describe('AuthDialog', { timeout: 15000 }, () => { await pressEnterAndWaitFor( stdin, lastFrame, - 'Alibaba ModelStudio · Step 1/2 · API Key', + 'Alibaba ModelStudio · Step 1/3 · Region', + ); + await pressEnterAndWaitFor( + stdin, + lastFrame, + 'Alibaba ModelStudio · Step 2/3 · API Key', ); await typeText(stdin, 'sk-token-plan'); @@ -1317,7 +1327,7 @@ describe('AuthDialog', { timeout: 15000 }, () => { await pressEnterAndWaitFor( stdin, lastFrame, - 'Alibaba ModelStudio · Step 2/2 · Model IDs', + 'Alibaba ModelStudio · Step 3/3 · Model IDs', ); // The Model IDs input is pre-filled with the saved custom model id @@ -1373,7 +1383,22 @@ describe('AuthDialog', { timeout: 15000 }, () => { await pressEnterAndWaitFor( stdin, lastFrame, - 'Alibaba ModelStudio · Step 1/2 · API Key', + 'Alibaba ModelStudio · Step 1/3 · Region', + ); + await pressEnterAndWaitFor( + stdin, + lastFrame, + 'Alibaba ModelStudio · Step 2/3 · API Key', + ); + stdin.write('\u001b'); + + await vi.waitFor( + () => { + expect(lastFrame()).toContain( + 'Alibaba ModelStudio · Step 1/3 · Region', + ); + }, + { timeout: WAIT_FOR_TIMEOUT }, ); stdin.write('\u001b'); diff --git a/packages/core/src/providers/__tests__/presets/alibaba-token-plan.test.ts b/packages/core/src/providers/__tests__/presets/alibaba-token-plan.test.ts index b54b526a33f..5d62b47eaeb 100644 --- a/packages/core/src/providers/__tests__/presets/alibaba-token-plan.test.ts +++ b/packages/core/src/providers/__tests__/presets/alibaba-token-plan.test.ts @@ -5,18 +5,22 @@ */ import { describe, expect, it } from 'vitest'; +import { AuthType } from '../../../core/contentGenerator.js'; import { - AuthType, TOKEN_PLAN_ENV_KEY, TOKEN_PLAN_BASE_URL, + TOKEN_PLAN_CHINA_BASE_URL, + TOKEN_PLAN_GLOBAL_BASE_URL, tokenPlanProvider, +} from '../../presets/alibaba-token-plan.js'; +import { buildInstallPlan, buildProviderTemplate, computeModelListVersion, getDefaultModelIds, resolveBaseUrl, providerMatchesCredentials, -} from '@qwen-code/qwen-code-core'; +} from '../../provider-config.js'; describe('token plan provider', () => { it('creates a Token Plan install plan', () => { @@ -93,7 +97,44 @@ describe('token plan provider', () => { ]); expect(plan.providerState).toEqual({ 'providerMetadata.token-plan': { - baseUrl: TOKEN_PLAN_BASE_URL, + baseUrl: TOKEN_PLAN_CHINA_BASE_URL, + version, + }, + }); + }); + + it('creates a Token Plan install plan for the Singapore region', () => { + expect(TOKEN_PLAN_GLOBAL_BASE_URL).toBe( + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', + ); + expect(tokenPlanProvider.uiLabels?.baseUrlStepTitle).toBe('Region'); + + const template = buildProviderTemplate( + tokenPlanProvider, + TOKEN_PLAN_GLOBAL_BASE_URL, + ); + const version = computeModelListVersion(template); + const baseUrl = resolveBaseUrl( + tokenPlanProvider, + TOKEN_PLAN_GLOBAL_BASE_URL, + ); + + const plan = buildInstallPlan(tokenPlanProvider, { + baseUrl, + apiKey: 'sk-token', + modelIds: getDefaultModelIds(tokenPlanProvider), + }); + + expect(baseUrl).toBe(TOKEN_PLAN_GLOBAL_BASE_URL); + const firstModel = template[0]!; + expect(firstModel).toMatchObject({ + name: `[ModelStudio Token Plan for Global/Intl] ${firstModel.id}`, + baseUrl: TOKEN_PLAN_GLOBAL_BASE_URL, + envKey: TOKEN_PLAN_ENV_KEY, + }); + expect(plan.providerState).toEqual({ + 'providerMetadata.token-plan': { + baseUrl: TOKEN_PLAN_GLOBAL_BASE_URL, version, }, }); @@ -107,6 +148,20 @@ describe('token plan provider', () => { TOKEN_PLAN_ENV_KEY, ), ).toBe(true); + expect( + providerMatchesCredentials( + tokenPlanProvider, + TOKEN_PLAN_CHINA_BASE_URL, + TOKEN_PLAN_ENV_KEY, + ), + ).toBe(true); + expect( + providerMatchesCredentials( + tokenPlanProvider, + TOKEN_PLAN_GLOBAL_BASE_URL, + TOKEN_PLAN_ENV_KEY, + ), + ).toBe(true); expect( providerMatchesCredentials( tokenPlanProvider, @@ -115,4 +170,43 @@ describe('token plan provider', () => { ), ).toBe(false); }); + + it('owns Token Plan models from both registered regions', () => { + expect( + tokenPlanProvider.ownsModel?.({ + id: 'token-model', + baseUrl: TOKEN_PLAN_CHINA_BASE_URL, + envKey: TOKEN_PLAN_ENV_KEY, + }), + ).toBe(true); + expect( + tokenPlanProvider.ownsModel?.({ + id: 'token-model', + baseUrl: TOKEN_PLAN_GLOBAL_BASE_URL, + envKey: TOKEN_PLAN_ENV_KEY, + }), + ).toBe(true); + expect( + tokenPlanProvider.ownsModel?.({ + id: 'custom-model', + baseUrl: 'https://custom.example.com/v1', + envKey: TOKEN_PLAN_ENV_KEY, + }), + ).toBe(false); + expect( + tokenPlanProvider.ownsModel?.({ + id: 'legacy-token-model', + name: '[ModelStudio Token Plan] legacy-token-model', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + envKey: TOKEN_PLAN_ENV_KEY, + }), + ).toBe(true); + expect( + tokenPlanProvider.ownsModel?.({ + id: 'token-model', + baseUrl: TOKEN_PLAN_CHINA_BASE_URL, + envKey: 'SOME_OTHER_API_KEY', + }), + ).toBe(false); + }); }); diff --git a/packages/core/src/providers/__tests__/provider-config.test.ts b/packages/core/src/providers/__tests__/provider-config.test.ts index 9cf902c8d88..9dafe4f9aa0 100644 --- a/packages/core/src/providers/__tests__/provider-config.test.ts +++ b/packages/core/src/providers/__tests__/provider-config.test.ts @@ -12,13 +12,17 @@ import { computeModelListVersion, findExistingProviderModels, findProviderByCredentials, - getAllProviderBaseUrls, getDefaultModelIds, resolveBaseUrl, shouldShowStep, providerMatchesCredentials, type ProviderConfig, } from '@qwen-code/qwen-code-core'; +import { + TOKEN_PLAN_CHINA_BASE_URL, + TOKEN_PLAN_ENV_KEY, + TOKEN_PLAN_GLOBAL_BASE_URL, +} from '../presets/alibaba-token-plan.js'; function makeConfig(overrides: Partial = {}): ProviderConfig { return { @@ -621,27 +625,46 @@ describe('findProviderByCredentials', () => { expect(china?.id).toBe('coding-plan'); expect(intl?.id).toBe('coding-plan'); }); + + it('matches Token Plan credentials against both registered region URLs', () => { + const china = findProviderByCredentialsSrc( + TOKEN_PLAN_CHINA_BASE_URL, + TOKEN_PLAN_ENV_KEY, + ); + const intl = findProviderByCredentialsSrc( + TOKEN_PLAN_GLOBAL_BASE_URL, + TOKEN_PLAN_ENV_KEY, + ); + expect(china?.id).toBe('token-plan'); + expect(intl?.id).toBe('token-plan'); + }); }); describe('getAllProviderBaseUrls', () => { it('returns a non-empty list including known preset URLs', () => { - const urls = getAllProviderBaseUrls(); + const urls = getAllProviderBaseUrlsSrc(); expect(urls.length).toBeGreaterThan(0); expect(urls).toContain('https://api.deepseek.com'); expect(urls).toContain('https://openrouter.ai/api/v1'); }); it('expands BaseUrlOption[] presets into each option URL', () => { - const urls = getAllProviderBaseUrls(); + const urls = getAllProviderBaseUrlsSrc(); // coding-plan has China + Singapore options expect(urls).toContain('https://coding.dashscope.aliyuncs.com/v1'); expect(urls).toContain('https://coding-intl.dashscope.aliyuncs.com/v1'); + expect(urls).toContain(TOKEN_PLAN_CHINA_BASE_URL); + expect(urls).toContain(TOKEN_PLAN_GLOBAL_BASE_URL); }); }); // The package-name imports above resolve to dist/, which lags the source on a // branch that hasn't been built yet. Re-import via the relative source path so // these new edge-case tests exercise the in-tree implementation. +import { + findProviderByCredentials as findProviderByCredentialsSrc, + getAllProviderBaseUrls as getAllProviderBaseUrlsSrc, +} from '../all-providers.js'; import { resolveBaseUrl as resolveBaseUrlSrc, providerMatchesCredentials as providerMatchesCredentialsSrc, diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts index fad4b9f31f5..fbe784bae52 100644 --- a/packages/core/src/providers/index.ts +++ b/packages/core/src/providers/index.ts @@ -66,7 +66,9 @@ export { } from './presets/alibaba-coding-plan.js'; export { TOKEN_PLAN_BASE_URL, + TOKEN_PLAN_CHINA_BASE_URL, TOKEN_PLAN_ENV_KEY, + TOKEN_PLAN_GLOBAL_BASE_URL, } from './presets/alibaba-token-plan.js'; export { GROK_BASE_URL, GROK_ENV_KEY } from './presets/grok.js'; export { diff --git a/packages/core/src/providers/presets/alibaba-token-plan.ts b/packages/core/src/providers/presets/alibaba-token-plan.ts index 3431a57084e..2e0a5e01054 100644 --- a/packages/core/src/providers/presets/alibaba-token-plan.ts +++ b/packages/core/src/providers/presets/alibaba-token-plan.ts @@ -12,8 +12,11 @@ import type { ProviderConfig, ModelSpec } from '../types.js'; // --------------------------------------------------------------------------- export const TOKEN_PLAN_ENV_KEY = 'BAILIAN_TOKEN_PLAN_API_KEY'; -export const TOKEN_PLAN_BASE_URL = +export const TOKEN_PLAN_CHINA_BASE_URL = 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1'; +export const TOKEN_PLAN_GLOBAL_BASE_URL = + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1'; +export const TOKEN_PLAN_BASE_URL = TOKEN_PLAN_CHINA_BASE_URL; const TOKEN_PLAN_MODELS: ModelSpec[] = [ { @@ -76,11 +79,36 @@ export const tokenPlanProvider: ProviderConfig = { description: 'For teams and companies · Usage-based billing with dedicated endpoint', protocol: AuthType.USE_OPENAI, - baseUrl: TOKEN_PLAN_BASE_URL, + baseUrl: [ + { + id: 'cn-beijing', + label: 'China (Beijing)', + url: TOKEN_PLAN_CHINA_BASE_URL, + documentationUrl: + 'https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856', + }, + { + id: 'ap-southeast-1', + label: 'Singapore (International)', + url: TOKEN_PLAN_GLOBAL_BASE_URL, + documentationUrl: + 'https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model', + }, + ], envKey: TOKEN_PLAN_ENV_KEY, models: TOKEN_PLAN_MODELS, modelsEditable: true, - modelNamePrefix: 'ModelStudio Token Plan', + modelNamePrefix: (baseUrl) => + baseUrl === TOKEN_PLAN_GLOBAL_BASE_URL + ? 'ModelStudio Token Plan for Global/Intl' + : 'ModelStudio Token Plan', + ownsModel: (model) => + model.envKey === TOKEN_PLAN_ENV_KEY && + ((typeof model.baseUrl === 'string' && + (model.baseUrl === TOKEN_PLAN_CHINA_BASE_URL || + model.baseUrl === TOKEN_PLAN_GLOBAL_BASE_URL)) || + (typeof model.name === 'string' && + model.name.startsWith('[ModelStudio Token Plan]'))), uiGroup: 'alibaba', - uiLabels: { flowTitle: 'Alibaba ModelStudio' }, + uiLabels: { flowTitle: 'Alibaba ModelStudio', baseUrlStepTitle: 'Region' }, }; diff --git a/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.test.ts b/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.test.ts index 723dc7bd2f6..c07b37163e0 100644 --- a/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.test.ts +++ b/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.test.ts @@ -6,7 +6,10 @@ import { describe, expect, it } from 'vitest'; -import { getSubscriptionPlanConfig } from './subscriptionPlanDefinitions.js'; +import { + CodingPlanRegion, + getSubscriptionPlanConfig, +} from './subscriptionPlanDefinitions.js'; describe('subscription plan definitions', () => { it('keeps Token Plan on its dedicated model list', () => { @@ -38,4 +41,31 @@ describe('subscription plan definitions', () => { ?.generationConfig, ).toEqual({ contextWindowSize: 1000000 }); }); + + it('defaults Token Plan to China and supports the Singapore region', () => { + const china = getSubscriptionPlanConfig('token'); + const global = getSubscriptionPlanConfig('token', CodingPlanRegion.GLOBAL); + + expect(china.region).toBe(CodingPlanRegion.CHINA); + expect(china.baseUrl).toBe( + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1', + ); + const firstChinaModel = china.template[0]!; + expect(firstChinaModel).toMatchObject({ + name: `[ModelStudio Token Plan] ${firstChinaModel.id}`, + baseUrl: + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1', + }); + + expect(global.region).toBe(CodingPlanRegion.GLOBAL); + expect(global.baseUrl).toBe( + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', + ); + const firstGlobalModel = global.template[0]!; + expect(firstGlobalModel).toMatchObject({ + name: `[ModelStudio Token Plan for Global/Intl] ${firstGlobalModel.id}`, + baseUrl: + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', + }); + }); }); diff --git a/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.ts b/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.ts index 47949eb0f50..be4278efc37 100644 --- a/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.ts +++ b/packages/vscode-ide-companion/src/services/subscriptionPlanDefinitions.ts @@ -26,6 +26,14 @@ export type CodingPlanTemplate = SubscriptionPlanModelConfig[]; export const CODING_PLAN_ENV_KEY = 'BAILIAN_CODING_PLAN_API_KEY'; export const TOKEN_PLAN_ENV_KEY = 'BAILIAN_TOKEN_PLAN_API_KEY'; +const TOKEN_PLAN_CHINA_ENDPOINT = + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1'; +const TOKEN_PLAN_GLOBAL_ENDPOINT = + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1'; +const TOKEN_PLAN_CHINA_DOC_URL = + 'https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856'; +const TOKEN_PLAN_GLOBAL_DOC_URL = + 'https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model'; interface SubscriptionPlanRegionConfig< TRegion extends string = SubscriptionPlanRegion, @@ -168,14 +176,25 @@ const TOKEN_PLAN: SubscriptionPlanDefinition<'token'> = { modelNamePrefix: 'ModelStudio Token Plan', authEventType: 'coding-plan', metadataKey: 'tokenPlan', - endpoint: - 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1', - documentationUrl: - 'https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856', - apiKeyUrl: - 'https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856', - usageDocumentationUrl: - 'https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856', + defaultRegion: CodingPlanRegion.CHINA, + regions: [ + { + id: CodingPlanRegion.CHINA, + title: 'China (Beijing)', + endpoint: TOKEN_PLAN_CHINA_ENDPOINT, + documentationUrl: TOKEN_PLAN_CHINA_DOC_URL, + apiKeyUrl: TOKEN_PLAN_CHINA_DOC_URL, + }, + { + id: CodingPlanRegion.GLOBAL, + title: 'Singapore (International)', + endpoint: TOKEN_PLAN_GLOBAL_ENDPOINT, + documentationUrl: TOKEN_PLAN_GLOBAL_DOC_URL, + apiKeyUrl: TOKEN_PLAN_GLOBAL_DOC_URL, + modelNamePrefix: 'ModelStudio Token Plan for Global/Intl', + }, + ], + usageDocumentationUrl: TOKEN_PLAN_CHINA_DOC_URL, models: BAILIAN_TOKEN_PLAN_MODELS, };