Merge pull request #653 from kaitranntt/kai/fix/agy-sonnet-46-model-migration

fix(agy): normalize sonnet 4.6 model IDs and migrate legacy alias
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-03-02 11:47:06 -05:00
commit 5988524ec3
16 files changed
+621 -93

No files matched your search

+46 -8
View File
@@ -60,7 +60,11 @@ function stripCodexEffortSuffix(modelId: string): string {
*
* Returns true if migration was performed and file was updated.
*/
function migrateDeprecatedModelNames(settingsPath: string, settings: ProviderSettings): boolean {
function migrateDeprecatedModelNames(
settingsPath: string,
provider: CLIProxyProvider,
settings: ProviderSettings
): boolean {
if (!settings.env || typeof settings.env !== 'object') return false;
let migrated = false;
@@ -68,13 +72,36 @@ function migrateDeprecatedModelNames(settingsPath: string, settings: ProviderSet
const value = settings.env[key];
if (typeof value !== 'string') continue;
// Check if the base model name (before any suffixes) uses the deprecated prefix
if (value.toLowerCase().startsWith(DEPRECATED_MODEL_PREFIX)) {
settings.env[key] = UPSTREAM_MODEL_PREFIX + value.slice(DEPRECATED_MODEL_PREFIX.length);
let canonical = value;
// Check if the base model name (before any suffixes) uses the deprecated prefix.
if (canonical.toLowerCase().startsWith(DEPRECATED_MODEL_PREFIX)) {
canonical = UPSTREAM_MODEL_PREFIX + canonical.slice(DEPRECATED_MODEL_PREFIX.length);
}
canonical = normalizeModelIdForProvider(canonical, provider);
if (canonical !== value) {
settings.env[key] = canonical;
migrated = true;
}
}
if (provider === 'agy' && Array.isArray(settings.presets)) {
for (const preset of settings.presets) {
if (!preset || typeof preset !== 'object') continue;
const presetRecord = preset as Record<string, unknown>;
for (const key of PRESET_MODEL_KEYS) {
const value = presetRecord[key];
if (typeof value !== 'string') continue;
const canonical = normalizeModelIdForProvider(value, provider);
if (canonical !== value) {
presetRecord[key] = canonical;
migrated = true;
}
}
}
}
if (migrated) {
try {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
@@ -409,7 +436,7 @@ export function getEffectiveEnvVars(
if (settings.env && typeof settings.env === 'object') {
// Migrate deprecated gemini-claude-* model names if present
migrateDeprecatedModelNames(expandedPath, settings);
migrateDeprecatedModelNames(expandedPath, provider, settings);
// Migrate codex effort suffixes to canonical IDs if present
migrateCodexEffortSuffixes(expandedPath, provider, settings);
// Migrate legacy iFlow placeholders to supported model IDs
@@ -444,7 +471,7 @@ export function getEffectiveEnvVars(
if (settings.env && typeof settings.env === 'object') {
// Migrate deprecated gemini-claude-* model names if present
migrateDeprecatedModelNames(settingsPath, settings);
migrateDeprecatedModelNames(settingsPath, provider, settings);
// Migrate codex effort suffixes to canonical IDs if present
migrateCodexEffortSuffixes(settingsPath, provider, settings);
// Migrate legacy iFlow placeholders to supported model IDs
@@ -532,6 +559,17 @@ export function ensureProviderSettings(provider: CLIProxyProvider): void {
}
}
// Canonicalize provider-specific model aliases (e.g., AGY Sonnet 4.6 thinking legacy IDs).
for (const key of MODEL_ENV_VAR_KEYS) {
const current = mergedEnv[key];
if (typeof current !== 'string' || current.trim().length === 0) continue;
const canonical = normalizeModelIdForProvider(current, provider);
if (canonical !== current) {
mergedEnv[key] = canonical;
mutated = true;
}
}
if (!mutated) {
return;
}
@@ -584,7 +622,7 @@ export function getRemoteEnvVars(
const content = fs.readFileSync(expandedPath, 'utf-8');
const settings: ProviderSettings = JSON.parse(content);
if (settings.env && typeof settings.env === 'object') {
migrateDeprecatedModelNames(expandedPath, settings);
migrateDeprecatedModelNames(expandedPath, provider, settings);
migrateCodexEffortSuffixes(expandedPath, provider, settings);
migrateIFlowPlaceholderModel(expandedPath, provider, settings);
userEnvVars = settings.env as Record<string, string>;
@@ -604,7 +642,7 @@ export function getRemoteEnvVars(
const content = fs.readFileSync(settingsPath, 'utf-8');
const settings: ProviderSettings = JSON.parse(content);
if (settings.env && typeof settings.env === 'object') {
migrateDeprecatedModelNames(settingsPath, settings);
migrateDeprecatedModelNames(settingsPath, provider, settings);
migrateCodexEffortSuffixes(settingsPath, provider, settings);
migrateIFlowPlaceholderModel(settingsPath, provider, settings);
userEnvVars = settings.env as Record<string, string>;
+14 -5
View File
@@ -32,8 +32,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs';
* v8: Added Gemini 3.1 preview aliases for provider routing compatibility
* v9: Added resilient alias compatibility expansion and cache-assisted alias enrichment
* v10: Migrated deprecated gemini-claude-* aliases to upstream claude-* aliases
* v11: Migrated deprecated claude-sonnet-4-6-thinking aliases to claude-sonnet-4-6
*/
export const CLIPROXY_CONFIG_VERSION = 10;
export const CLIPROXY_CONFIG_VERSION = 11;
interface OAuthModelAliasEntry {
name: string;
@@ -44,6 +45,8 @@ interface OAuthModelAliasEntry {
const GEMINI_MINOR_COMPAT_RANGE = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
const DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX = 'gemini-claude-';
const UPSTREAM_CLAUDE_ALIAS_PREFIX = 'claude-';
const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_ALIAS = 'claude-sonnet-4-6-thinking';
const CANONICAL_ANTIGRAVITY_SONNET_46_ALIAS = 'claude-sonnet-4-6';
/**
* Default Antigravity oauth-model-alias entries.
@@ -58,7 +61,8 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [
{ name: 'gemini-3-pro-high', alias: 'gemini-3.1-pro-preview-customtools' },
{ name: 'gemini-3-flash', alias: 'gemini-3-flash-preview' },
{ name: 'claude-sonnet-4-6', alias: 'claude-sonnet-4-6', fork: true },
{ name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6-thinking', fork: true },
// Backward compatibility: legacy sonnet thinking alias now routes to canonical model ID.
{ name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6', fork: true },
{ name: 'claude-sonnet-4-5', alias: 'claude-sonnet-4-5', fork: true },
{ name: 'claude-sonnet-4-5-thinking', alias: 'claude-sonnet-4-5-thinking', fork: true },
{ name: 'claude-opus-4-5-thinking', alias: 'claude-opus-4-5-thinking', fork: true },
@@ -107,10 +111,15 @@ function sanitizeYamlScalar(rawValue: string): string {
function normalizeAntigravityAlias(rawAlias: string): string {
const normalized = sanitizeYamlScalar(rawAlias);
if (normalized.toLowerCase().startsWith(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX)) {
return (
UPSTREAM_CLAUDE_ALIAS_PREFIX + normalized.slice(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX.length)
);
const migratedPrefix =
UPSTREAM_CLAUDE_ALIAS_PREFIX + normalized.slice(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX.length);
return normalizeAntigravityAlias(migratedPrefix);
}
if (normalized.toLowerCase() === DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_ALIAS) {
return CANONICAL_ANTIGRAVITY_SONNET_46_ALIAS;
}
return normalized;
}
+4 -10
View File
@@ -104,23 +104,17 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
},
},
{
id: 'claude-sonnet-4-6-thinking',
name: 'Claude Sonnet 4.6 Thinking',
description: 'Latest Sonnet with extended thinking',
id: 'claude-sonnet-4-6',
name: 'Claude Sonnet 4.6',
description: 'Latest Sonnet with thinking budget support',
thinking: {
type: 'budget',
min: 1024,
max: 128000,
max: 64000,
zeroAllowed: true,
dynamicAllowed: true,
},
},
{
id: 'claude-sonnet-4-6',
name: 'Claude Sonnet 4.6',
description: 'Latest Sonnet baseline',
thinking: { type: 'none' },
},
{
id: 'claude-sonnet-4-5-thinking',
name: 'Claude Sonnet 4.5 Thinking',
+12 -10
View File
@@ -13,6 +13,7 @@ import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator';
import { CLIProxyProvider } from './types';
import { initUI, color, bold, dim, ok, info, header } from '../utils/ui';
import { getCcsDir } from '../utils/config-manager';
import { normalizeModelIdForProvider } from './model-id-normalizer';
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
@@ -21,9 +22,9 @@ function stripCodexEffortSuffix(model: string, provider: CLIProxyProvider): stri
return model.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
}
function normalizeCodexTierModel(provider: CLIProxyProvider, model: string): string {
if (provider !== 'codex') return model;
return stripCodexEffortSuffix(model, provider);
function canonicalizeModelForProvider(provider: CLIProxyProvider, model: string): string {
const withoutCodexSuffix = stripCodexEffortSuffix(model, provider);
return normalizeModelIdForProvider(withoutCodexSuffix, provider);
}
/**
@@ -50,7 +51,8 @@ export function getCurrentModel(
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
return settings.env?.ANTHROPIC_MODEL;
const model = settings.env?.ANTHROPIC_MODEL;
return typeof model === 'string' ? canonicalizeModelForProvider(provider, model) : model;
} catch {
return undefined;
}
@@ -134,7 +136,7 @@ export async function configureProviderModel(
// Find default index - use current model if configured, otherwise catalog default
const currentModel = getCurrentModel(provider, customSettingsPath);
const targetModel = currentModel
? stripCodexEffortSuffix(currentModel, provider)
? canonicalizeModelForProvider(provider, currentModel)
: catalog.defaultModel;
const defaultIdx = catalog.models.findIndex((m) => m.id === targetModel);
const safeDefaultIdx = defaultIdx >= 0 ? defaultIdx : 0;
@@ -157,10 +159,10 @@ export async function configureProviderModel(
// Get base env vars for defaults
const baseEnv = getClaudeEnvVars(provider);
const selectedDefaultModel = normalizeCodexTierModel(provider, selectedModel);
const selectedOpusModel = normalizeCodexTierModel(provider, selectedModel);
const selectedSonnetModel = normalizeCodexTierModel(provider, selectedModel);
const selectedHaikuModel = normalizeCodexTierModel(
const selectedDefaultModel = canonicalizeModelForProvider(provider, selectedModel);
const selectedOpusModel = canonicalizeModelForProvider(provider, selectedModel);
const selectedSonnetModel = canonicalizeModelForProvider(provider, selectedModel);
const selectedHaikuModel = canonicalizeModelForProvider(
provider,
baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || selectedModel
);
@@ -251,7 +253,7 @@ export async function showCurrentConfig(provider: CLIProxyProvider): Promise<voi
const currentModel = getCurrentModel(provider);
const settingsPath = getProviderSettingsPath(provider);
const normalizedCurrentModel = currentModel
? stripCodexEffortSuffix(currentModel, provider)
? canonicalizeModelForProvider(provider, currentModel)
: undefined;
console.error('');
+47 -5
View File
@@ -20,6 +20,10 @@ type ProviderLike = CLIProxyProvider | string | null | undefined;
const CLAUDE_DOTTED_VERSION_REGEX = /claude-(sonnet|opus|haiku)-(\d+)\.(\d+)(?=(?:$|-|\[|\(|\/))/gi;
const CLAUDE_DOTTED_THINKING_REGEX =
/claude-(sonnet|opus|haiku)-(\d+)\.(\d+)-thinking(?=(?:$|-|\[|\(|\/))/gi;
const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX =
/claude-sonnet-4(?:[.-])6-thinking(?=(?:$|-|\[|\(|\/))/gi;
const CANONICAL_ANTIGRAVITY_SONNET_46_MODEL = 'claude-sonnet-4-6';
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
/**
* Extract provider segment from `/api/provider/{provider}` request paths.
@@ -41,6 +45,17 @@ export function isAntigravityProvider(provider: ProviderLike): boolean {
return normalized === 'agy' || normalized === 'antigravity';
}
/** Whether provider maps to Codex model canonicalization rules. */
export function isCodexProvider(provider: ProviderLike): boolean {
if (typeof provider !== 'string') return false;
return provider.trim().toLowerCase() === 'codex';
}
/** Normalize Codex effort-suffixed IDs to canonical IDs. */
export function stripCodexEffortSuffix(model: string): string {
return model.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
}
/** Normalize Claude dotted major.minor IDs to hyphenated format. */
export function normalizeClaudeDottedMajorMinor(model: string): string {
return model.replace(
@@ -62,6 +77,17 @@ export function normalizeClaudeDottedThinkingMajorMinor(model: string): string {
);
}
/**
* Antigravity no longer exposes `claude-sonnet-4-6-thinking`.
* Canonicalize legacy aliases to `claude-sonnet-4-6` while preserving suffixes.
*/
export function normalizeDeprecatedAntigravityModelAliases(model: string): string {
return model.replace(
DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX,
CANONICAL_ANTIGRAVITY_SONNET_46_MODEL
);
}
/**
* Normalize model ID for a specific provider.
* Antigravity requires hyphenated Claude major.minor model IDs.
@@ -76,7 +102,18 @@ export function normalizeClaudeDottedThinkingMajorMinor(model: string): string {
*/
export function normalizeModelIdForProvider(model: string, provider: ProviderLike): string {
if (!isAntigravityProvider(provider)) return model;
return normalizeClaudeDottedMajorMinor(model);
const normalizedDottedVersion = normalizeClaudeDottedMajorMinor(model);
return normalizeDeprecatedAntigravityModelAliases(normalizedDottedVersion);
}
/**
* Canonicalize model ID for provider-specific compatibility.
* - Codex: strip effort suffixes.
* - Antigravity: normalize dotted/historical aliases.
*/
export function canonicalizeModelIdForProvider(model: string, provider: ProviderLike): string {
const withoutCodexSuffix = isCodexProvider(provider) ? stripCodexEffortSuffix(model) : model;
return normalizeModelIdForProvider(withoutCodexSuffix, provider);
}
/**
@@ -87,7 +124,7 @@ export function normalizeModelIdForProvider(model: string, provider: ProviderLik
*
* @example
* normalizeModelIdForRouting('claude-sonnet-4.6-thinking', null)
* // => 'claude-sonnet-4-6-thinking'
* // => 'claude-sonnet-4-6'
*
* @example
* normalizeModelIdForRouting('claude-sonnet-4.6', null)
@@ -95,9 +132,14 @@ export function normalizeModelIdForProvider(model: string, provider: ProviderLik
*/
export function normalizeModelIdForRouting(model: string, provider: ProviderLike): string {
if (isAntigravityProvider(provider)) {
return normalizeClaudeDottedMajorMinor(model);
return normalizeModelIdForProvider(model, provider);
}
return normalizeClaudeDottedThinkingMajorMinor(model);
// Explicit non-AGY provider routes should pass through unchanged.
if (typeof provider === 'string' && provider.trim().length > 0) {
return model;
}
const normalizedThinking = normalizeClaudeDottedThinkingMajorMinor(model);
return normalizeDeprecatedAntigravityModelAliases(normalizedThinking);
}
/**
@@ -109,7 +151,7 @@ export function normalizeModelIdForRouting(model: string, provider: ProviderLike
* { ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking' },
* 'agy'
* )
* // => { ANTHROPIC_MODEL: 'claude-sonnet-4-6-thinking' }
* // => { ANTHROPIC_MODEL: 'claude-sonnet-4-6' }
*/
export function normalizeModelEnvVarsForProvider(
envVars: NodeJS.ProcessEnv,
+5 -2
View File
@@ -18,6 +18,7 @@ import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector'
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { getEffectiveApiKey } from '../auth-token-manager';
import { warn } from '../../utils/ui';
import { normalizeModelIdForProvider } from '../model-id-normalizer';
/** Environment settings structure */
interface SettingsEnv {
@@ -41,8 +42,10 @@ function canonicalizeModelForProvider(
provider: CLIProxyProfileName | undefined,
model: string
): string {
if (provider !== 'codex') return model;
return model.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
const withoutCodexSuffix =
provider === 'codex' ? model.replace(CODEX_EFFORT_SUFFIX_REGEX, '') : model;
if (!provider) return withoutCodexSuffix;
return normalizeModelIdForProvider(withoutCodexSuffix, provider);
}
/**
+35 -4
View File
@@ -51,6 +51,10 @@ import {
} from '../../cliproxy/platform-detector';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { CLIPROXY_DEFAULT_PORT } from '../../cliproxy/config/port-manager';
import {
MODEL_ENV_VAR_KEYS,
canonicalizeModelIdForProvider,
} from '../../cliproxy/model-id-normalizer';
const router = Router();
@@ -596,16 +600,43 @@ router.put('/models/:provider', async (req: Request, res: Response): Promise<voi
}
// Read and update settings
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
settings.env = settings.env || {};
settings.env.ANTHROPIC_MODEL = model;
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env?: Record<string, unknown>;
[key: string]: unknown;
};
const canonicalModel = canonicalizeModelIdForProvider(model, provider);
const env =
settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)
? settings.env
: {};
const previousDefault =
typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : canonicalModel;
const previousCanonicalDefault = canonicalizeModelIdForProvider(previousDefault, provider);
for (const key of MODEL_ENV_VAR_KEYS) {
if (key === 'ANTHROPIC_MODEL') {
env[key] = canonicalModel;
continue;
}
const current = env[key];
if (typeof current !== 'string') {
env[key] = canonicalModel;
continue;
}
const canonicalCurrent = canonicalizeModelIdForProvider(current, provider);
env[key] = canonicalCurrent === previousCanonicalDefault ? canonicalModel : canonicalCurrent;
}
settings.env = env;
// Write atomically
const tempPath = settingsPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
fs.renameSync(tempPath, settingsPath);
res.json({ success: true, provider, model });
res.json({ success: true, provider, model: canonicalModel });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
+30 -15
View File
@@ -25,9 +25,11 @@ import {
mutateUnifiedConfig,
} from '../../config/unified-config-loader';
import type { Settings } from '../../types/config';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { mapExternalProviderName } from '../../cliproxy/provider-capabilities';
import { canonicalizeModelIdForProvider } from '../../cliproxy/model-id-normalizer';
const router = Router();
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
const MODEL_ENV_KEYS = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
@@ -122,18 +124,30 @@ function resolveSettingsPath(profileOrVariant: string): string {
return path.join(ccsDir, `${profileOrVariant}.settings.json`);
}
function isCodexProfile(profileOrVariant: string): boolean {
if (profileOrVariant.toLowerCase() === 'codex') return true;
function resolveProviderForProfile(profileOrVariant: string): CLIProxyProvider | null {
const directProvider = mapExternalProviderName(profileOrVariant);
if (directProvider) {
return directProvider;
}
const variants = listVariants();
return variants[profileOrVariant]?.provider === 'codex';
const variantProvider = variants[profileOrVariant]?.provider;
if (typeof variantProvider === 'string') {
return mapExternalProviderName(variantProvider);
}
return null;
}
function stripCodexEffortSuffix(modelId: string): string {
return modelId.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
function canonicalizeProfileModelId(profileOrVariant: string, modelId: string): string {
const provider = resolveProviderForProfile(profileOrVariant);
if (!provider) return modelId;
return canonicalizeModelIdForProvider(modelId, provider);
}
function canonicalizeCodexSettings(profileOrVariant: string, settings: Settings): Settings {
if (!isCodexProfile(profileOrVariant)) return settings;
function canonicalizeProfileSettings(profileOrVariant: string, settings: Settings): Settings {
const provider = resolveProviderForProfile(profileOrVariant);
if (!provider) return settings;
let changed = false;
const next: Settings = { ...settings };
@@ -143,7 +157,7 @@ function canonicalizeCodexSettings(profileOrVariant: string, settings: Settings)
for (const key of MODEL_ENV_KEYS) {
const value = env[key];
if (typeof value !== 'string') continue;
const canonical = stripCodexEffortSuffix(value);
const canonical = canonicalizeModelIdForProvider(value, provider);
if (canonical !== value) {
env[key] = canonical;
changed = true;
@@ -159,7 +173,8 @@ function canonicalizeCodexSettings(profileOrVariant: string, settings: Settings)
for (const key of PRESET_MODEL_KEYS) {
const value = normalizedPreset[key];
const canonical = stripCodexEffortSuffix(value);
if (typeof value !== 'string') continue;
const canonical = canonicalizeModelIdForProvider(value, provider);
if (canonical !== value) {
normalizedPreset[key] = canonical;
presetChanged = true;
@@ -206,7 +221,7 @@ router.get('/:profile', (req: Request, res: Response): void => {
}
const stat = fs.statSync(settingsPath);
const settings = canonicalizeCodexSettings(profile, loadSettings(settingsPath));
const settings = canonicalizeProfileSettings(profile, loadSettings(settingsPath));
const masked = maskApiKeys(settings);
res.json({
@@ -234,7 +249,7 @@ router.get('/:profile/raw', (req: Request, res: Response): void => {
}
const stat = fs.statSync(settingsPath);
const settings = canonicalizeCodexSettings(profile, loadSettings(settingsPath));
const settings = canonicalizeProfileSettings(profile, loadSettings(settingsPath));
res.json({
profile,
@@ -270,7 +285,7 @@ router.put('/:profile', (req: Request, res: Response): void => {
return;
}
const normalizedSettings = canonicalizeCodexSettings(profile, settings as Settings);
const normalizedSettings = canonicalizeProfileSettings(profile, settings as Settings);
// Deduplicate CCS hooks to prevent accumulation (fixes #450)
// This handles cases where duplicate hooks were added by previous versions
@@ -355,7 +370,7 @@ router.get('/:profile/presets', (req: Request, res: Response): void => {
return;
}
const settings = canonicalizeCodexSettings(profile, loadSettings(settingsPath));
const settings = canonicalizeProfileSettings(profile, loadSettings(settingsPath));
res.json({ presets: settings.presets || [] });
} catch (error) {
respondInternalError(res, error, 'Internal server error.');
@@ -393,7 +408,7 @@ router.post('/:profile/presets', (req: Request, res: Response): void => {
}
const normalizePresetModel = (modelId: string): string =>
isCodexProfile(profile) ? stripCodexEffortSuffix(modelId) : modelId;
canonicalizeProfileModelId(profile, modelId);
const normalizedDefaultModel = normalizePresetModel(defaultModel);
const normalizedOpusModel = normalizePresetModel(opus || defaultModel);
+2 -2
View File
@@ -792,8 +792,8 @@ oauth-model-alias:
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
assert(
newConfig.includes('alias: claude-sonnet-4-6-thinking'),
'Should include normalized upstream Claude alias'
newConfig.includes('alias: claude-sonnet-4-6'),
'Should migrate deprecated sonnet thinking alias to canonical model ID'
);
assert(
!newConfig.includes('alias: gemini-claude-sonnet-4-6-thinking'),
@@ -105,10 +105,17 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
});
const env = getEffectiveEnvVars('agy', 8317, settingsPath);
expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6-thinking');
expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6-thinking');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
env: Record<string, string>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
});
it('migrates codex preset model mappings to canonical IDs', () => {
@@ -222,6 +229,39 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBeDefined();
});
it('migrates deprecated agy sonnet 4.6 thinking IDs during ensureProviderSettings', () => {
process.env.CCS_HOME = tempHome;
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
fs.writeFileSync(
agySettingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6-thinking(8192)',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.6-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-sonnet-4-5',
},
},
null,
2
)
);
ensureProviderSettings('agy');
const repaired = JSON.parse(fs.readFileSync(agySettingsPath, 'utf-8')) as {
env?: Record<string, string>;
};
expect(repaired.env?.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(8192)');
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-5');
});
it('recovers malformed provider settings files by writing defaults and backup copy', () => {
process.env.CCS_HOME = tempHome;
const agySettingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
+2 -11
View File
@@ -85,15 +85,6 @@ describe('Model Catalog', () => {
assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.5 Thinking');
});
it('includes Claude Sonnet 4.6 Thinking', () => {
const { MODEL_CATALOG } = modelCatalog;
const sonnetThinking = MODEL_CATALOG.agy.models.find(
(m) => m.id === 'claude-sonnet-4-6-thinking'
);
assert(sonnetThinking, 'Should include Claude Sonnet 4.6 Thinking');
assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.6 Thinking');
});
it('includes Claude Sonnet 4.6', () => {
const { MODEL_CATALOG } = modelCatalog;
const sonnet = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-6');
@@ -117,9 +108,9 @@ describe('Model Catalog', () => {
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier');
});
it('has 7 models total', () => {
it('has 6 models total', () => {
const { MODEL_CATALOG } = modelCatalog;
assert.strictEqual(MODEL_CATALOG.agy.models.length, 7);
assert.strictEqual(MODEL_CATALOG.agy.models.length, 6);
});
});
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'bun:test';
import {
canonicalizeModelIdForProvider,
extractProviderFromPathname,
isAntigravityProvider,
normalizeClaudeDottedMajorMinor,
@@ -44,13 +45,22 @@ describe('model-id-normalizer', () => {
it('applies provider-aware routing normalization', () => {
expect(normalizeModelIdForRouting('claude-sonnet-4.6-thinking', null)).toBe(
'claude-sonnet-4-6-thinking'
'claude-sonnet-4-6'
);
expect(normalizeModelIdForRouting('claude-sonnet-4.6', null)).toBe('claude-sonnet-4.6');
expect(normalizeModelIdForRouting('claude-sonnet-4.6', 'agy')).toBe('claude-sonnet-4-6');
expect(normalizeModelIdForRouting('claude-sonnet-4-6-thinking', 'claude')).toBe(
'claude-sonnet-4-6-thinking'
);
expect(normalizeModelIdForRouting('claude-sonnet-4.6-thinking', 'claude')).toBe(
'claude-sonnet-4.6-thinking'
);
});
it('applies provider-only normalization for antigravity', () => {
expect(normalizeModelIdForProvider('claude-sonnet-4.6-thinking', 'agy')).toBe(
'claude-sonnet-4-6'
);
expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'agy')).toBe(
'claude-opus-4-6-thinking'
);
@@ -58,6 +68,16 @@ describe('model-id-normalizer', () => {
'claude-opus-4.6-thinking'
);
});
it('applies provider canonicalization for codex and antigravity', () => {
expect(canonicalizeModelIdForProvider('gpt-5.3-codex-xhigh', 'codex')).toBe('gpt-5.3-codex');
expect(canonicalizeModelIdForProvider('claude-sonnet-4.6-thinking', 'agy')).toBe(
'claude-sonnet-4-6'
);
expect(canonicalizeModelIdForProvider('claude-sonnet-4-6-thinking', 'claude')).toBe(
'claude-sonnet-4-6-thinking'
);
});
});
describe('env normalization', () => {
@@ -71,7 +91,7 @@ describe('model-id-normalizer', () => {
};
const normalized = normalizeModelEnvVarsForProvider(input, 'agy');
expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6-thinking');
expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(normalized.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
@@ -201,9 +201,7 @@ describe('ToolSanitizationProxy Integration', () => {
});
expect(lastRequest).not.toBeNull();
expect((lastRequest!.body as Record<string, unknown>).model).toBe(
'claude-sonnet-4-6-thinking'
);
expect((lastRequest!.body as Record<string, unknown>).model).toBe('claude-sonnet-4-6');
} finally {
proxy.stop();
}
@@ -255,6 +253,31 @@ describe('ToolSanitizationProxy Integration', () => {
}
});
it('does not rewrite AGY legacy aliases on non-AGY provider routes', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
await fetch(`http://127.0.0.1:${port}/api/provider/claude/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-6-thinking',
messages: [{ role: 'user', content: 'test' }],
}),
});
expect(lastRequest).not.toBeNull();
expect((lastRequest!.body as Record<string, unknown>).model).toBe(
'claude-sonnet-4-6-thinking'
);
} finally {
proxy.stop();
}
});
it('preserves other tool properties during sanitization', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
@@ -0,0 +1,123 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import express from 'express';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
import cliproxyStatsRoutes from '../../../src/web-server/routes/cliproxy-stats-routes';
function writeSettings(filePath: string, env: Record<string, string>): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify({ env }, null, 2) + '\n');
}
describe('cliproxy-stats-routes model update canonicalization', () => {
let server: Server;
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use('/api/cliproxy', cliproxyStatsRoutes);
await new Promise<void>((resolve, reject) => {
server = app.listen(0, '127.0.0.1');
const onError = (error: Error) => reject(error);
server.once('error', onError);
server.once('listening', () => {
server.off('error', onError);
resolve();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
baseUrl = `http://127.0.0.1:${address.port}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-model-route-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('updates linked tiers while preserving intentionally distinct tier models', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'cliproxy', 'agy.settings.json');
writeSettings(settingsPath, {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5',
});
const response = await fetch(`${baseUrl}/api/cliproxy/models/agy`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'claude-sonnet-4-6-thinking(8192)' }),
});
expect(response.status).toBe(200);
const body = (await response.json()) as { model: string };
expect(body.model).toBe('claude-sonnet-4-6(8192)');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(8192)');
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6(8192)');
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
});
it('canonicalizes Codex effort suffix and syncs linked core model env vars', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'cliproxy', 'codex.settings.json');
writeSettings(settingsPath, {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'gpt-5.3-codex',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini',
});
const response = await fetch(`${baseUrl}/api/cliproxy/models/codex`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'gpt-5.3-codex-xhigh' }),
});
expect(response.status).toBe(200);
const body = (await response.json()) as { model: string };
expect(body.model).toBe('gpt-5.3-codex');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex');
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
});
});
@@ -0,0 +1,208 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import express from 'express';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
import settingsRoutes from '../../../src/web-server/routes/settings-routes';
function writeSettings(filePath: string, value: Record<string, unknown>): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
}
describe('settings-routes model canonicalization', () => {
let server: Server;
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use('/api/settings', settingsRoutes);
await new Promise<void>((resolve, reject) => {
server = app.listen(0, '127.0.0.1');
const onError = (error: Error) => reject(error);
server.once('error', onError);
server.once('listening', () => {
server.off('error', onError);
resolve();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
baseUrl = `http://127.0.0.1:${address.port}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-settings-routes-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('canonicalizes AGY env and preset model IDs on PUT /:profile', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
writeSettings(settingsPath, { env: {} });
const response = await fetch(`${baseUrl}/api/settings/agy`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settings: {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6-thinking(8192)',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.6-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5',
},
presets: [
{
name: 'legacy',
default: 'claude-sonnet-4-6-thinking',
opus: 'claude-opus-4.6-thinking',
sonnet: 'claude-sonnet-4.6-thinking',
haiku: 'claude-haiku-4.5',
},
],
},
}),
});
expect(response.status).toBe(200);
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
presets: Array<Record<string, string>>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(8192)');
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
expect(persisted.presets[0]?.default).toBe('claude-sonnet-4-6');
expect(persisted.presets[0]?.opus).toBe('claude-opus-4-6-thinking');
expect(persisted.presets[0]?.sonnet).toBe('claude-sonnet-4-6');
expect(persisted.presets[0]?.haiku).toBe('claude-haiku-4-5');
});
it('canonicalizes AGY preset values on POST /:profile/presets', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
writeSettings(settingsPath, {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
presets: [],
});
const response = await fetch(`${baseUrl}/api/settings/agy/presets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'new-legacy',
default: 'claude-sonnet-4-6-thinking',
opus: 'claude-opus-4.6-thinking',
sonnet: 'claude-sonnet-4.6-thinking',
haiku: 'claude-haiku-4.5',
}),
});
expect(response.status).toBe(201);
const body = (await response.json()) as { preset: Record<string, string> };
expect(body.preset.default).toBe('claude-sonnet-4-6');
expect(body.preset.opus).toBe('claude-opus-4-6-thinking');
expect(body.preset.sonnet).toBe('claude-sonnet-4-6');
expect(body.preset.haiku).toBe('claude-haiku-4-5');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
presets: Array<Record<string, string>>;
};
expect(persisted.presets[0]?.default).toBe('claude-sonnet-4-6');
expect(persisted.presets[0]?.opus).toBe('claude-opus-4-6-thinking');
expect(persisted.presets[0]?.sonnet).toBe('claude-sonnet-4-6');
expect(persisted.presets[0]?.haiku).toBe('claude-haiku-4-5');
});
it('canonicalizes AGY aliases via provider alias profile names', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'antigravity.settings.json');
writeSettings(settingsPath, { env: {} });
const response = await fetch(`${baseUrl}/api/settings/antigravity`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settings: {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5',
},
presets: [],
},
}),
});
expect(response.status).toBe(200);
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
});
it('does not crash on malformed non-string preset model values', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
writeSettings(settingsPath, {
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
},
presets: [
{
name: 'malformed',
default: 123,
opus: null,
sonnet: 'claude-sonnet-4.6-thinking',
haiku: 'claude-haiku-4.5',
},
],
});
const response = await fetch(`${baseUrl}/api/settings/agy/raw`);
expect(response.status).toBe(200);
const body = (await response.json()) as {
settings: { presets: Array<Record<string, unknown>> };
};
expect(body.settings.presets[0]?.default).toBe(123);
expect(body.settings.presets[0]?.sonnet).toBe('claude-sonnet-4-6');
});
});
+3 -14
View File
@@ -22,7 +22,7 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
presetMapping: {
default: 'claude-opus-4-6-thinking',
opus: 'claude-opus-4-6-thinking',
sonnet: 'claude-sonnet-4-6-thinking',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-sonnet-4-6',
},
},
@@ -33,25 +33,14 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
presetMapping: {
default: 'claude-opus-4-5-thinking',
opus: 'claude-opus-4-5-thinking',
sonnet: 'claude-sonnet-4-6-thinking',
haiku: 'claude-sonnet-4-6',
},
},
{
id: 'claude-sonnet-4-6-thinking',
name: 'Claude Sonnet 4.6 Thinking',
description: 'Latest Sonnet with extended thinking',
presetMapping: {
default: 'claude-sonnet-4-6-thinking',
opus: 'claude-opus-4-6-thinking',
sonnet: 'claude-sonnet-4-6-thinking',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-sonnet-4-6',
},
},
{
id: 'claude-sonnet-4-6',
name: 'Claude Sonnet 4.6',
description: 'Latest Sonnet baseline',
description: 'Latest Sonnet with thinking budget support',
presetMapping: {
default: 'claude-sonnet-4-6',
opus: 'claude-opus-4-6-thinking',