mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(agy): normalize sonnet 4.6 model ids and migrate legacy thinking alias
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user