diff --git a/config/base-iflow.settings.json b/config/base-iflow.settings.json index 2fc56b47..4d9c3ba2 100644 --- a/config/base-iflow.settings.json +++ b/config/base-iflow.settings.json @@ -2,9 +2,9 @@ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/iflow", "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", - "ANTHROPIC_MODEL": "deepseek-v3.2", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k2-thinking", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v3.2", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "minimax-m2" + "ANTHROPIC_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder-plus", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder-plus" } } diff --git a/src/cliproxy/config/env-builder.ts b/src/cliproxy/config/env-builder.ts index 5347962d..f90a0691 100644 --- a/src/cliproxy/config/env-builder.ts +++ b/src/cliproxy/config/env-builder.ts @@ -37,6 +37,8 @@ const DEPRECATED_MODEL_PREFIX = 'gemini-claude-'; /** Replacement prefix matching actual upstream model names */ const UPSTREAM_MODEL_PREFIX = 'claude-'; const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; +const IFLOW_PLACEHOLDER_MODEL = 'iflow-default'; +const IFLOW_DEFAULT_MODEL = 'qwen3-coder-plus'; const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const; const REQUIRED_PROVIDER_ENV_KEYS = [ 'ANTHROPIC_BASE_URL', @@ -136,6 +138,61 @@ function migrateCodexEffortSuffixes( return migrated; } +/** + * Migrate legacy iFlow placeholder model IDs to a real default model. + * Example: iflow-default -> qwen3-coder-plus + */ +function migrateIFlowPlaceholderModel( + settingsPath: string, + provider: CLIProxyProvider, + settings: ProviderSettings +): boolean { + if (provider !== 'iflow') return false; + if (!settings.env || typeof settings.env !== 'object') return false; + + let migrated = false; + const normalize = (value: string): string => value.trim().toLowerCase(); + const replaceIfPlaceholder = (value: string): string => + normalize(value) === IFLOW_PLACEHOLDER_MODEL ? IFLOW_DEFAULT_MODEL : value; + + for (const key of MODEL_ENV_VAR_KEYS) { + const value = settings.env[key]; + if (typeof value !== 'string') continue; + const replaced = replaceIfPlaceholder(value); + if (replaced !== value) { + settings.env[key] = replaced; + migrated = true; + } + } + + if (Array.isArray(settings.presets)) { + for (const preset of settings.presets) { + if (!preset || typeof preset !== 'object') continue; + const presetRecord = preset as Record; + + for (const key of PRESET_MODEL_KEYS) { + const value = presetRecord[key]; + if (typeof value !== 'string') continue; + const replaced = replaceIfPlaceholder(value); + if (replaced !== value) { + presetRecord[key] = replaced; + migrated = true; + } + } + } + } + + if (migrated) { + try { + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 }); + } catch { + // Best-effort migration — don't block startup if write fails + } + } + + return migrated; +} + /** Remote proxy configuration for URL rewriting */ export interface RemoteProxyRewriteConfig { host: string; @@ -355,6 +412,8 @@ export function getEffectiveEnvVars( migrateDeprecatedModelNames(expandedPath, settings); // Migrate codex effort suffixes to canonical IDs if present migrateCodexEffortSuffixes(expandedPath, provider, settings); + // Migrate legacy iFlow placeholders to supported model IDs + migrateIFlowPlaceholderModel(expandedPath, provider, settings); // Custom variant settings found - merge with global env envVars = { ...globalEnv, ...settings.env }; // Ensure required vars are present (fall back to defaults if missing) @@ -388,6 +447,8 @@ export function getEffectiveEnvVars( migrateDeprecatedModelNames(settingsPath, settings); // Migrate codex effort suffixes to canonical IDs if present migrateCodexEffortSuffixes(settingsPath, provider, settings); + // Migrate legacy iFlow placeholders to supported model IDs + migrateIFlowPlaceholderModel(settingsPath, provider, settings); // User override found - merge with global env envVars = { ...globalEnv, ...settings.env }; // Ensure required vars are present (fall back to defaults if missing) @@ -525,6 +586,7 @@ export function getRemoteEnvVars( if (settings.env && typeof settings.env === 'object') { migrateDeprecatedModelNames(expandedPath, settings); migrateCodexEffortSuffixes(expandedPath, provider, settings); + migrateIFlowPlaceholderModel(expandedPath, provider, settings); userEnvVars = settings.env as Record; } } catch { @@ -544,6 +606,7 @@ export function getRemoteEnvVars( if (settings.env && typeof settings.env === 'object') { migrateDeprecatedModelNames(settingsPath, settings); migrateCodexEffortSuffixes(settingsPath, provider, settings); + migrateIFlowPlaceholderModel(settingsPath, provider, settings); userEnvVars = settings.env as Record; } } catch { diff --git a/tests/unit/cliproxy/env-builder-provider-url.test.ts b/tests/unit/cliproxy/env-builder-provider-url.test.ts index 63efe2c7..9c6f9002 100644 --- a/tests/unit/cliproxy/env-builder-provider-url.test.ts +++ b/tests/unit/cliproxy/env-builder-provider-url.test.ts @@ -146,6 +146,51 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { expect(persisted.presets[0]?.haiku).toBe('gpt-5-mini'); }); + it('migrates iflow placeholder model IDs to a supported default', () => { + const iflowSettingsPath = path.join(tempHome, 'iflow.settings.json'); + writeSettings( + iflowSettingsPath, + { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/iflow', + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', + ANTHROPIC_MODEL: 'iflow-default', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'iflow-default', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'iflow-default', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'iflow-default', + }, + { + presets: [ + { + name: 'legacy-iflow', + default: 'iflow-default', + opus: 'iflow-default', + sonnet: 'iflow-default', + haiku: 'iflow-default', + }, + ], + } + ); + + const env = getEffectiveEnvVars('iflow', 8317, iflowSettingsPath); + expect(env.ANTHROPIC_MODEL).toBe('qwen3-coder-plus'); + expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus'); + expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('qwen3-coder-plus'); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('qwen3-coder-plus'); + + const persisted = JSON.parse(fs.readFileSync(iflowSettingsPath, 'utf-8')) as { + env: Record; + presets: Array>; + }; + expect(persisted.env.ANTHROPIC_MODEL).toBe('qwen3-coder-plus'); + expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('qwen3-coder-plus'); + expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('qwen3-coder-plus'); + expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('qwen3-coder-plus'); + expect(persisted.presets[0]?.default).toBe('qwen3-coder-plus'); + expect(persisted.presets[0]?.opus).toBe('qwen3-coder-plus'); + expect(persisted.presets[0]?.sonnet).toBe('qwen3-coder-plus'); + expect(persisted.presets[0]?.haiku).toBe('qwen3-coder-plus'); + }); + it('repairs existing provider settings files that are missing env keys', () => { process.env.CCS_HOME = tempHome; const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json'); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 3e398d05..0a8e98f3 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -288,12 +288,58 @@ export const MODEL_CATALOGS: Record = { iflow: { provider: 'iflow', displayName: 'iFlow', - defaultModel: 'iflow-default', + defaultModel: 'qwen3-coder-plus', models: [ { - id: 'iflow-default', - name: 'iFlow Default', - description: 'Default iFlow model', + id: 'qwen3-coder-plus', + name: 'Qwen3 Coder Plus', + description: 'Recommended default for iFlow accounts', + presetMapping: { + default: 'qwen3-coder-plus', + opus: 'qwen3-coder-plus', + sonnet: 'qwen3-coder-plus', + haiku: 'qwen3-coder-plus', + }, + }, + { + id: 'qwen3-max', + name: 'Qwen3 Max', + description: 'Flagship Qwen model via iFlow', + }, + { + id: 'kimi-k2.5', + name: 'Kimi K2.5', + description: 'Latest Kimi model via iFlow', + }, + { + id: 'kimi-k2', + name: 'Kimi K2', + description: 'Kimi general model', + }, + { + id: 'deepseek-v3.2-chat', + name: 'DeepSeek V3.2 Chat', + description: 'Stable DeepSeek chat model', + }, + { + id: 'deepseek-r1', + name: 'DeepSeek R1', + description: 'Reasoning-focused DeepSeek model', + }, + { + id: 'glm-4.7', + name: 'GLM 4.7', + description: 'Zhipu GLM 4.7 via iFlow', + }, + { + id: 'minimax-m2.5', + name: 'MiniMax M2.5', + description: 'MiniMax M2.5 via iFlow', + }, + { + id: 'qwen3-vl-plus', + name: 'Qwen3 VL Plus', + description: 'Vision-language model', }, ], },