fix(cliproxy): prevent agy model reset and add sonnet 4.6 models

This commit is contained in:
Tam Nhu Tran committed 2026-02-25 00:12:49 +07:00
1 parent 1dae52014b
commit bebb7582cf
10 files changed
+224 -53

No files matched your search

+17 -13
View File
@@ -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);
});
});
+28 -20
View File
@@ -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');
});
});