feat(cli): add Coding Plan Global/Intl region support

Add support for Coding Plan international region with separate base URL:
- Add CodingPlanRegion enum (CHINA, GLOBAL) for region management
- Add CODING_PLAN_INTL_MODELS template with intl base URL
- Add version storage for both regions (codingPlan.version/versionIntl)
- Update AuthDialog to show both region options
- Update useCodingPlanUpdates to handle region-specific updates
- Add i18n translations for all supported languages
- Fix and update unit tests

Users can now choose between:
- Coding Plan (Bailian, China) - https://coding.dashscope.aliyuncs.com/v1
- Coding Plan (Bailian, Global/Intl) - https://coding-intl.dashscope.aliyuncs.com/v1

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mingholy.lmh 2026-02-17 20:19:21 +08:00 committed by qwen-code-ci-bot
parent a0a0a70b12
commit 39360dc058
13 changed files with 684 additions and 219 deletions

View file

@ -17,6 +17,7 @@ import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { t } from '../../i18n/index.js';
import { CodingPlanRegion } from '../../constants/codingPlan.js';
const MODEL_PROVIDERS_DOCUMENTATION_URL =
'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/#modelproviders';
@ -34,7 +35,7 @@ function parseDefaultAuthType(
}
// Sub-mode types for API-KEY authentication
type ApiKeySubMode = 'coding-plan' | 'custom';
type ApiKeySubMode = 'coding-plan' | 'coding-plan-intl' | 'custom';
// View level for navigation
type ViewLevel = 'main' | 'api-key-sub' | 'api-key-input' | 'custom-info';
@ -52,6 +53,9 @@ export function AuthDialog(): React.JSX.Element {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [viewLevel, setViewLevel] = useState<ViewLevel>('main');
const [apiKeySubModeIndex, setApiKeySubModeIndex] = useState<number>(0);
const [region, setRegion] = useState<CodingPlanRegion>(
CodingPlanRegion.CHINA,
);
// Main authentication entries
const mainItems = [
@ -71,9 +75,14 @@ export function AuthDialog(): React.JSX.Element {
const apiKeySubItems = [
{
key: 'coding-plan',
label: t('Coding Plan (Bailian)'),
label: t('Coding Plan (Bailian, China)'),
value: 'coding-plan' as ApiKeySubMode,
},
{
key: 'coding-plan-intl',
label: t('Coding Plan (Bailian, Global/Intl)'),
value: 'coding-plan-intl' as ApiKeySubMode,
},
{
key: 'custom',
label: t('Custom'),
@ -135,6 +144,10 @@ export function AuthDialog(): React.JSX.Element {
onAuthError(null);
if (subMode === 'coding-plan') {
setRegion(CodingPlanRegion.CHINA);
setViewLevel('api-key-input');
} else if (subMode === 'coding-plan-intl') {
setRegion(CodingPlanRegion.GLOBAL);
setViewLevel('api-key-input');
} else {
setViewLevel('custom-info');
@ -149,8 +162,8 @@ export function AuthDialog(): React.JSX.Element {
return;
}
// Submit to parent for processing
await handleCodingPlanSubmit(apiKey);
// Submit to parent for processing with region info
await handleCodingPlanSubmit(apiKey, region);
};
const handleGoBack = () => {
@ -264,7 +277,11 @@ export function AuthDialog(): React.JSX.Element {
// Render API key input for coding-plan mode
const renderApiKeyInputView = () => (
<Box marginTop={1}>
<ApiKeyInput onSubmit={handleApiKeyInputSubmit} onCancel={handleGoBack} />
<ApiKeyInput
onSubmit={handleApiKeyInputSubmit}
onCancel={handleGoBack}
region={region}
/>
</Box>
);

View file

@ -30,9 +30,9 @@ import { AuthState, MessageType } from '../types.js';
import type { HistoryItem } from '../types.js';
import { t } from '../../i18n/index.js';
import {
CODING_PLAN_MODELS,
CODING_PLAN_ENV_KEY,
CODING_PLAN_VERSION,
getCodingPlanConfig,
isCodingPlanConfig,
CodingPlanRegion,
} from '../../constants/codingPlan.js';
export type { QwenAuthState } from '../hooks/useQwenAuth.js';
@ -285,29 +285,36 @@ export const useAuthCommand = (
/**
* Handle coding plan submission - generates configs from template and stores api-key
* @param apiKey - The API key to store
* @param region - The region to use (default: CHINA)
*/
const handleCodingPlanSubmit = useCallback(
async (apiKey: string) => {
async (
apiKey: string,
region: CodingPlanRegion = CodingPlanRegion.CHINA,
) => {
try {
setIsAuthenticating(true);
setAuthError(null);
const envKeyName = CODING_PLAN_ENV_KEY;
// Get configuration based on region
const codingPlanConfig = getCodingPlanConfig(region);
const { template, envKey, version } = codingPlanConfig;
// Get persist scope
const persistScope = getPersistScopeForModelSelection(settings);
// Store api-key in settings.env
settings.setValue(persistScope, `env.${envKeyName}`, apiKey);
settings.setValue(persistScope, `env.${envKey}`, apiKey);
// Sync to process.env immediately so refreshAuth can read the apiKey
process.env[envKeyName] = apiKey;
process.env[envKey] = apiKey;
// Generate model configs from template
const newConfigs: ProviderModelConfig[] = CODING_PLAN_MODELS.map(
const newConfigs: ProviderModelConfig[] = template.map(
(templateConfig) => ({
...templateConfig,
envKey: envKeyName,
envKey,
}),
);
@ -317,17 +324,14 @@ export const useAuthCommand = (
settings.merged.modelProviders as ModelProvidersConfig | undefined
)?.[AuthType.USE_OPENAI] || [];
// Identify Coding Plan configs by baseUrl + envKey
// Identify Coding Plan configs by baseUrl + envKey for the given region
// Remove existing Coding Plan configs to ensure template changes are applied
const isCodingPlanConfig = (config: ProviderModelConfig) =>
config.envKey === envKeyName &&
CODING_PLAN_MODELS.some(
(template) => template.baseUrl === config.baseUrl,
);
const checkIsCodingPlanConfig = (config: ProviderModelConfig) =>
isCodingPlanConfig(config.baseUrl, config.envKey, region);
// Filter out existing Coding Plan configs, keep user custom configs
// Filter out existing Coding Plan configs for this region, keep user custom configs
const nonCodingPlanConfigs = existingConfigs.filter(
(existing) => !isCodingPlanConfig(existing),
(existing) => !checkIsCodingPlanConfig(existing),
);
// Add new Coding Plan configs at the beginning
@ -348,11 +352,12 @@ export const useAuthCommand = (
);
// Persist coding plan version for future update detection
settings.setValue(
persistScope,
'codingPlan.version',
CODING_PLAN_VERSION,
);
// Store version with region suffix to distinguish between China and Intl versions
const versionKey =
region === CodingPlanRegion.GLOBAL
? 'codingPlan.versionIntl'
: 'codingPlan.version';
settings.setValue(persistScope, versionKey, version);
// If there are configs, use the first one as the model
if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) {
@ -382,11 +387,16 @@ export const useAuthCommand = (
onAuthChange?.();
// Add success message
const regionLabel =
region === CodingPlanRegion.GLOBAL
? 'Coding Plan (Global/Intl)'
: 'Coding Plan';
addItem(
{
type: MessageType.INFO,
text: t(
'Authenticated successfully with Coding Plan. API key is stored in settings.env.',
'Authenticated successfully with {{region}}. API key is stored in settings.env.',
{ region: regionLabel },
),
},
Date.now(),

View file

@ -11,23 +11,34 @@ import { TextInput } from './shared/TextInput.js';
import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
import { CodingPlanRegion } from '../../constants/codingPlan.js';
import Link from 'ink-link';
interface ApiKeyInputProps {
onSubmit: (apiKey: string) => void;
onCancel: () => void;
region?: CodingPlanRegion;
}
const CODING_PLAN_API_KEY_URL =
'https://bailian.console.aliyun.com/?tab=model#/efm/coding_plan';
const CODING_PLAN_INTL_API_KEY_URL =
'https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=globalset#/efm/api_key';
export function ApiKeyInput({
onSubmit,
onCancel,
region = CodingPlanRegion.CHINA,
}: ApiKeyInputProps): React.JSX.Element {
const [apiKey, setApiKey] = useState('');
const [error, setError] = useState<string | null>(null);
const apiKeyUrl =
region === CodingPlanRegion.GLOBAL
? CODING_PLAN_INTL_API_KEY_URL
: CODING_PLAN_API_KEY_URL;
useKeypress(
(key) => {
if (key.name === 'escape') {
@ -59,9 +70,9 @@ export function ApiKeyInput({
<Text>{t('You can get your exclusive Coding Plan API-KEY here:')}</Text>
</Box>
<Box marginTop={0}>
<Link url={CODING_PLAN_API_KEY_URL} fallback={false}>
<Link url={apiKeyUrl} fallback={false}>
<Text color={theme.status.success} underline>
{CODING_PLAN_API_KEY_URL}
{apiKeyUrl}
</Text>
</Link>
</Box>

View file

@ -15,6 +15,7 @@ import {
type ApprovalMode,
} from '@qwen-code/qwen-code-core';
import { type SettingScope } from '../../config/settings.js';
import { type CodingPlanRegion } from '../../constants/codingPlan.js';
import type { AuthState } from '../types.js';
import { type VisionSwitchOutcome } from '../components/ModelSwitchDialog.js';
// OpenAICredentials type (previously imported from OpenAIKeyPrompt)
@ -40,7 +41,10 @@ export interface UIActions {
authType: AuthType | undefined,
credentials?: OpenAICredentials,
) => Promise<void>;
handleCodingPlanSubmit: (apiKey: string) => Promise<void>;
handleCodingPlanSubmit: (
apiKey: string,
region?: CodingPlanRegion,
) => Promise<void>;
setAuthState: (state: AuthState) => void;
onAuthError: (error: string | null) => void;
cancelAuthentication: () => void;

View file

@ -7,34 +7,16 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useCodingPlanUpdates } from './useCodingPlanUpdates.js';
import { CODING_PLAN_ENV_KEY } from '../../constants/codingPlan.js';
import {
CODING_PLAN_ENV_KEY,
CODING_PLAN_INTL_ENV_KEY,
CODING_PLAN_BASE_URL,
CODING_PLAN_INTL_BASE_URL,
CODING_PLAN_VERSION,
CODING_PLAN_INTL_VERSION,
} from '../../constants/codingPlan.js';
import { AuthType } from '@qwen-code/qwen-code-core';
// Mock the constants module
vi.mock('../../constants/codingPlan.js', async () => {
const actual = await vi.importActual('../../constants/codingPlan.js');
return {
...actual,
CODING_PLAN_VERSION: 'test-version-hash',
CODING_PLAN_MODELS: [
{
id: 'test-model-1',
name: 'Test Model 1',
baseUrl: 'https://test.example.com/v1',
description: 'Test model 1',
envKey: 'BAILIAN_CODING_PLAN_API_KEY',
},
{
id: 'test-model-2',
name: 'Test Model 2',
baseUrl: 'https://test.example.com/v1',
description: 'Test model 2',
envKey: 'BAILIAN_CODING_PLAN_API_KEY',
},
],
};
});
describe('useCodingPlanUpdates', () => {
const mockSettings = {
merged: {
@ -57,6 +39,7 @@ describe('useCodingPlanUpdates', () => {
beforeEach(() => {
vi.clearAllMocks();
delete process.env[CODING_PLAN_ENV_KEY];
delete process.env[CODING_PLAN_INTL_ENV_KEY];
});
describe('version comparison', () => {
@ -74,8 +57,8 @@ describe('useCodingPlanUpdates', () => {
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should not show update prompt when versions match', () => {
mockSettings.merged.codingPlan = { version: 'test-version-hash' };
it('should not show update prompt when China versions match', () => {
mockSettings.merged.codingPlan = { version: CODING_PLAN_VERSION };
const { result } = renderHook(() =>
useCodingPlanUpdates(
@ -88,7 +71,23 @@ describe('useCodingPlanUpdates', () => {
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should show update prompt when versions differ', async () => {
it('should not show update prompt when Global versions match', () => {
mockSettings.merged.codingPlan = {
versionIntl: CODING_PLAN_INTL_VERSION,
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should show update prompt when China versions differ', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const { result } = renderHook(() =>
@ -103,21 +102,38 @@ describe('useCodingPlanUpdates', () => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
expect(result.current.codingPlanUpdateRequest?.prompt).toContain('China');
});
it('should show update prompt when Global versions differ', async () => {
mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
'New model configurations',
'Global',
);
});
});
describe('update execution', () => {
it('should execute update when user confirms', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
it('should execute China region update when user confirms', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-1',
baseUrl: 'https://test.example.com/v1',
id: 'test-model-china-1',
baseUrl: CODING_PLAN_BASE_URL,
envKey: CODING_PLAN_ENV_KEY,
},
{
@ -150,22 +166,81 @@ describe('useCodingPlanUpdates', () => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should update version
// Should update version with correct hash
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.version',
'test-version-hash',
CODING_PLAN_VERSION,
);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
// Should show success message
// Should show success message with region info
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('updated successfully'),
text: expect.stringContaining('Coding Plan'),
}),
expect.any(Number),
);
});
it('should execute Global region update when user confirms', async () => {
mockSettings.merged.codingPlan = { versionIntl: 'old-version-hash' };
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-global-1',
baseUrl: CODING_PLAN_INTL_BASE_URL,
envKey: CODING_PLAN_INTL_ENV_KEY,
},
{
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
},
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Confirm the update
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should update versionIntl with correct hash
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.versionIntl',
CODING_PLAN_INTL_VERSION,
);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
// Should show success message with Global region info
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('Global'),
}),
expect.any(Number),
);
@ -194,8 +269,82 @@ describe('useCodingPlanUpdates', () => {
expect(mockConfig.reloadModelProvidersConfig).not.toHaveBeenCalled();
});
it('should only update configs for the specific region', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const chinaConfig = {
id: 'test-model-china-1',
baseUrl: CODING_PLAN_BASE_URL,
envKey: CODING_PLAN_ENV_KEY,
};
const globalConfig = {
id: 'test-model-global-1',
baseUrl: CODING_PLAN_INTL_BASE_URL,
envKey: CODING_PLAN_INTL_ENV_KEY,
};
const customConfig = {
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [chinaConfig, globalConfig, customConfig],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Get the updated configs passed to setValue
const setValueCalls = mockSettings.setValue.mock.calls;
const modelProvidersCall = setValueCalls.find((call: unknown[]) =>
(call[1] as string).includes('modelProviders'),
);
// Should preserve Global config and custom config, only update China configs
expect(modelProvidersCall).toBeDefined();
const updatedConfigs = modelProvidersCall![2] as Array<
Record<string, unknown>
>;
// Should have new China configs + preserved Global config + custom config
expect(updatedConfigs.length).toBeGreaterThanOrEqual(3);
// Should contain the Global config (not modified)
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['id'] === 'test-model-global-1',
),
).toBe(true);
// Should contain the custom config
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['id'] === 'custom-model',
),
).toBe(true);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('should preserve non-Coding Plan configs during update', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const customConfig = {
id: 'custom-model',
@ -205,8 +354,8 @@ describe('useCodingPlanUpdates', () => {
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-1',
baseUrl: 'https://test.example.com/v1',
id: 'test-model-china-1',
baseUrl: CODING_PLAN_BASE_URL,
envKey: CODING_PLAN_ENV_KEY,
},
customConfig,
@ -233,10 +382,38 @@ describe('useCodingPlanUpdates', () => {
// Should preserve custom config - verify setValue was called
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Get the updated configs passed to setValue
const setValueCalls = mockSettings.setValue.mock.calls;
const modelProvidersCall = setValueCalls.find((call: unknown[]) =>
(call[1] as string).includes('modelProviders'),
);
// Should preserve custom config
expect(modelProvidersCall).toBeDefined();
const updatedConfigs = modelProvidersCall![2] as Array<
Record<string, unknown>
>;
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['id'] === 'custom-model',
),
).toBe(true);
});
it('should handle missing API key error', async () => {
it('should handle update errors gracefully', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-china-1',
baseUrl: CODING_PLAN_BASE_URL,
envKey: CODING_PLAN_ENV_KEY,
},
],
};
// Simulate an error during refreshAuth
mockConfig.refreshAuth.mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() =>
useCodingPlanUpdates(
@ -253,12 +430,14 @@ describe('useCodingPlanUpdates', () => {
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Should show error message
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
expect.any(Number),
);
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
expect.any(Number),
);
});
});
});

View file

@ -10,9 +10,11 @@ import { AuthType } from '@qwen-code/qwen-code-core';
import type { LoadedSettings } from '../../config/settings.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
import {
CODING_PLAN_MODELS,
CODING_PLAN_ENV_KEY,
isCodingPlanConfig,
CODING_PLAN_VERSION,
CODING_PLAN_INTL_VERSION,
getCodingPlanConfig,
CodingPlanRegion,
} from '../../constants/codingPlan.js';
import { t } from '../../i18n/index.js';
@ -21,20 +23,6 @@ export interface CodingPlanUpdateRequest {
onConfirm: (confirmed: boolean) => void;
}
/**
* Checks if a config is a Coding Plan configuration by matching baseUrl and envKey.
* This ensures only configs from the Coding Plan provider are identified.
*/
function isCodingPlanConfig(config: {
baseUrl?: string;
envKey?: string;
}): boolean {
return (
config.envKey === CODING_PLAN_ENV_KEY &&
CODING_PLAN_MODELS.some((template) => template.baseUrl === config.baseUrl)
);
}
/**
* Hook for detecting and handling Coding Plan template updates.
* Compares the persisted version with the current template version
@ -55,134 +43,161 @@ export function useCodingPlanUpdates(
/**
* Execute the Coding Plan configuration update.
* Removes old Coding Plan configs and replaces them with new ones from the template.
* Automatically detects whether the user is using China or Intl version.
*/
const executeUpdate = useCallback(async () => {
try {
const persistScope = getPersistScopeForModelSelection(settings);
const executeUpdate = useCallback(
async (region: CodingPlanRegion = CodingPlanRegion.CHINA) => {
try {
const persistScope = getPersistScopeForModelSelection(settings);
// Get current configs
const currentConfigs =
(
settings.merged.modelProviders as
| Record<string, Array<Record<string, unknown>>>
| undefined
)?.[AuthType.USE_OPENAI] || [];
// Get current configs
const currentConfigs =
(
settings.merged.modelProviders as
| Record<string, Array<Record<string, unknown>>>
| undefined
)?.[AuthType.USE_OPENAI] || [];
// Filter out Coding Plan configs (keep user custom configs)
const nonCodingPlanConfigs = currentConfigs.filter(
(cfg) =>
!isCodingPlanConfig({
baseUrl: cfg['baseUrl'] as string | undefined,
envKey: cfg['envKey'] as string | undefined,
}),
);
// Generate new configs from template with the stored API key
const apiKey = process.env[CODING_PLAN_ENV_KEY];
if (!apiKey) {
throw new Error(
t(
'Coding Plan API key not found. Please re-authenticate with Coding Plan.',
),
// Filter out Coding Plan configs for the given region (keep user custom configs)
const nonCodingPlanConfigs = currentConfigs.filter(
(cfg) =>
!isCodingPlanConfig(
cfg['baseUrl'] as string | undefined,
cfg['envKey'] as string | undefined,
region,
),
);
// Get the correct configuration based on region
const codingPlanConfig = getCodingPlanConfig(region);
const { template, envKey, version } = codingPlanConfig;
// Generate new configs from template
const newConfigs = template.map((templateConfig) => ({
...templateConfig,
envKey,
}));
// Combine: new Coding Plan configs at the front, user configs preserved
const updatedConfigs = [
...newConfigs,
...(nonCodingPlanConfigs as Array<Record<string, unknown>>),
] as Array<Record<string, unknown>>;
// Persist updated model providers
settings.setValue(
persistScope,
`modelProviders.${AuthType.USE_OPENAI}`,
updatedConfigs,
);
// Update the version with region-specific key
const versionKey =
region === CodingPlanRegion.GLOBAL
? 'codingPlan.versionIntl'
: 'codingPlan.version';
settings.setValue(persistScope, versionKey, version);
// Hot-reload model providers configuration
const updatedModelProviders = {
...(settings.merged.modelProviders as
| Record<string, unknown>
| undefined),
[AuthType.USE_OPENAI]: updatedConfigs,
};
config.reloadModelProvidersConfig(
updatedModelProviders as unknown as ModelProvidersConfig,
);
// Refresh auth with the new configuration
await config.refreshAuth(AuthType.USE_OPENAI);
const regionLabel =
region === CodingPlanRegion.GLOBAL
? 'Coding Plan (Global/Intl)'
: 'Coding Plan';
addItem(
{
type: 'info',
text: t(
'{{region}} configuration updated successfully. New models are now available.',
{ region: regionLabel },
),
},
Date.now(),
);
return true;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
addItem(
{
type: 'error',
text: t('Failed to update Coding Plan configuration: {{message}}', {
message: errorMessage,
}),
},
Date.now(),
);
return false;
}
const newConfigs = CODING_PLAN_MODELS.map((templateConfig) => ({
...templateConfig,
envKey: CODING_PLAN_ENV_KEY,
}));
// Combine: new Coding Plan configs at the front, user configs preserved
const updatedConfigs = [
...newConfigs,
...(nonCodingPlanConfigs as Array<Record<string, unknown>>),
] as Array<Record<string, unknown>>;
// Persist updated model providers
settings.setValue(
persistScope,
`modelProviders.${AuthType.USE_OPENAI}`,
updatedConfigs,
);
// Update the version
settings.setValue(
persistScope,
'codingPlan.version',
CODING_PLAN_VERSION,
);
// Hot-reload model providers configuration
const updatedModelProviders = {
...(settings.merged.modelProviders as
| Record<string, unknown>
| undefined),
[AuthType.USE_OPENAI]: updatedConfigs,
};
config.reloadModelProvidersConfig(
updatedModelProviders as unknown as ModelProvidersConfig,
);
// Refresh auth with the new configuration
await config.refreshAuth(AuthType.USE_OPENAI);
addItem(
{
type: 'info',
text: t(
'Coding Plan configuration updated successfully. New models are now available.',
),
},
Date.now(),
);
return true;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
addItem(
{
type: 'error',
text: t('Failed to update Coding Plan configuration: {{message}}', {
message: errorMessage,
}),
},
Date.now(),
);
return false;
}
}, [settings, config, addItem]);
},
[settings, config, addItem],
);
/**
* Check for version mismatch and prompt user for update if needed.
*/
const checkForUpdates = useCallback(() => {
const savedVersion = (
settings.merged as { codingPlan?: { version?: string } }
).codingPlan?.version;
const mergedSettings = settings.merged as {
codingPlan?: { version?: string; versionIntl?: string };
};
const savedChinaVersion = mergedSettings.codingPlan?.version;
const savedIntlVersion = mergedSettings.codingPlan?.versionIntl;
// Determine which version the user is using based on saved version
// Check China version first
if (savedChinaVersion) {
if (savedChinaVersion !== CODING_PLAN_VERSION) {
// China version mismatch - prompt for update
setUpdateRequest({
prompt: t(
'New model configurations are available for Bailian Coding Plan (China). Update now?',
),
onConfirm: async (confirmed: boolean) => {
setUpdateRequest(undefined);
if (confirmed) {
await executeUpdate(CodingPlanRegion.CHINA);
}
},
});
return;
}
}
// Check Intl version
if (savedIntlVersion) {
if (savedIntlVersion !== CODING_PLAN_INTL_VERSION) {
// Intl version mismatch - prompt for update
setUpdateRequest({
prompt: t(
'New model configurations are available for Coding Plan (Global/Intl). Update now?',
),
onConfirm: async (confirmed: boolean) => {
setUpdateRequest(undefined);
if (confirmed) {
await executeUpdate(CodingPlanRegion.GLOBAL);
}
},
});
return;
}
}
// If no version is stored, user hasn't used Coding Plan yet - skip check
if (!savedVersion) {
return;
}
// If versions match, no update needed
if (savedVersion === CODING_PLAN_VERSION) {
return;
}
// Version mismatch - prompt user for update
setUpdateRequest({
prompt: t(
'New model configurations are available for Bailian Coding Plan. Update now?',
),
onConfirm: async (confirmed: boolean) => {
setUpdateRequest(undefined);
if (confirmed) {
await executeUpdate();
}
},
});
return;
}, [settings, executeUpdate]);
// Check for updates on mount