Merge pull request #626 from kaitranntt/kai/fix/agy-model-reset-sonnet46

fix(cliproxy): prevent agy model reset and add sonnet 4.6 models
This commit is contained in:
Kai (Tam Nhu) Tran
2026-02-24 12:23:35 -05:00
committed by GitHub
10 changed files with 224 additions and 53 deletions
+69 -11
View File
@@ -4,6 +4,7 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderModelMapping } from '../types';
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../base-config-loader';
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
@@ -37,6 +38,14 @@ const DEPRECATED_MODEL_PREFIX = 'gemini-claude-';
const UPSTREAM_MODEL_PREFIX = 'claude-';
const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
const REQUIRED_PROVIDER_ENV_KEYS = [
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
] as const;
function stripCodexEffortSuffix(modelId: string): string {
return modelId.replace(CODEX_EFFORT_SUFFIX_REGEX, '');
@@ -405,23 +414,72 @@ export function getEffectiveEnvVars(
*/
export function ensureProviderSettings(provider: CLIProxyProvider): void {
const settingsPath = getProviderSettingsPath(provider);
const defaultEnv = getClaudeEnvVars(provider);
// Only create if doesn't exist (preserve user edits)
if (fs.existsSync(settingsPath)) {
const writeSettings = (settings: Record<string, unknown>): void => {
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', {
mode: 0o600,
});
};
// Create initial file when missing.
if (!fs.existsSync(settingsPath)) {
writeSettings({ env: defaultEnv });
return;
}
// Generate default settings from PROVIDER_CONFIGS
const envVars = getClaudeEnvVars(provider);
const settings: ProviderSettings = { env: envVars };
// Existing file: repair missing/invalid core env keys without dropping user data.
let rawContent = '';
try {
rawContent = fs.readFileSync(settingsPath, 'utf-8');
} catch {
return;
}
// Ensure directory exists
fs.mkdirSync(require('path').dirname(settingsPath), { recursive: true });
let parsed: Record<string, unknown>;
try {
const value = JSON.parse(rawContent) as unknown;
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('settings root must be an object');
}
parsed = value as Record<string, unknown>;
} catch {
// Preserve corrupt payload for manual inspection, then recover with defaults.
const backupPath = `${settingsPath}.corrupt-${Date.now()}`;
try {
fs.writeFileSync(backupPath, rawContent || '', { mode: 0o600 });
} catch {
// Best effort only.
}
writeSettings({ env: defaultEnv });
return;
}
// Write with restricted permissions
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', {
mode: 0o600,
});
const envCandidate = parsed.env;
const mergedEnv: Record<string, string> =
envCandidate && typeof envCandidate === 'object' && !Array.isArray(envCandidate)
? { ...(envCandidate as Record<string, string>) }
: {};
let mutated = !(envCandidate && typeof envCandidate === 'object' && !Array.isArray(envCandidate));
for (const key of REQUIRED_PROVIDER_ENV_KEYS) {
const current = mergedEnv[key];
if (typeof current !== 'string' || current.trim().length === 0) {
mergedEnv[key] = defaultEnv[key] || '';
mutated = true;
}
}
if (!mutated) {
return;
}
const repairedSettings: Record<string, unknown> = {
...parsed,
env: mergedEnv,
};
writeSettings(repairedSettings);
}
/**
+2
View File
@@ -54,6 +54,8 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [
{ name: 'gemini-3-pro-high', alias: 'gemini-3.1-pro-preview' },
{ 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: 'gemini-claude-sonnet-4-6', fork: true },
{ name: 'claude-sonnet-4-6-thinking', alias: 'gemini-claude-sonnet-4-6-thinking', fork: true },
{ name: 'claude-sonnet-4-5', alias: 'gemini-claude-sonnet-4-5', fork: true },
{ name: 'claude-sonnet-4-5-thinking', alias: 'gemini-claude-sonnet-4-5-thinking', fork: true },
{ name: 'claude-opus-4-5-thinking', alias: 'gemini-claude-opus-4-5-thinking', fork: true },
+18
View File
@@ -103,6 +103,24 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
dynamicAllowed: true,
},
},
{
id: 'claude-sonnet-4-6-thinking',
name: 'Claude Sonnet 4.6 Thinking',
description: 'Latest Sonnet with extended thinking',
thinking: {
type: 'budget',
min: 1024,
max: 128000,
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',
+3 -2
View File
@@ -116,8 +116,9 @@ export async function configureProviderModel(
? customSettingsPath.replace(/^~/, os.homedir())
: getProviderSettingsPath(provider);
// Skip if already configured (unless --config flag)
if (!force && fs.existsSync(settingsPath)) {
// Skip if already configured with a model (unless --config flag).
// A settings file can exist without model env keys (e.g., hook-only writes).
if (!force && getCurrentModel(provider, customSettingsPath)?.trim()) {
return false;
}
@@ -133,7 +133,8 @@ export function ensureProfileHooks(profileName: string): boolean {
warn(`Malformed ${profileName}.settings.json: ${(parseError as Error).message}`)
);
}
// Continue with empty settings, will add hooks
// Never overwrite malformed settings files; avoid destructive data loss.
return false;
}
}
+2 -1
View File
@@ -114,7 +114,8 @@ export function ensureProfileHooks(profileName: string): boolean {
warn(`Malformed ${profileName}.settings.json: ${(parseError as Error).message}`)
);
}
// Continue with empty settings, will add hooks
// Never overwrite malformed settings files; avoid destructive data loss.
return false;
}
}