mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(cliproxy): prevent agy model reset and add sonnet 4.6 models
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,7 @@ describe('Config Generator', () => {
|
||||
assert(!configLine.includes('\\'), 'Should not contain backslashes');
|
||||
|
||||
// Verify it's valid YAML format
|
||||
assert.strictEqual(
|
||||
configLine,
|
||||
'auth-dir: "C:/Users/TestUser/.ccs/cliproxy/auth"'
|
||||
);
|
||||
assert.strictEqual(configLine, 'auth-dir: "C:/Users/TestUser/.ccs/cliproxy/auth"');
|
||||
});
|
||||
|
||||
it('handles multiple consecutive backslashes', () => {
|
||||
@@ -79,7 +76,10 @@ describe('Config Generator', () => {
|
||||
const complexPath = 'D:\\Projects\\ccs-2024\\test\\.ccs\\auth-tokens\\provider.json';
|
||||
const normalizedPath = complexPath.replace(/\\/g, '/');
|
||||
|
||||
assert.strictEqual(normalizedPath, 'D:/Projects/ccs-2024/test/.ccs/auth-tokens/provider.json');
|
||||
assert.strictEqual(
|
||||
normalizedPath,
|
||||
'D:/Projects/ccs-2024/test/.ccs/auth-tokens/provider.json'
|
||||
);
|
||||
assert(normalizedPath.includes('Projects'), 'Should preserve directory names');
|
||||
assert(normalizedPath.includes('auth-tokens'), 'Should preserve hyphens');
|
||||
assert(normalizedPath.includes('provider.json'), 'Should preserve filenames');
|
||||
@@ -348,7 +348,8 @@ auth-dir: "/test"
|
||||
});
|
||||
|
||||
it('handles tabs in indentation', () => {
|
||||
const configContent = 'api-keys:\n\t- "ccs-internal-managed"\n\t- "user-key-with-tab"\n\nauth-dir: "/test"';
|
||||
const configContent =
|
||||
'api-keys:\n\t- "ccs-internal-managed"\n\t- "user-key-with-tab"\n\nauth-dir: "/test"';
|
||||
|
||||
const userKeys = parseUserApiKeys(configContent);
|
||||
|
||||
@@ -544,11 +545,7 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
|
||||
testPaths.forEach(({ input, expected }) => {
|
||||
const normalized = input.replace(/\\/g, '/');
|
||||
assert.strictEqual(
|
||||
normalized,
|
||||
expected,
|
||||
`Should normalize "${input}" to "${expected}"`
|
||||
);
|
||||
assert.strictEqual(normalized, expected, `Should normalize "${input}" to "${expected}"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -588,6 +585,7 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
|
||||
// Claude aliases should have fork: true
|
||||
assert(config.includes('claude-sonnet-4-5'), 'Should include Claude sonnet model');
|
||||
assert(config.includes('claude-sonnet-4-6'), 'Should include Claude Sonnet 4.6 model');
|
||||
assert(config.includes('fork: true'), 'Should include fork: true for Claude aliases');
|
||||
|
||||
// Verify fork: true appears after each Claude alias entry
|
||||
@@ -650,8 +648,14 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
|
||||
assert(config.includes('alias: gemini-3.1-pro-preview'), 'Should include dotted version alias');
|
||||
assert(config.includes('alias: gemini-3-1-pro-preview'), 'Should include hyphen version alias');
|
||||
assert(
|
||||
config.includes('alias: gemini-3.1-pro-preview'),
|
||||
'Should include dotted version alias'
|
||||
);
|
||||
assert(
|
||||
config.includes('alias: gemini-3-1-pro-preview'),
|
||||
'Should include hyphen version alias'
|
||||
);
|
||||
assert(
|
||||
config.includes('alias: gemini-3-1-pro-preview-customtools'),
|
||||
'Should include hyphen customtools alias'
|
||||
|
||||
@@ -2,7 +2,10 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { getEffectiveEnvVars } from '../../../src/cliproxy/config/env-builder';
|
||||
import {
|
||||
ensureProviderSettings,
|
||||
getEffectiveEnvVars,
|
||||
} from '../../../src/cliproxy/config/env-builder';
|
||||
|
||||
interface EnvSettings {
|
||||
ANTHROPIC_BASE_URL: string;
|
||||
@@ -24,13 +27,16 @@ function writeSettings(
|
||||
describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
let tempHome: string;
|
||||
let settingsPath: string;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-env-url-'));
|
||||
settingsPath = path.join(tempHome, 'codex.settings.json');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -139,4 +145,54 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
expect(persisted.presets[0]?.sonnet).toBe('gpt-5.3-codex');
|
||||
expect(persisted.presets[0]?.haiku).toBe('gpt-5-mini');
|
||||
});
|
||||
|
||||
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');
|
||||
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
agySettingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
hooks: {
|
||||
PreToolUse: [{ matcher: 'WebSearch', hooks: [] }],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
ensureProviderSettings('agy');
|
||||
|
||||
const repaired = JSON.parse(fs.readFileSync(agySettingsPath, 'utf-8')) as {
|
||||
env?: Record<string, string>;
|
||||
hooks?: Record<string, unknown>;
|
||||
};
|
||||
expect(repaired.hooks?.PreToolUse).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8317/api/provider/agy');
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBeDefined();
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBeDefined();
|
||||
});
|
||||
|
||||
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');
|
||||
fs.mkdirSync(path.dirname(agySettingsPath), { recursive: true });
|
||||
fs.writeFileSync(agySettingsPath, '{"env": {"ANTHROPIC_MODEL": "claude-sonnet-4-6-thinking",}');
|
||||
|
||||
ensureProviderSettings('agy');
|
||||
|
||||
const repaired = JSON.parse(fs.readFileSync(agySettingsPath, 'utf-8')) as {
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBeDefined();
|
||||
|
||||
const backups = fs
|
||||
.readdirSync(path.dirname(agySettingsPath))
|
||||
.filter((file) => file.startsWith('agy.settings.json.corrupt-'));
|
||||
expect(backups.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,9 +71,7 @@ describe('Model Catalog', () => {
|
||||
|
||||
it('includes Claude Opus 4.5 Thinking', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const opus = MODEL_CATALOG.agy.models.find(
|
||||
(m) => m.id === 'claude-opus-4-5-thinking'
|
||||
);
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.5 Thinking');
|
||||
assert.strictEqual(opus.name, 'Claude Opus 4.5 Thinking');
|
||||
});
|
||||
@@ -87,6 +85,22 @@ 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');
|
||||
assert(sonnet, 'Should include Claude Sonnet 4.6');
|
||||
assert.strictEqual(sonnet.name, 'Claude Sonnet 4.6');
|
||||
});
|
||||
|
||||
it('includes Claude Sonnet 4.5', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const sonnet = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-5');
|
||||
@@ -103,9 +117,9 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier');
|
||||
});
|
||||
|
||||
it('has 5 models total', () => {
|
||||
it('has 7 models total', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 5);
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 7);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,9 +280,7 @@ describe('Model Catalog', () => {
|
||||
describe('Thinking models ordering', () => {
|
||||
it('Claude Opus 4.5 Thinking is not deprecated', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const opus = MODEL_CATALOG.agy.models.find(
|
||||
(m) => m.id === 'claude-opus-4-5-thinking'
|
||||
);
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.5 Thinking');
|
||||
assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated');
|
||||
});
|
||||
@@ -279,7 +291,11 @@ describe('Model Catalog', () => {
|
||||
(m) => m.id === 'claude-sonnet-4-5-thinking'
|
||||
);
|
||||
assert(sonnetThinking, 'Should include Claude Sonnet 4.5 Thinking');
|
||||
assert.strictEqual(sonnetThinking.deprecated, undefined, 'Should not be marked as deprecated');
|
||||
assert.strictEqual(
|
||||
sonnetThinking.deprecated,
|
||||
undefined,
|
||||
'Should not be marked as deprecated'
|
||||
);
|
||||
});
|
||||
|
||||
it('thinking models are at the top of the list', () => {
|
||||
@@ -288,9 +304,7 @@ describe('Model Catalog', () => {
|
||||
|
||||
// Find indices of thinking models
|
||||
const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
const sonnetThinkingIdx = models.findIndex(
|
||||
(m) => m.id === 'claude-sonnet-4-5-thinking'
|
||||
);
|
||||
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5-thinking');
|
||||
|
||||
// Find indices of non-thinking models
|
||||
const sonnetIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5');
|
||||
@@ -299,14 +313,8 @@ describe('Model Catalog', () => {
|
||||
// Thinking models should come before non-thinking models
|
||||
assert(opusIdx < sonnetIdx, 'Opus Thinking should be above non-thinking Sonnet');
|
||||
assert(opusIdx < geminiIdx, 'Opus Thinking should be above non-thinking Gemini');
|
||||
assert(
|
||||
sonnetThinkingIdx < sonnetIdx,
|
||||
'Sonnet Thinking should be above non-thinking Sonnet'
|
||||
);
|
||||
assert(
|
||||
sonnetThinkingIdx < geminiIdx,
|
||||
'Sonnet Thinking should be above non-thinking Gemini'
|
||||
);
|
||||
assert(sonnetThinkingIdx < sonnetIdx, 'Sonnet Thinking should be above non-thinking Sonnet');
|
||||
assert(sonnetThinkingIdx < geminiIdx, 'Sonnet Thinking should be above non-thinking Gemini');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-6-thinking',
|
||||
opus: 'claude-opus-4-6-thinking',
|
||||
sonnet: 'claude-sonnet-4-5-thinking',
|
||||
haiku: 'claude-sonnet-4-5',
|
||||
sonnet: 'claude-sonnet-4-6-thinking',
|
||||
haiku: 'claude-sonnet-4-6',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -33,8 +33,30 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-5-thinking',
|
||||
opus: 'claude-opus-4-5-thinking',
|
||||
sonnet: 'claude-sonnet-4-5-thinking',
|
||||
haiku: 'claude-sonnet-4-5',
|
||||
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',
|
||||
haiku: 'claude-sonnet-4-6',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-6',
|
||||
name: 'Claude Sonnet 4.6',
|
||||
description: 'Latest Sonnet baseline',
|
||||
presetMapping: {
|
||||
default: 'claude-sonnet-4-6',
|
||||
opus: 'claude-opus-4-6-thinking',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-sonnet-4-6',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user