fix(agy): normalize sonnet 4.6 model ids and migrate legacy thinking alias

This commit is contained in:
Tam Nhu Tran
2026-03-02 21:10:49 +07:00
parent 8c6913b0ed
commit 8a2b55449b
10 changed files with 96 additions and 62 deletions
+35 -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
@@ -584,7 +611,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 +631,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',
+21 -5
View File
@@ -20,6 +20,9 @@ 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';
/**
* Extract provider segment from `/api/provider/{provider}` request paths.
@@ -62,6 +65,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 +90,8 @@ 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);
}
/**
@@ -87,7 +102,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 +110,10 @@ 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);
const normalizedThinking = normalizeClaudeDottedThinkingMajorMinor(model);
return normalizeDeprecatedAntigravityModelAliases(normalizedThinking);
}
/**
@@ -109,7 +125,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,
+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', () => {
+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);
});
});
@@ -44,13 +44,16 @@ 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');
});
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'
);
@@ -71,7 +74,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();
}
+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',