mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 12:16:38 +00:00
fix(cliproxy): denylist deprecated agy 4.5 models
This commit is contained in:
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:
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in new issue
Block a user