fix(cliproxy): denylist deprecated agy 4.5 models

This commit is contained in:
Tam Nhu Tran committed 2026-03-03 03:10:53 +07:00
1 parent 5913f730ac
commit f63e2cd5a3
29 files changed
+850 -77

No files matched your search

@@ -0,0 +1,48 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { createApiProfile } from '../../../src/api/services/profile-writer';
describe('profile-writer AGY denylist', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-writer-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('rejects denylisted AGY 4.5 models on profile create', () => {
const result = createApiProfile(
'agy-denied',
'http://127.0.0.1:8317/api/provider/agy',
'test-token',
{
default: 'claude-opus-4.5',
opus: 'claude-opus-4.5',
sonnet: 'claude-sonnet-4.5',
haiku: 'claude-haiku-4.5',
},
'claude'
);
expect(result.success).toBe(false);
expect(result.error).toContain('denylist');
const settingsPath = path.join(tempHome, '.ccs', 'agy-denied.settings.json');
expect(fs.existsSync(settingsPath)).toBe(false);
});
});
@@ -10,7 +10,7 @@ import {
const validTier = {
provider: 'agy',
model: 'claude-sonnet-4-5-thinking',
model: 'claude-sonnet-4-6',
};
describe('validateCompositeDefaultTier', () => {
@@ -77,4 +77,17 @@ describe('validateCompositeTiers', () => {
expect(error).toContain("Circular fallback in tier 'opus'");
});
it('rejects denylisted agy 4.5 models', () => {
const error = validateCompositeTiers(
{
opus: { provider: 'agy', model: 'claude-opus-4.5-thinking' },
sonnet: { provider: 'gemini', model: 'gemini-2.5-pro' },
haiku: { provider: 'gemini', model: 'gemini-2.5-flash' },
},
{ defaultTier: 'opus', requireAllTiers: true }
);
expect(error).toContain('denylist');
});
});
@@ -73,7 +73,7 @@ cliproxy:
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
@@ -99,7 +99,7 @@ cliproxy:
expect(result.success).toBe(true);
expect(result.variant?.tiers?.opus.model).toBe('claude-opus-4-6-thinking');
expect(result.variant?.tiers?.sonnet.model).toBe('claude-sonnet-4-5-thinking'); // Unchanged
expect(result.variant?.tiers?.sonnet.model).toBe('claude-sonnet-4-6'); // Unchanged
expect(result.variant?.tiers?.haiku.model).toBe('claude-haiku-4-5-20251001'); // Unchanged
});
@@ -110,7 +110,7 @@ cliproxy:
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
@@ -144,7 +144,7 @@ cliproxy:
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
@@ -188,7 +188,7 @@ cliproxy:
thinking: 'xhigh',
account: 'team-a',
},
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
@@ -228,7 +228,7 @@ cliproxy:
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: customSettingsPath,
@@ -244,9 +244,9 @@ cliproxy:
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8318',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-5-thinking',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-preview',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
CUSTOM_ENV: 'preserve-this',
},
@@ -261,7 +261,7 @@ cliproxy:
const result = updateCompositeVariant('test', {
tiers: {
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking(high)' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6(high)' },
},
});
@@ -273,7 +273,7 @@ cliproxy:
hooks: { PreToolUse: unknown[] };
customPreset: boolean;
};
expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-5-thinking(high)');
expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(high)');
expect(updatedSettings.env.CUSTOM_ENV).toBe('preserve-this');
expect(updatedSettings.hooks.PreToolUse.length).toBe(1);
expect(updatedSettings.customPreset).toBe(true);
@@ -377,7 +377,7 @@ cliproxy:
defaultTier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
} as unknown as {
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
sonnet: { provider: 'gemini' | 'codex' | 'agy'; model: string };
@@ -395,7 +395,7 @@ cliproxy:
defaultTier: 'sonnet',
tiers: {
opus: null,
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
} as unknown as {
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
@@ -414,7 +414,7 @@ cliproxy:
defaultTier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
});
@@ -479,7 +479,7 @@ cliproxy:
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
@@ -547,7 +547,7 @@ cliproxy:
model: gemini-3-pro-preview
sonnet:
provider: agy
model: claude-sonnet-4-5-thinking
model: claude-sonnet-4-6
haiku:
provider: agy
model: claude-haiku-4-5-20251001
@@ -588,7 +588,7 @@ cliproxy:
thinking: xhigh
sonnet:
provider: agy
model: claude-sonnet-4-5-thinking
model: claude-sonnet-4-6
thinking: medium
haiku:
provider: agy
@@ -632,7 +632,7 @@ cliproxy:
model: claude-opus-4-6-thinking
sonnet:
provider: agy
model: claude-sonnet-4-5-thinking
model: claude-sonnet-4-6
haiku:
provider: agy
model: claude-haiku-4-5-20251001
@@ -671,7 +671,7 @@ cliproxy:
model: claude-opus-4-6-thinking
sonnet:
provider: agy
model: claude-sonnet-4-5-thinking
model: claude-sonnet-4-6
settings: cliproxy/bad.settings.json
port: 8318
good:
+8 -1
View File
@@ -584,9 +584,16 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// 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');
assert(
!config.includes('claude-sonnet-4-5'),
'Should not include denylisted Claude Sonnet 4.5 aliases'
);
assert(
!config.includes('claude-opus-4-5'),
'Should not include denylisted Claude Opus 4.5 aliases'
);
assert(
!config.includes('alias: gemini-claude-'),
'Should not emit deprecated gemini-claude aliases'
@@ -118,6 +118,50 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
});
it('migrates denylisted AGY 4.5 model IDs to supported 4.6 fallbacks', () => {
writeSettings(
settingsPath,
{
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-opus-4.5',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.5-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-sonnet-4-5',
},
{
presets: [
{
name: 'legacy-45',
default: 'claude-opus-4.5',
opus: 'claude-opus-4-5-thinking',
sonnet: 'claude-sonnet-4.5-thinking',
haiku: 'claude-sonnet-4-5',
},
],
}
);
const env = getEffectiveEnvVars('agy', 8317, settingsPath);
expect(env.ANTHROPIC_MODEL).toBe('claude-opus-4-6-thinking');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-6');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
env: Record<string, string>;
presets?: Array<Record<string, string>>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-opus-4-6-thinking');
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-sonnet-4-6');
expect(persisted.presets?.[0]?.default).toBe('claude-opus-4-6-thinking');
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-sonnet-4-6');
});
it('migrates codex preset model mappings to canonical IDs', () => {
writeSettings(
settingsPath,
@@ -269,11 +313,11 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
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');
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-6');
expect(repaired.presets?.[0]?.default).toBe('claude-sonnet-4-6');
expect(repaired.presets?.[0]?.opus).toBe('claude-opus-4-6-thinking');
expect(repaired.presets?.[0]?.sonnet).toBe('claude-sonnet-4-6');
expect(repaired.presets?.[0]?.haiku).toBe('claude-sonnet-4-5');
expect(repaired.presets?.[0]?.haiku).toBe('claude-sonnet-4-6');
});
it('migrates codex effort-suffixed preset IDs during ensureProviderSettings', () => {
@@ -2,7 +2,10 @@ import { describe, it, expect } from 'bun:test';
import {
canonicalizeModelIdForProvider,
extractProviderFromPathname,
getDeniedModelIdReasonForProvider,
isDeniedAntigravityModelId,
isAntigravityProvider,
migrateDeniedAntigravityModelAliases,
normalizeClaudeDottedMajorMinor,
normalizeClaudeDottedThinkingMajorMinor,
normalizeModelIdForProvider,
@@ -65,12 +68,12 @@ describe('model-id-normalizer', () => {
'claude-opus-4-6-thinking'
);
expect(normalizeModelIdForProvider('claude-opus-4.5-thinking', 'agy')).toBe(
'claude-opus-4-6-thinking'
'claude-opus-4-5-thinking'
);
expect(normalizeModelIdForProvider('claude-sonnet-4.5-thinking', 'agy')).toBe(
'claude-sonnet-4-6'
'claude-sonnet-4-5-thinking'
);
expect(normalizeModelIdForProvider('claude-sonnet-4.5', 'agy')).toBe('claude-sonnet-4-6');
expect(normalizeModelIdForProvider('claude-sonnet-4.5', 'agy')).toBe('claude-sonnet-4-5');
expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'gemini')).toBe(
'claude-opus-4.6-thinking'
);
@@ -113,9 +116,9 @@ describe('model-id-normalizer', () => {
};
const normalized = normalizeModelEnvVarsForProvider(input, 'agy');
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_MODEL).toBe('claude-sonnet-4-5-thinking');
expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-5-thinking');
expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-5');
expect(normalized.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
expect(normalized.UNRELATED).toBe('keep-me');
@@ -123,5 +126,23 @@ describe('model-id-normalizer', () => {
expect(unchanged.ANTHROPIC_MODEL).toBe('claude-sonnet-4.5-thinking');
expect(unchanged.UNRELATED).toBe('keep-me');
});
it('flags denylisted antigravity models', () => {
expect(isDeniedAntigravityModelId('claude-opus-4-5-thinking')).toBe(true);
expect(isDeniedAntigravityModelId('claude-sonnet-4.5')).toBe(true);
expect(getDeniedModelIdReasonForProvider('claude-sonnet-4.5', 'agy')).toContain('denylist');
expect(getDeniedModelIdReasonForProvider('claude-sonnet-4.5', 'gemini')).toBeNull();
expect(getDeniedModelIdReasonForProvider('claude-haiku-4.5', 'agy')).toBeNull();
});
it('migrates denylisted antigravity IDs to supported 4.6 fallbacks', () => {
expect(migrateDeniedAntigravityModelAliases('claude-opus-4.5')).toBe(
'claude-opus-4-6-thinking'
);
expect(migrateDeniedAntigravityModelAliases('claude-sonnet-4-5-thinking(8192)')).toBe(
'claude-sonnet-4-6(8192)'
);
expect(migrateDeniedAntigravityModelAliases('claude-haiku-4-5')).toBe('claude-haiku-4-5');
});
});
});
@@ -253,6 +253,31 @@ describe('ToolSanitizationProxy Integration', () => {
}
});
it('rejects denylisted AGY 4.5 model IDs on antigravity provider route', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
const response = await fetch(`http://127.0.0.1:${port}/api/provider/agy/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: 'test' }],
}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('denylist');
expect(lastRequest).toBeNull();
} finally {
proxy.stop();
}
});
it('does not rewrite AGY legacy aliases on non-AGY provider routes', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
@@ -83,6 +83,15 @@ cliproxy:
expect(result.error).toContain('Changing provider requires model update');
});
it('rejects denylisted AGY 4.5 model updates', () => {
const result = updateVariant('demo', {
provider: 'agy',
model: 'claude-sonnet-4.5',
});
expect(result.success).toBe(false);
expect(result.error).toContain('denylist');
});
it('updates provider and regenerates provider-specific core env in same settings file', () => {
const result = updateVariant('demo', {
provider: 'codex',
@@ -91,6 +91,33 @@ describe('cliproxy-stats-routes model update canonicalization', () => {
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
});
it('rejects denylisted AGY 4.5 model updates', 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',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
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-opus-4.5' }),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('denylist');
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
});
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, {
@@ -0,0 +1,74 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
createSettingsFile,
updateSettingsFile,
} from '../../../src/web-server/routes/route-helpers';
describe('route-helpers AGY denylist', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-route-helpers-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('rejects denylisted AGY models on settings create', () => {
expect(() =>
createSettingsFile(
'agy-denied',
'http://127.0.0.1:8317/api/provider/agy',
'test-token',
{
model: 'claude-sonnet-4.5',
opusModel: 'claude-opus-4.5',
sonnetModel: 'claude-sonnet-4.5',
haikuModel: 'claude-haiku-4.5',
}
)
).toThrow(/denylist/i);
});
it('rejects denylisted AGY models on settings update', () => {
const settingsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(settingsDir, { recursive: true });
const settingsPath = path.join(settingsDir, 'agy-profile.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
ANTHROPIC_AUTH_TOKEN: 'test-token',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5',
},
},
null,
2
) + '\n'
);
expect(() => updateSettingsFile('agy-profile', { model: 'claude-opus-4.5' })).toThrow(
/denylist/i
);
});
});
@@ -324,6 +324,88 @@ describe('settings-routes model canonicalization', () => {
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
});
it('rejects denylisted AGY models 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.5',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5',
},
presets: [],
},
}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error?: string };
expect(body.error).toContain('denylist');
});
it('rejects denylisted AGY models 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: 'legacy',
default: 'claude-opus-4.5-thinking',
}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error?: string };
expect(body.error).toContain('denylist');
});
it('drops denylisted AGY presets on read canonicalization', 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',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
},
presets: [
{
name: 'denylisted',
default: 'claude-sonnet-4.5',
opus: 'claude-opus-4-6-thinking',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4.5',
},
],
});
const response = await fetch(`${baseUrl}/api/settings/agy/presets`);
expect(response.status).toBe(200);
const body = (await response.json()) as { presets: Array<Record<string, string>> };
expect(body.presets.length).toBe(0);
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
presets: Array<Record<string, string>>;
};
expect(persisted.presets.length).toBe(0);
});
it('does not crash on malformed non-string preset model values', async () => {
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
writeSettings(settingsPath, {