From 92d76cad89e00ece4bc7a91dd1f28a3b2b22ee54 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 07:39:16 +0200 Subject: [PATCH 01/17] feat(cliproxy): add --json flag to catalog command Add machine-readable JSON output for `ccs cliproxy catalog --json` to enable programmatic access to available models per provider. Output format: { "provider": [{ "id": "...", "name": "..." }, ...] } Closes CCS-1 Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 15 +++++++++++++++ src/commands/cliproxy/index.ts | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index 13374e65..a41cb56f 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -5,6 +5,7 @@ import { SYNCABLE_PROVIDERS, getResolvedCatalog, refreshCatalogFromProxy, + getAllResolvedCatalogs, } from '../../cliproxy/catalog-cache'; import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing'; import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes'; @@ -173,6 +174,20 @@ export async function handleCatalogRefresh(verbose: boolean): Promise { console.log(''); } +/** + * Output catalog as JSON for programmatic consumption. + * Used by OnSteroids and other tools to get available models per provider. + * Format: { provider: [{ id, name }], ... } + */ +export function handleCatalogJson(): void { + const catalogs = getAllResolvedCatalogs(); + const result: Record> = {}; + for (const [provider, catalog] of Object.entries(catalogs)) { + result[provider] = catalog.models.map((m) => ({ id: m.id, name: m.name })); + } + console.log(JSON.stringify(result)); +} + /** Reset catalog cache */ export async function handleCatalogReset(): Promise { await initUI(); diff --git a/src/commands/cliproxy/index.ts b/src/commands/cliproxy/index.ts index 50c9013a..bad626b4 100644 --- a/src/commands/cliproxy/index.ts +++ b/src/commands/cliproxy/index.ts @@ -40,6 +40,7 @@ import { handleCatalogStatus, handleCatalogRefresh, handleCatalogReset, + handleCatalogJson, } from './catalog-subcommand'; /** @@ -146,6 +147,10 @@ export async function handleCliproxyCommand(args: string[]): Promise { // Catalog commands if (command === 'catalog') { + if (hasAnyFlag(remainingArgs, ['--json'])) { + handleCatalogJson(); + return; + } const subcommand = remainingArgs[1]; if (subcommand === 'refresh') { await handleCatalogRefresh(verbose); From 48634549f87a9b07b279ed5b9b545d44144fd511 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 07:43:14 +0200 Subject: [PATCH 02/17] chore: trigger PResto review From f090c32c2f23065dbc1633ab9689124cffe4411d Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 18:02:32 +0200 Subject: [PATCH 03/17] docs(cliproxy): clarify catalog --json output format in JSDoc Use TypeScript-style index signature notation instead of ambiguous placeholder name in format description. Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index a41cb56f..183759d7 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -177,7 +177,7 @@ export async function handleCatalogRefresh(verbose: boolean): Promise { /** * Output catalog as JSON for programmatic consumption. * Used by OnSteroids and other tools to get available models per provider. - * Format: { provider: [{ id, name }], ... } + * Format: { [providerName: string]: Array<{ id: string, name: string }> } */ export function handleCatalogJson(): void { const catalogs = getAllResolvedCatalogs(); From c1d462ea41d4086cb6facb808131a51cb85730ff Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 18:27:39 +0200 Subject: [PATCH 04/17] test(cliproxy): add catalog --json tests and clarify flag priority - Add unit tests verifying JSON output structure, field filtering, and minified format - Document that --json takes priority over subcommands (refresh/reset) Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/index.ts | 2 + .../commands/cliproxy-catalog-json.test.ts | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/unit/commands/cliproxy-catalog-json.test.ts diff --git a/src/commands/cliproxy/index.ts b/src/commands/cliproxy/index.ts index bad626b4..52c4cb02 100644 --- a/src/commands/cliproxy/index.ts +++ b/src/commands/cliproxy/index.ts @@ -147,6 +147,8 @@ export async function handleCliproxyCommand(args: string[]): Promise { // Catalog commands if (command === 'catalog') { + // --json takes priority over subcommands (refresh/reset) — it always + // outputs the current resolved catalog regardless of other arguments. if (hasAnyFlag(remainingArgs, ['--json'])) { handleCatalogJson(); return; diff --git a/tests/unit/commands/cliproxy-catalog-json.test.ts b/tests/unit/commands/cliproxy-catalog-json.test.ts new file mode 100644 index 00000000..0e1928ff --- /dev/null +++ b/tests/unit/commands/cliproxy-catalog-json.test.ts @@ -0,0 +1,64 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; + +let originalConsoleLog: typeof console.log; +let capturedOutput: string[]; + +beforeEach(() => { + originalConsoleLog = console.log; + capturedOutput = []; + console.log = (...args: unknown[]) => { + capturedOutput.push(args.map(String).join(' ')); + }; +}); + +afterEach(() => { + console.log = originalConsoleLog; +}); + +describe('cliproxy catalog --json output', () => { + it('outputs valid JSON mapping provider names to model arrays', async () => { + const { handleCatalogJson } = await import( + `../../../src/commands/cliproxy/catalog-subcommand?catalog-json-format=${Date.now()}` + ); + + handleCatalogJson(); + + expect(capturedOutput).toHaveLength(1); + const parsed = JSON.parse(capturedOutput[0]) as Record< + string, + Array<{ id: string; name: string }> + >; + + // Must be a non-empty object (static catalog always has providers) + expect(typeof parsed).toBe('object'); + expect(Object.keys(parsed).length).toBeGreaterThan(0); + + // Every provider entry must be an array of { id, name } objects + for (const [provider, models] of Object.entries(parsed)) { + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); + for (const model of models) { + expect(typeof model.id).toBe('string'); + expect(typeof model.name).toBe('string'); + // Only id and name — no extra metadata leaks + expect(Object.keys(model).sort()).toEqual(['id', 'name']); + } + // Provider key should be a non-empty string + expect(provider.length).toBeGreaterThan(0); + } + }); + + it('outputs minified JSON (single line, no whitespace formatting)', async () => { + const { handleCatalogJson } = await import( + `../../../src/commands/cliproxy/catalog-subcommand?catalog-json-minified=${Date.now()}` + ); + + handleCatalogJson(); + + const output = capturedOutput[0]; + // Minified JSON has no newlines + expect(output.includes('\n')).toBe(false); + // Valid JSON roundtrip + expect(JSON.stringify(JSON.parse(output))).toBe(output); + }); +}); From 6fc22625ac3917d4066e6ea96df80ad5952a8f85 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 18:53:33 +0200 Subject: [PATCH 05/17] feat(cliproxy): include model metadata in catalog --json output - Expose tier, description, deprecated, broken, extendedContext, nativeImageInput fields (omitted when undefined) - Simplify tests to use static imports instead of dynamic cache-busting - Add integration tests verifying --json routing through handleCliproxyCommand - Test --json priority over refresh/reset subcommands Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 29 +++++- .../commands/cliproxy-catalog-json.test.ts | 93 ++++++++++++++----- 2 files changed, 97 insertions(+), 25 deletions(-) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index 183759d7..78fb6e1d 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -174,16 +174,39 @@ export async function handleCatalogRefresh(verbose: boolean): Promise { console.log(''); } +/** JSON-serialisable model entry emitted by `catalog --json`. */ +interface CatalogJsonModel { + id: string; + name: string; + tier?: 'free' | 'pro' | 'ultra'; + description?: string; + deprecated?: boolean; + deprecationReason?: string; + broken?: boolean; + extendedContext?: boolean; + nativeImageInput?: boolean; +} + /** * Output catalog as JSON for programmatic consumption. * Used by OnSteroids and other tools to get available models per provider. - * Format: { [providerName: string]: Array<{ id: string, name: string }> } + * Format: { [providerName: string]: CatalogJsonModel[] } */ export function handleCatalogJson(): void { const catalogs = getAllResolvedCatalogs(); - const result: Record> = {}; + const result: Record = {}; for (const [provider, catalog] of Object.entries(catalogs)) { - result[provider] = catalog.models.map((m) => ({ id: m.id, name: m.name })); + result[provider] = catalog.models.map((m) => { + const entry: CatalogJsonModel = { id: m.id, name: m.name }; + if (m.tier) entry.tier = m.tier; + if (m.description) entry.description = m.description; + if (m.deprecated) entry.deprecated = m.deprecated; + if (m.deprecationReason) entry.deprecationReason = m.deprecationReason; + if (m.broken) entry.broken = m.broken; + if (m.extendedContext) entry.extendedContext = m.extendedContext; + if (m.nativeImageInput) entry.nativeImageInput = m.nativeImageInput; + return entry; + }); } console.log(JSON.stringify(result)); } diff --git a/tests/unit/commands/cliproxy-catalog-json.test.ts b/tests/unit/commands/cliproxy-catalog-json.test.ts index 0e1928ff..cd3fe17f 100644 --- a/tests/unit/commands/cliproxy-catalog-json.test.ts +++ b/tests/unit/commands/cliproxy-catalog-json.test.ts @@ -1,4 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { handleCatalogJson } from '../../../src/commands/cliproxy/catalog-subcommand'; +import { handleCliproxyCommand } from '../../../src/commands/cliproxy/index'; let originalConsoleLog: typeof console.log; let capturedOutput: string[]; @@ -16,49 +18,96 @@ afterEach(() => { }); describe('cliproxy catalog --json output', () => { - it('outputs valid JSON mapping provider names to model arrays', async () => { - const { handleCatalogJson } = await import( - `../../../src/commands/cliproxy/catalog-subcommand?catalog-json-format=${Date.now()}` - ); - + it('outputs valid JSON mapping provider names to model arrays', () => { handleCatalogJson(); expect(capturedOutput).toHaveLength(1); - const parsed = JSON.parse(capturedOutput[0]) as Record< - string, - Array<{ id: string; name: string }> - >; + const parsed = JSON.parse(capturedOutput[0]) as Record; // Must be a non-empty object (static catalog always has providers) expect(typeof parsed).toBe('object'); expect(Object.keys(parsed).length).toBeGreaterThan(0); - // Every provider entry must be an array of { id, name } objects - for (const [provider, models] of Object.entries(parsed)) { + // Every provider entry must be an array of objects with at least id and name + for (const models of Object.values(parsed)) { expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); - for (const model of models) { + for (const model of models as Array>) { expect(typeof model.id).toBe('string'); expect(typeof model.name).toBe('string'); - // Only id and name — no extra metadata leaks - expect(Object.keys(model).sort()).toEqual(['id', 'name']); } - // Provider key should be a non-empty string - expect(provider.length).toBeGreaterThan(0); } }); - it('outputs minified JSON (single line, no whitespace formatting)', async () => { - const { handleCatalogJson } = await import( - `../../../src/commands/cliproxy/catalog-subcommand?catalog-json-minified=${Date.now()}` - ); + it('includes metadata fields when present on model entries', () => { + handleCatalogJson(); + const parsed = JSON.parse(capturedOutput[0]) as Record< + string, + Array> + >; + const allModels = Object.values(parsed).flat(); + + // At least some models in the static catalog have tier set + const withTier = allModels.filter((m) => m.tier !== undefined); + expect(withTier.length).toBeGreaterThan(0); + + // Tier values must be one of the allowed strings + for (const model of withTier) { + expect(['free', 'pro', 'ultra']).toContain(model.tier); + } + }); + + it('omits undefined optional fields instead of including nulls', () => { + handleCatalogJson(); + + const parsed = JSON.parse(capturedOutput[0]) as Record< + string, + Array> + >; + const allModels = Object.values(parsed).flat(); + + for (const model of allModels) { + for (const value of Object.values(model)) { + expect(value).not.toBeNull(); + expect(value).not.toBeUndefined(); + } + } + }); + + it('outputs minified JSON (single line, no whitespace formatting)', () => { handleCatalogJson(); const output = capturedOutput[0]; - // Minified JSON has no newlines expect(output.includes('\n')).toBe(false); - // Valid JSON roundtrip expect(JSON.stringify(JSON.parse(output))).toBe(output); }); }); + +describe('cliproxy catalog --json routing', () => { + it('routes catalog --json through handleCliproxyCommand', async () => { + await handleCliproxyCommand(['catalog', '--json']); + + expect(capturedOutput).toHaveLength(1); + const parsed = JSON.parse(capturedOutput[0]); + expect(typeof parsed).toBe('object'); + expect(Object.keys(parsed).length).toBeGreaterThan(0); + }); + + it('--json takes priority over refresh subcommand', async () => { + await handleCliproxyCommand(['catalog', 'refresh', '--json']); + + expect(capturedOutput).toHaveLength(1); + // Should output JSON, not refresh output + const parsed = JSON.parse(capturedOutput[0]); + expect(typeof parsed).toBe('object'); + }); + + it('--json takes priority when placed before subcommand', async () => { + await handleCliproxyCommand(['catalog', '--json', 'reset']); + + expect(capturedOutput).toHaveLength(1); + const parsed = JSON.parse(capturedOutput[0]); + expect(typeof parsed).toBe('object'); + }); +}); From f5520de42ad76ec9ed22bf36893e68d8fba0af4e Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 19:08:24 +0200 Subject: [PATCH 06/17] fix(cliproxy): preserve explicit false values in catalog --json Use !== undefined checks instead of truthy checks so that explicitly set false booleans (e.g. extendedContext: false) appear in JSON output, allowing consumers to distinguish "not set" from "explicitly disabled". Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index 78fb6e1d..3090c28e 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -198,13 +198,13 @@ export function handleCatalogJson(): void { for (const [provider, catalog] of Object.entries(catalogs)) { result[provider] = catalog.models.map((m) => { const entry: CatalogJsonModel = { id: m.id, name: m.name }; - if (m.tier) entry.tier = m.tier; - if (m.description) entry.description = m.description; - if (m.deprecated) entry.deprecated = m.deprecated; - if (m.deprecationReason) entry.deprecationReason = m.deprecationReason; - if (m.broken) entry.broken = m.broken; - if (m.extendedContext) entry.extendedContext = m.extendedContext; - if (m.nativeImageInput) entry.nativeImageInput = m.nativeImageInput; + if (m.tier !== undefined) entry.tier = m.tier; + if (m.description !== undefined) entry.description = m.description; + if (m.deprecated !== undefined) entry.deprecated = m.deprecated; + if (m.deprecationReason !== undefined) entry.deprecationReason = m.deprecationReason; + if (m.broken !== undefined) entry.broken = m.broken; + if (m.extendedContext !== undefined) entry.extendedContext = m.extendedContext; + if (m.nativeImageInput !== undefined) entry.nativeImageInput = m.nativeImageInput; return entry; }); } From c1d0a4d2eb15d69a42498d13b2fa9fbe763451e6 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 19:22:05 +0200 Subject: [PATCH 07/17] feat(cliproxy): add thinking config to catalog --json and test false values - Include thinking support configuration (type, min, max, levels) in JSON output for reasoning-capable models - Add test verifying explicit false booleans are preserved (not omitted) - Add test verifying thinking config appears for thinking models Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 3 ++ .../commands/cliproxy-catalog-json.test.ts | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index 3090c28e..05c4f172 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -10,6 +10,7 @@ import { import { getCatalogRoutingSnapshot } from '../../cliproxy/catalog-routing'; import { ensureManagedModelPrefixes } from '../../cliproxy/managed-model-prefixes'; import { getProxyTarget } from '../../cliproxy/proxy-target-resolver'; +import type { ThinkingSupport } from '../../cliproxy/model-catalog'; import type { CLIProxyProvider } from '../../cliproxy/types'; import type { RemoteModelInfo } from '../../cliproxy/management-api-types'; import type { CliproxyProviderRoutingHints } from '../../shared/cliproxy-model-routing'; @@ -183,6 +184,7 @@ interface CatalogJsonModel { deprecated?: boolean; deprecationReason?: string; broken?: boolean; + thinking?: ThinkingSupport; extendedContext?: boolean; nativeImageInput?: boolean; } @@ -203,6 +205,7 @@ export function handleCatalogJson(): void { if (m.deprecated !== undefined) entry.deprecated = m.deprecated; if (m.deprecationReason !== undefined) entry.deprecationReason = m.deprecationReason; if (m.broken !== undefined) entry.broken = m.broken; + if (m.thinking !== undefined) entry.thinking = m.thinking; if (m.extendedContext !== undefined) entry.extendedContext = m.extendedContext; if (m.nativeImageInput !== undefined) entry.nativeImageInput = m.nativeImageInput; return entry; diff --git a/tests/unit/commands/cliproxy-catalog-json.test.ts b/tests/unit/commands/cliproxy-catalog-json.test.ts index cd3fe17f..7bc3e5ea 100644 --- a/tests/unit/commands/cliproxy-catalog-json.test.ts +++ b/tests/unit/commands/cliproxy-catalog-json.test.ts @@ -75,6 +75,39 @@ describe('cliproxy catalog --json output', () => { } }); + it('includes explicit false boolean values in output', () => { + handleCatalogJson(); + + const parsed = JSON.parse(capturedOutput[0]) as Record< + string, + Array> + >; + const allModels = Object.values(parsed).flat(); + + // Static catalog has models with extendedContext: false + const withExplicitFalse = allModels.filter((m) => m.extendedContext === false); + expect(withExplicitFalse.length).toBeGreaterThan(0); + }); + + it('includes thinking configuration when present on models', () => { + handleCatalogJson(); + + const parsed = JSON.parse(capturedOutput[0]) as Record< + string, + Array> + >; + const allModels = Object.values(parsed).flat(); + + // Static catalog has thinking models (e.g. Claude Opus 4.6 Thinking) + const withThinking = allModels.filter((m) => m.thinking !== undefined); + expect(withThinking.length).toBeGreaterThan(0); + + for (const model of withThinking) { + const thinking = model.thinking as Record; + expect(['budget', 'levels', 'none']).toContain(thinking.type); + } + }); + it('outputs minified JSON (single line, no whitespace formatting)', () => { handleCatalogJson(); From 02c5a5c7a17d580606b4666362c6a9840a49e1d7 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 19:33:00 +0200 Subject: [PATCH 08/17] fix(cliproxy): guard against undefined catalog and add issueUrl field - Add null check for catalog entries from Partial type - Include issueUrl in JSON output for broken model tracking Built [OnSteroids](https://onsteroids.ai) --- src/commands/cliproxy/catalog-subcommand.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands/cliproxy/catalog-subcommand.ts b/src/commands/cliproxy/catalog-subcommand.ts index 05c4f172..adcdb5e5 100644 --- a/src/commands/cliproxy/catalog-subcommand.ts +++ b/src/commands/cliproxy/catalog-subcommand.ts @@ -184,6 +184,7 @@ interface CatalogJsonModel { deprecated?: boolean; deprecationReason?: string; broken?: boolean; + issueUrl?: string; thinking?: ThinkingSupport; extendedContext?: boolean; nativeImageInput?: boolean; @@ -198,6 +199,9 @@ export function handleCatalogJson(): void { const catalogs = getAllResolvedCatalogs(); const result: Record = {}; for (const [provider, catalog] of Object.entries(catalogs)) { + if (!catalog) { + continue; + } result[provider] = catalog.models.map((m) => { const entry: CatalogJsonModel = { id: m.id, name: m.name }; if (m.tier !== undefined) entry.tier = m.tier; @@ -205,6 +209,7 @@ export function handleCatalogJson(): void { if (m.deprecated !== undefined) entry.deprecated = m.deprecated; if (m.deprecationReason !== undefined) entry.deprecationReason = m.deprecationReason; if (m.broken !== undefined) entry.broken = m.broken; + if (m.issueUrl !== undefined) entry.issueUrl = m.issueUrl; if (m.thinking !== undefined) entry.thinking = m.thinking; if (m.extendedContext !== undefined) entry.extendedContext = m.extendedContext; if (m.nativeImageInput !== undefined) entry.nativeImageInput = m.nativeImageInput; From 513896b1afd156030a7d24036218499d76df0d6c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 19 Apr 2026 21:38:12 +0000 Subject: [PATCH 09/17] chore(release): 7.73.0 [skip ci] ## [7.73.0](https://github.com/kaitranntt/ccs/compare/v7.72.1...v7.73.0) (2026-04-19) ### Features * **proxy:** enhance Anthropic-to-OpenAI message transformation and schema sanitization ([2672e35](https://github.com/kaitranntt/ccs/commit/2672e35362938b054710de681760bd24c3c39e19)) ### Bug Fixes * **browser:** bootstrap managed attach profile setup ([b525033](https://github.com/kaitranntt/ccs/commit/b52503300b0c434fbb9d88283643d3ceaa673d44)) * **cliproxy:** preserve legacy openai-compat connectors on restart ([16f81fc](https://github.com/kaitranntt/ccs/commit/16f81fc8a595792f1c1a6df060f88cdf28d7afb6)) * **codex:** probe Windows cmd wrappers via cmd shell ([210ec33](https://github.com/kaitranntt/ccs/commit/210ec33c04fe88be2b651c2163f638085b1b6bd2)) * **proxy:** harden Anthropic request transformation semantics ([ebc9219](https://github.com/kaitranntt/ccs/commit/ebc92194bb9bf2810e10f64de1fa6e599c2fa156)) * **proxy:** preserve valid enums during schema normalization ([22ab58b](https://github.com/kaitranntt/ccs/commit/22ab58b02e5dc34f152ecce048c1fbf7d615bdfb)) * **proxy:** restore strict Anthropic message validation ([32d6bfd](https://github.com/kaitranntt/ccs/commit/32d6bfdda7881a42a376d0973c021f0b47eed7e2)) * **repo:** remove tracked plans workspace files ([bc4ab98](https://github.com/kaitranntt/ccs/commit/bc4ab98af41e9c9f049cc5da2e6ff356832ef14b)) * **targets:** use cmd.exe for escaped Windows wrapper launches ([a945b0b](https://github.com/kaitranntt/ccs/commit/a945b0b1047b85a7b343dfe35d8ae9680346e25f)) * **windows:** align escaped wrapper shell handling ([ccdd0b6](https://github.com/kaitranntt/ccs/commit/ccdd0b6e8e4185ec2b01186cfc69e28f171d0906)) ### Tests * **cliproxy:** assert routing persistence via unified config loader ([485fe4b](https://github.com/kaitranntt/ccs/commit/485fe4ba1c5878083c6e2b6a2d98b37208772a1f)) * **cliproxy:** cover legacy connector removal cleanup ([00d902b](https://github.com/kaitranntt/ccs/commit/00d902b6e64839cd6b89446cc5dee540f2a059b8)) * **cliproxy:** isolate routing strategy service state ([5e1e807](https://github.com/kaitranntt/ccs/commit/5e1e8070e821dbe660ea77bc96fec610937ed44a)) * **websearch:** clear CCS_PROFILE_TYPE in SearXNG fallback spawn env ([e25d791](https://github.com/kaitranntt/ccs/commit/e25d791d9bee38e7143757d37c42bd850e471ae8)) ### CI * add concurrency group and split build/test with artifact sharing ([ad54e17](https://github.com/kaitranntt/ccs/commit/ad54e179b6a3921fd26ad3ac585f5ef965b01c1b)) * **pre-push:** skip gate for delete-only pushes ([6236cb7](https://github.com/kaitranntt/ccs/commit/6236cb7feb15e954f7f65ae98e6fd98b323a4adb)) --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd1a8a5..fcd2b3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## [7.73.0](https://github.com/kaitranntt/ccs/compare/v7.72.1...v7.73.0) (2026-04-19) + +### Features + +* **proxy:** enhance Anthropic-to-OpenAI message transformation and schema sanitization ([2672e35](https://github.com/kaitranntt/ccs/commit/2672e35362938b054710de681760bd24c3c39e19)) + +### Bug Fixes + +* **browser:** bootstrap managed attach profile setup ([b525033](https://github.com/kaitranntt/ccs/commit/b52503300b0c434fbb9d88283643d3ceaa673d44)) +* **cliproxy:** preserve legacy openai-compat connectors on restart ([16f81fc](https://github.com/kaitranntt/ccs/commit/16f81fc8a595792f1c1a6df060f88cdf28d7afb6)) +* **codex:** probe Windows cmd wrappers via cmd shell ([210ec33](https://github.com/kaitranntt/ccs/commit/210ec33c04fe88be2b651c2163f638085b1b6bd2)) +* **proxy:** harden Anthropic request transformation semantics ([ebc9219](https://github.com/kaitranntt/ccs/commit/ebc92194bb9bf2810e10f64de1fa6e599c2fa156)) +* **proxy:** preserve valid enums during schema normalization ([22ab58b](https://github.com/kaitranntt/ccs/commit/22ab58b02e5dc34f152ecce048c1fbf7d615bdfb)) +* **proxy:** restore strict Anthropic message validation ([32d6bfd](https://github.com/kaitranntt/ccs/commit/32d6bfdda7881a42a376d0973c021f0b47eed7e2)) +* **repo:** remove tracked plans workspace files ([bc4ab98](https://github.com/kaitranntt/ccs/commit/bc4ab98af41e9c9f049cc5da2e6ff356832ef14b)) +* **targets:** use cmd.exe for escaped Windows wrapper launches ([a945b0b](https://github.com/kaitranntt/ccs/commit/a945b0b1047b85a7b343dfe35d8ae9680346e25f)) +* **windows:** align escaped wrapper shell handling ([ccdd0b6](https://github.com/kaitranntt/ccs/commit/ccdd0b6e8e4185ec2b01186cfc69e28f171d0906)) + +### Tests + +* **cliproxy:** assert routing persistence via unified config loader ([485fe4b](https://github.com/kaitranntt/ccs/commit/485fe4ba1c5878083c6e2b6a2d98b37208772a1f)) +* **cliproxy:** cover legacy connector removal cleanup ([00d902b](https://github.com/kaitranntt/ccs/commit/00d902b6e64839cd6b89446cc5dee540f2a059b8)) +* **cliproxy:** isolate routing strategy service state ([5e1e807](https://github.com/kaitranntt/ccs/commit/5e1e8070e821dbe660ea77bc96fec610937ed44a)) +* **websearch:** clear CCS_PROFILE_TYPE in SearXNG fallback spawn env ([e25d791](https://github.com/kaitranntt/ccs/commit/e25d791d9bee38e7143757d37c42bd850e471ae8)) + +### CI + +* add concurrency group and split build/test with artifact sharing ([ad54e17](https://github.com/kaitranntt/ccs/commit/ad54e179b6a3921fd26ad3ac585f5ef965b01c1b)) +* **pre-push:** skip gate for delete-only pushes ([6236cb7](https://github.com/kaitranntt/ccs/commit/6236cb7feb15e954f7f65ae98e6fd98b323a4adb)) + ## [7.72.1](https://github.com/kaitranntt/ccs/compare/v7.72.0...v7.72.1) (2026-04-17) ### Bug Fixes diff --git a/package.json b/package.json index f2ffe18a..2ec0516e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.72.1-dev.7", + "version": "7.73.0", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli", From dea704c57dea8ba9c9538ddea6638fbd4e5c7660 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 16:58:13 +0200 Subject: [PATCH 10/17] fix(husky): re-exec pre-push under bash for sh compatibility Husky v9 invokes hooks via `sh -e` regardless of the script's shebang, causing `set -o pipefail` and bash regex `[[ =~ ]]` in .husky/pre-push to fail with "Illegal option -o pipefail" on systems where /bin/sh is not bash (Debian/Ubuntu dash). Add a POSIX-compatible preamble that re-execs the script under bash when invoked without BASH_VERSION. No changes to gate logic. Built [OnSteroids](https://onsteroids.ai) --- .husky/pre-push | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.husky/pre-push b/.husky/pre-push index 4dee9050..07a426a0 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,10 @@ #!/usr/bin/env bash +# Husky v9 invokes hooks via `sh -e` (see .husky/_/h), ignoring this shebang. +# Re-exec under bash so `set -o pipefail` and `[[ ... ]]` below work portably. +if [ -z "${BASH_VERSION:-}" ]; then + exec bash "$0" "$@" +fi + set -euo pipefail # Protected branches keep the full CI parity gate. From 61058123e10de6ed21cadc0d1450b08cd28df6db Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 16:29:16 +0200 Subject: [PATCH 11/17] feat(cliproxy): add Claude Opus 4.7 support Adds Claude Opus 4.7 as a new model to the AGY (Antigravity) and Claude provider catalogs, with pricing, Cursor IDE support, and updated defaults across the codebase. - Add claude-opus-4-7-thinking and claude-opus-4-7 to AGY catalog (defaultModel bumped to claude-opus-4-7-thinking) - Add claude-opus-4-7 entry to Claude provider catalog - Register claude-opus-4-7-thinking fork alias in CLIProxy config (CLIPROXY_CONFIG_VERSION bumped 17 -> 18) - Add pricing for claude-opus-4-7 and claude-opus-4-7-thinking ($5/$25 per million, matching Opus 4.6) - Add claude-4.7-opus and claude-4.7-opus-fast-mode to Cursor catalog - Update ANTHROPIC_DEFAULT_OPUS_MODEL in base-claude settings to 4.7 - Update Droid adapter and code-reviewer fallback models to 4.7 - Update unit tests for model-catalog and model-pricing Opus 4.6 is retained as a supported fallback in both catalogs. Built [OnSteroids](https://onsteroids.ai) --- config/base-claude.settings.json | 2 +- scripts/code-reviewer.ts | 2 +- src/cliproxy/config/generator.ts | 3 +- src/cliproxy/model-catalog.ts | 50 +++++++++++++++++++++-- src/cursor/cursor-default-models.ts | 10 +++++ src/targets/droid-adapter.ts | 2 +- src/web-server/model-pricing.ts | 13 ++++++ tests/unit/cliproxy/model-catalog.test.js | 45 +++++++++++++++----- tests/unit/model-pricing.test.ts | 18 ++++++++ 9 files changed, 127 insertions(+), 18 deletions(-) diff --git a/config/base-claude.settings.json b/config/base-claude.settings.json index c8b703c9..e9986c70 100644 --- a/config/base-claude.settings.json +++ b/config/base-claude.settings.json @@ -3,7 +3,7 @@ "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/claude", "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", "ANTHROPIC_MODEL": "claude-sonnet-4-6", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-7", "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001" } diff --git a/scripts/code-reviewer.ts b/scripts/code-reviewer.ts index 89d7a829..02dda7f6 100755 --- a/scripts/code-reviewer.ts +++ b/scripts/code-reviewer.ts @@ -26,7 +26,7 @@ interface PRContext { // Config const MAX_DIFF_LINES = 10000; const CLIPROXY_URL = process.env.CLIPROXY_URL || 'http://localhost:8317'; -const MODEL = process.env.REVIEW_MODEL || 'claude-opus-4-6-thinking'; +const MODEL = process.env.REVIEW_MODEL || 'claude-opus-4-7-thinking'; // System prompt for code review - new style const CODE_REVIEWER_SYSTEM_PROMPT = `You are the CCS AGY Code Reviewer, an expert AI assistant reviewing pull requests for the CCS CLI project. diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 43ce4672..343bbcc8 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -41,7 +41,7 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges * v17: Persist routing.strategy from CCS unified config */ -export const CLIPROXY_CONFIG_VERSION = 17; +export const CLIPROXY_CONFIG_VERSION = 18; interface RegenerateConfigOptions { configPath?: string; @@ -86,6 +86,7 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ // Backward compatibility: legacy sonnet thinking alias now routes to canonical model ID. { name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6', fork: true }, { name: 'claude-opus-4-6-thinking', alias: 'claude-opus-4-6-thinking', fork: true }, + { name: 'claude-opus-4-7-thinking', alias: 'claude-opus-4-7-thinking', fork: true }, ]; const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index f4187873..a7fa5eac 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -87,12 +87,42 @@ export const MODEL_CATALOG: Partial> = agy: { provider: 'agy', displayName: 'Antigravity', - defaultModel: 'claude-opus-4-6-thinking', + defaultModel: 'claude-opus-4-7-thinking', models: [ + { + id: 'claude-opus-4-7-thinking', + name: 'Claude Opus 4.7 Thinking', + description: 'Latest flagship, extended thinking', + nativeImageInput: true, + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: true, + dynamicAllowed: true, + }, + // TODO: Re-enable when Antigravity backend supports 1M context (currently 256k) + // extendedContext: true, + extendedContext: false, + }, + { + id: 'claude-opus-4-7', + name: 'Claude Opus 4.7', + description: 'Latest flagship without thinking', + nativeImageInput: true, + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: true, + dynamicAllowed: true, + }, + extendedContext: false, + }, { id: 'claude-opus-4-6-thinking', name: 'Claude Opus 4.6 Thinking', - description: 'Latest flagship, extended thinking', + description: 'Previous flagship, extended thinking', nativeImageInput: true, thinking: { type: 'budget', @@ -290,10 +320,24 @@ export const MODEL_CATALOG: Partial> = displayName: 'Claude (Anthropic)', defaultModel: 'claude-sonnet-4-6', models: [ + { + id: 'claude-opus-4-7', + name: 'Claude Opus 4.7', + description: 'Latest flagship model', + nativeImageInput: true, + thinking: { + type: 'budget', + min: 1024, + max: 128000, + zeroAllowed: false, + dynamicAllowed: true, + }, + extendedContext: true, + }, { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', - description: 'Latest flagship model', + description: 'Previous flagship model', nativeImageInput: true, thinking: { type: 'budget', diff --git a/src/cursor/cursor-default-models.ts b/src/cursor/cursor-default-models.ts index 1457de05..1a7f3eb6 100644 --- a/src/cursor/cursor-default-models.ts +++ b/src/cursor/cursor-default-models.ts @@ -14,6 +14,16 @@ export const DEFAULT_CURSOR_MODEL = 'gpt-5.3-codex'; */ export const DEFAULT_CURSOR_MODELS: CursorModel[] = [ // Anthropic Models + { + id: 'claude-4.7-opus', + name: 'Claude 4.7 Opus', + provider: 'anthropic', + }, + { + id: 'claude-4.7-opus-fast-mode', + name: 'Claude 4.7 Opus (Fast mode)', + provider: 'anthropic', + }, { id: 'claude-4.6-opus', name: 'Claude 4.6 Opus', diff --git a/src/targets/droid-adapter.ts b/src/targets/droid-adapter.ts index c162f7f3..a2bac165 100644 --- a/src/targets/droid-adapter.ts +++ b/src/targets/droid-adapter.ts @@ -54,7 +54,7 @@ export class DroidAdapter implements TargetAdapter { model: creds.model, }); const modelRef = await upsertCcsModel(creds.profile, { - model: creds.model || 'claude-opus-4-6', + model: creds.model || 'claude-opus-4-7', displayName: `CCS ${creds.profile}`, baseUrl: creds.baseUrl, apiKey: creds.apiKey, diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index d8e40679..011ff444 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -228,6 +228,19 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 6.25, cacheReadPerMillion: 0.5, }, + // Claude 4.7 Opus ($5/$25) + 'claude-opus-4-7': { + inputPerMillion: 5.0, + outputPerMillion: 25.0, + cacheCreationPerMillion: 6.25, + cacheReadPerMillion: 0.5, + }, + 'claude-opus-4-7-thinking': { + inputPerMillion: 5.0, + outputPerMillion: 25.0, + cacheCreationPerMillion: 6.25, + cacheReadPerMillion: 0.5, + }, // --------------------------------------------------------------------------- // OpenAI Models - Source: better-ccusage diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 3e8f3751..36905d66 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -66,7 +66,21 @@ describe('Model Catalog', () => { describe('AGY models', () => { it('has correct default model', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-6-thinking'); + assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-7-thinking'); + }); + + it('includes Claude Opus 4.7 Thinking', () => { + const { MODEL_CATALOG } = modelCatalog; + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7-thinking'); + assert(opus, 'Should include Claude Opus 4.7 Thinking'); + assert.strictEqual(opus.name, 'Claude Opus 4.7 Thinking'); + }); + + it('includes Claude Opus 4.7 (non-thinking)', () => { + const { MODEL_CATALOG } = modelCatalog; + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7'); + assert(opus, 'Should include Claude Opus 4.7'); + assert.strictEqual(opus.name, 'Claude Opus 4.7'); }); it('includes Claude Opus 4.6 Thinking', () => { @@ -117,9 +131,9 @@ describe('Model Catalog', () => { assert.strictEqual(flash.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,6 +280,13 @@ describe('Model Catalog', () => { }); describe('findModel', () => { + it('finds Claude Opus 4.7 Thinking in agy', () => { + const { findModel } = modelCatalog; + const model = findModel('agy', 'claude-opus-4-7-thinking'); + assert(model, 'Should find model'); + assert.strictEqual(model.name, 'Claude Opus 4.7 Thinking'); + }); + it('finds Claude Opus 4.6 Thinking in agy', () => { const { findModel } = modelCatalog; const model = findModel('agy', 'claude-opus-4-6-thinking'); @@ -310,10 +331,10 @@ describe('Model Catalog', () => { it('falls back to the next supported model when the default is excluded', () => { const { getSuggestedReplacementModel } = modelCatalog; - expect(getSuggestedReplacementModel('agy', 'claude-opus-4-6-thinking')).toBe( - 'claude-sonnet-4-6' + expect(getSuggestedReplacementModel('agy', 'claude-opus-4-7-thinking')).toBe( + 'claude-opus-4-7' ); - expect(getSuggestedReplacementModel('agy')).toBe('claude-opus-4-6-thinking'); + expect(getSuggestedReplacementModel('agy')).toBe('claude-opus-4-7-thinking'); }); it('returns undefined when no provider catalog exists', () => { @@ -374,10 +395,10 @@ describe('Model Catalog', () => { }); describe('Thinking models ordering', () => { - it('Claude Opus 4.6 Thinking is not deprecated', () => { + it('Claude Opus 4.7 Thinking is not deprecated', () => { const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking'); - assert(opus, 'Should include Claude Opus 4.6 Thinking'); + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7-thinking'); + assert(opus, 'Should include Claude Opus 4.7 Thinking'); assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated'); }); @@ -397,14 +418,14 @@ describe('Model Catalog', () => { const models = MODEL_CATALOG.agy.models; // Find indices of thinking models - const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-6-thinking'); + const opus47Idx = models.findIndex((m) => m.id === 'claude-opus-4-7-thinking'); const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6'); // Find indices of the remaining non-Claude model const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-high'); // Primary Claude choices should appear ahead of Gemini fallback. - assert(opusIdx < geminiIdx, 'Opus should be above Gemini'); + assert(opus47Idx < geminiIdx, 'Opus 4.7 should be above Gemini'); assert(sonnetThinkingIdx < geminiIdx, 'Sonnet should be above Gemini'); }); }); @@ -412,6 +433,7 @@ describe('Model Catalog', () => { describe('isModelDeprecated', () => { it('returns false for active AGY Claude models', () => { const { isModelDeprecated } = modelCatalog; + assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-7-thinking'), false); assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-6-thinking'), false); assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false); }); @@ -430,6 +452,7 @@ describe('Model Catalog', () => { describe('getModelDeprecationReason', () => { it('returns undefined for active AGY Claude models', () => { const { getModelDeprecationReason } = modelCatalog; + assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-7-thinking'), undefined); assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-6-thinking'), undefined); assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined); }); diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 470ced04..c73354fc 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -148,6 +148,24 @@ describe('model-pricing', () => { expect(opus46.inputPerMillion).toBe(5.0); expect(opus46.outputPerMillion).toBe(25.0); }); + + it('should return correct pricing for Claude Opus 4.7', () => { + const opus47 = getModelPricing('claude-opus-4-7'); + expect(opus47.inputPerMillion).toBe(5.0); + expect(opus47.outputPerMillion).toBe(25.0); + }); + + it('should return correct pricing for Claude Opus 4.7 thinking variant', () => { + const opus47t = getModelPricing('claude-opus-4-7-thinking'); + expect(opus47t.inputPerMillion).toBe(5.0); + expect(opus47t.outputPerMillion).toBe(25.0); + }); + + it('should match date-stamped Claude Opus 4.7 to correct pricing', () => { + const opus47dated = getModelPricing('claude-opus-4-7-20260401'); + expect(opus47dated.inputPerMillion).toBe(5.0); + expect(opus47dated.outputPerMillion).toBe(25.0); + }); }); describe('calculateCost', () => { From faca421705ba7279e3c2ed3a74afca51b88d770c Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 18:14:15 +0200 Subject: [PATCH 12/17] docs(cliproxy): document v18 config bump for Opus 4.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PResto review (atrvd/ccs#8, issue #1): bumping CLIPROXY_CONFIG_VERSION 17→18 was missing a changelog entry in the header comment. Adds the v18 line to preserve regeneration history. Built [OnSteroids](https://onsteroids.ai) --- src/cliproxy/config/generator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 343bbcc8..e64c4f8c 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -40,6 +40,7 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v15: Prune stale generated Antigravity Gemini preview aliases during regeneration * v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges * v17: Persist routing.strategy from CCS unified config + * v18: Added Claude Opus 4.7 Thinking alias and promoted to default AGY model */ export const CLIPROXY_CONFIG_VERSION = 18; From 0d09199439d335d948ac1a7e48e00cac5b18be27 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 18:43:15 +0200 Subject: [PATCH 13/17] fix(cliproxy): address Presto review feedback on Opus 4.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model-catalog: change `claude-opus-4-7` description from "Latest flagship without thinking" to "Latest flagship model". The model supports thinking budget (min 1024, max 128000) but doesn't enable it by default; the old wording implied no thinking support, which was misleading. Aligns with the `claude` provider's description. - generator: add DEFAULT_ANTIGRAVITY_ALIASES entry for non-thinking `claude-opus-4-7`, restoring symmetry with `claude-sonnet-4-6` (which has both -thinking and non-thinking aliases). Bump CLIPROXY_CONFIG_VERSION 18 → 19 to trigger regeneration on upgrade. Addresses PResto atrvd/ccs#8 issues #2 (important) and #3 (suggestion). Built [OnSteroids](https://onsteroids.ai) --- src/cliproxy/config/generator.ts | 4 +++- src/cliproxy/model-catalog.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index e64c4f8c..7ea5abaf 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -41,8 +41,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges * v17: Persist routing.strategy from CCS unified config * v18: Added Claude Opus 4.7 Thinking alias and promoted to default AGY model + * v19: Added Claude Opus 4.7 non-thinking alias for symmetry with Sonnet */ -export const CLIPROXY_CONFIG_VERSION = 18; +export const CLIPROXY_CONFIG_VERSION = 19; interface RegenerateConfigOptions { configPath?: string; @@ -88,6 +89,7 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ { name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6', fork: true }, { name: 'claude-opus-4-6-thinking', alias: 'claude-opus-4-6-thinking', fork: true }, { name: 'claude-opus-4-7-thinking', alias: 'claude-opus-4-7-thinking', fork: true }, + { name: 'claude-opus-4-7', alias: 'claude-opus-4-7', fork: true }, ]; const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index a7fa5eac..f2038161 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -108,7 +108,7 @@ export const MODEL_CATALOG: Partial> = { id: 'claude-opus-4-7', name: 'Claude Opus 4.7', - description: 'Latest flagship without thinking', + description: 'Latest flagship model', nativeImageInput: true, thinking: { type: 'budget', From 7492688effa2ab2dd12094dc4b6d9f7f2e8d909b Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 19:00:39 +0200 Subject: [PATCH 14/17] test(cliproxy): cover Claude Opus 4.7 in Claude provider catalog Adds a unit test verifying that `claude-opus-4-7` is registered in the `claude` provider with its provider-specific settings: - `thinking.zeroAllowed: false` (contrasts with AGY where it is true) - `extendedContext: true` (1M context via Anthropic API) Addresses PResto atrvd/ccs#8 issue #4 (suggestion). Built [OnSteroids](https://onsteroids.ai) --- tests/unit/cliproxy/model-catalog.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 36905d66..35ec26c5 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -156,6 +156,17 @@ describe('Model Catalog', () => { assert.strictEqual(sonnet.name, 'Claude Sonnet 4.6'); }); + it('includes Claude Opus 4.7 with provider-specific settings', () => { + const { MODEL_CATALOG } = modelCatalog; + const opus47 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-4-7'); + assert(opus47, 'Should include Claude Opus 4.7'); + assert.strictEqual(opus47.name, 'Claude Opus 4.7'); + // Claude provider differs from AGY: zero thinking budget is disallowed, + // and extended (1M) context is available on the Anthropic API. + assert.strictEqual(opus47.thinking.zeroAllowed, false); + assert.strictEqual(opus47.extendedContext, true); + }); + it('retains previous 4.5 snapshot models for explicit selection', () => { const { MODEL_CATALOG } = modelCatalog; const ids = MODEL_CATALOG.claude.models.map((m) => m.id); From de0f8b32642c2eda34efc8d06317f91ee00957ba Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 19:24:43 +0200 Subject: [PATCH 15/17] test(model-pricing): add cache pricing coverage for Opus 4.7 Mirrors the existing `claude-opus-4-6` cache pricing test for both `claude-opus-4-7` and `claude-opus-4-7-thinking` to explicitly validate cache creation/read rates in end-to-end cost calculation. Addresses PResto atrvd/ccs#8 issue #6 (suggestion). Built [OnSteroids](https://onsteroids.ai) --- tests/unit/model-pricing.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index c73354fc..5abe6cb9 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -236,6 +236,28 @@ describe('model-pricing', () => { const cost = calculateCost(usage, 'claude-opus-4-6'); expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 }); + + it('should calculate Claude Opus 4.7 cost including cache token rates', () => { + const usage: TokenUsage = { + inputTokens: 1_000_000, + outputTokens: 1_000_000, + cacheCreationTokens: 1_000_000, + cacheReadTokens: 1_000_000, + }; + const cost = calculateCost(usage, 'claude-opus-4-7'); + expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 + }); + + it('should calculate Claude Opus 4.7 Thinking cost including cache token rates', () => { + const usage: TokenUsage = { + inputTokens: 1_000_000, + outputTokens: 1_000_000, + cacheCreationTokens: 1_000_000, + cacheReadTokens: 1_000_000, + }; + const cost = calculateCost(usage, 'claude-opus-4-7-thinking'); + expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 + }); }); describe('getKnownModels', () => { From 571538dc1c3cf08929705f3efde9d944ba48de59 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 20 Apr 2026 13:26:22 -0400 Subject: [PATCH 16/17] fix(cliproxy): scope Opus 4.7 rollout to verified providers --- CHANGELOG.md | 30 ---------------- package.json | 2 +- scripts/code-reviewer.ts | 2 +- src/cliproxy/config/generator.ts | 6 +--- src/cliproxy/model-catalog.ts | 34 ++---------------- src/cursor/cursor-default-models.ts | 2 +- src/targets/droid-adapter.ts | 2 +- src/web-server/model-pricing.ts | 6 ---- tests/unit/cliproxy/model-catalog.test.js | 44 ++++++++--------------- tests/unit/cursor/cursor-models.test.ts | 8 +++++ tests/unit/model-pricing.test.ts | 6 ---- 11 files changed, 29 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd2b3f0..7fd1a8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,3 @@ -## [7.73.0](https://github.com/kaitranntt/ccs/compare/v7.72.1...v7.73.0) (2026-04-19) - -### Features - -* **proxy:** enhance Anthropic-to-OpenAI message transformation and schema sanitization ([2672e35](https://github.com/kaitranntt/ccs/commit/2672e35362938b054710de681760bd24c3c39e19)) - -### Bug Fixes - -* **browser:** bootstrap managed attach profile setup ([b525033](https://github.com/kaitranntt/ccs/commit/b52503300b0c434fbb9d88283643d3ceaa673d44)) -* **cliproxy:** preserve legacy openai-compat connectors on restart ([16f81fc](https://github.com/kaitranntt/ccs/commit/16f81fc8a595792f1c1a6df060f88cdf28d7afb6)) -* **codex:** probe Windows cmd wrappers via cmd shell ([210ec33](https://github.com/kaitranntt/ccs/commit/210ec33c04fe88be2b651c2163f638085b1b6bd2)) -* **proxy:** harden Anthropic request transformation semantics ([ebc9219](https://github.com/kaitranntt/ccs/commit/ebc92194bb9bf2810e10f64de1fa6e599c2fa156)) -* **proxy:** preserve valid enums during schema normalization ([22ab58b](https://github.com/kaitranntt/ccs/commit/22ab58b02e5dc34f152ecce048c1fbf7d615bdfb)) -* **proxy:** restore strict Anthropic message validation ([32d6bfd](https://github.com/kaitranntt/ccs/commit/32d6bfdda7881a42a376d0973c021f0b47eed7e2)) -* **repo:** remove tracked plans workspace files ([bc4ab98](https://github.com/kaitranntt/ccs/commit/bc4ab98af41e9c9f049cc5da2e6ff356832ef14b)) -* **targets:** use cmd.exe for escaped Windows wrapper launches ([a945b0b](https://github.com/kaitranntt/ccs/commit/a945b0b1047b85a7b343dfe35d8ae9680346e25f)) -* **windows:** align escaped wrapper shell handling ([ccdd0b6](https://github.com/kaitranntt/ccs/commit/ccdd0b6e8e4185ec2b01186cfc69e28f171d0906)) - -### Tests - -* **cliproxy:** assert routing persistence via unified config loader ([485fe4b](https://github.com/kaitranntt/ccs/commit/485fe4ba1c5878083c6e2b6a2d98b37208772a1f)) -* **cliproxy:** cover legacy connector removal cleanup ([00d902b](https://github.com/kaitranntt/ccs/commit/00d902b6e64839cd6b89446cc5dee540f2a059b8)) -* **cliproxy:** isolate routing strategy service state ([5e1e807](https://github.com/kaitranntt/ccs/commit/5e1e8070e821dbe660ea77bc96fec610937ed44a)) -* **websearch:** clear CCS_PROFILE_TYPE in SearXNG fallback spawn env ([e25d791](https://github.com/kaitranntt/ccs/commit/e25d791d9bee38e7143757d37c42bd850e471ae8)) - -### CI - -* add concurrency group and split build/test with artifact sharing ([ad54e17](https://github.com/kaitranntt/ccs/commit/ad54e179b6a3921fd26ad3ac585f5ef965b01c1b)) -* **pre-push:** skip gate for delete-only pushes ([6236cb7](https://github.com/kaitranntt/ccs/commit/6236cb7feb15e954f7f65ae98e6fd98b323a4adb)) - ## [7.72.1](https://github.com/kaitranntt/ccs/compare/v7.72.0...v7.72.1) (2026-04-17) ### Bug Fixes diff --git a/package.json b/package.json index 2ec0516e..f2ffe18a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.73.0", + "version": "7.72.1-dev.7", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli", diff --git a/scripts/code-reviewer.ts b/scripts/code-reviewer.ts index 02dda7f6..89d7a829 100755 --- a/scripts/code-reviewer.ts +++ b/scripts/code-reviewer.ts @@ -26,7 +26,7 @@ interface PRContext { // Config const MAX_DIFF_LINES = 10000; const CLIPROXY_URL = process.env.CLIPROXY_URL || 'http://localhost:8317'; -const MODEL = process.env.REVIEW_MODEL || 'claude-opus-4-7-thinking'; +const MODEL = process.env.REVIEW_MODEL || 'claude-opus-4-6-thinking'; // System prompt for code review - new style const CODE_REVIEWER_SYSTEM_PROMPT = `You are the CCS AGY Code Reviewer, an expert AI assistant reviewing pull requests for the CCS CLI project. diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 7ea5abaf..43ce4672 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -40,10 +40,8 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v15: Prune stale generated Antigravity Gemini preview aliases during regeneration * v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges * v17: Persist routing.strategy from CCS unified config - * v18: Added Claude Opus 4.7 Thinking alias and promoted to default AGY model - * v19: Added Claude Opus 4.7 non-thinking alias for symmetry with Sonnet */ -export const CLIPROXY_CONFIG_VERSION = 19; +export const CLIPROXY_CONFIG_VERSION = 17; interface RegenerateConfigOptions { configPath?: string; @@ -88,8 +86,6 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ // Backward compatibility: legacy sonnet thinking alias now routes to canonical model ID. { name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6', fork: true }, { name: 'claude-opus-4-6-thinking', alias: 'claude-opus-4-6-thinking', fork: true }, - { name: 'claude-opus-4-7-thinking', alias: 'claude-opus-4-7-thinking', fork: true }, - { name: 'claude-opus-4-7', alias: 'claude-opus-4-7', fork: true }, ]; const BUILT_IN_GEMINI_ALIAS_NAMES = new Set( diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index f2038161..6c19ab7c 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -87,42 +87,12 @@ export const MODEL_CATALOG: Partial> = agy: { provider: 'agy', displayName: 'Antigravity', - defaultModel: 'claude-opus-4-7-thinking', + defaultModel: 'claude-opus-4-6-thinking', models: [ - { - id: 'claude-opus-4-7-thinking', - name: 'Claude Opus 4.7 Thinking', - description: 'Latest flagship, extended thinking', - nativeImageInput: true, - thinking: { - type: 'budget', - min: 1024, - max: 128000, - zeroAllowed: true, - dynamicAllowed: true, - }, - // TODO: Re-enable when Antigravity backend supports 1M context (currently 256k) - // extendedContext: true, - extendedContext: false, - }, - { - id: 'claude-opus-4-7', - name: 'Claude Opus 4.7', - description: 'Latest flagship model', - nativeImageInput: true, - thinking: { - type: 'budget', - min: 1024, - max: 128000, - zeroAllowed: true, - dynamicAllowed: true, - }, - extendedContext: false, - }, { id: 'claude-opus-4-6-thinking', name: 'Claude Opus 4.6 Thinking', - description: 'Previous flagship, extended thinking', + description: 'Latest flagship, extended thinking', nativeImageInput: true, thinking: { type: 'budget', diff --git a/src/cursor/cursor-default-models.ts b/src/cursor/cursor-default-models.ts index 1a7f3eb6..01cc05c6 100644 --- a/src/cursor/cursor-default-models.ts +++ b/src/cursor/cursor-default-models.ts @@ -10,7 +10,7 @@ export const DEFAULT_CURSOR_MODEL = 'gpt-5.3-codex'; /** * Default models available through Cursor IDE. * Used as fallback when daemon is not reachable. - * Source: Cursor docs model catalog (Feb 2026) + * Source: Cursor docs / official announcements (Apr 2026) */ export const DEFAULT_CURSOR_MODELS: CursorModel[] = [ // Anthropic Models diff --git a/src/targets/droid-adapter.ts b/src/targets/droid-adapter.ts index a2bac165..c162f7f3 100644 --- a/src/targets/droid-adapter.ts +++ b/src/targets/droid-adapter.ts @@ -54,7 +54,7 @@ export class DroidAdapter implements TargetAdapter { model: creds.model, }); const modelRef = await upsertCcsModel(creds.profile, { - model: creds.model || 'claude-opus-4-7', + model: creds.model || 'claude-opus-4-6', displayName: `CCS ${creds.profile}`, baseUrl: creds.baseUrl, apiKey: creds.apiKey, diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index 011ff444..93da148b 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -235,12 +235,6 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 6.25, cacheReadPerMillion: 0.5, }, - 'claude-opus-4-7-thinking': { - inputPerMillion: 5.0, - outputPerMillion: 25.0, - cacheCreationPerMillion: 6.25, - cacheReadPerMillion: 0.5, - }, // --------------------------------------------------------------------------- // OpenAI Models - Source: better-ccusage diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 35ec26c5..5023bf32 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -66,21 +66,7 @@ describe('Model Catalog', () => { describe('AGY models', () => { it('has correct default model', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-7-thinking'); - }); - - it('includes Claude Opus 4.7 Thinking', () => { - const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7-thinking'); - assert(opus, 'Should include Claude Opus 4.7 Thinking'); - assert.strictEqual(opus.name, 'Claude Opus 4.7 Thinking'); - }); - - it('includes Claude Opus 4.7 (non-thinking)', () => { - const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7'); - assert(opus, 'Should include Claude Opus 4.7'); - assert.strictEqual(opus.name, 'Claude Opus 4.7'); + assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-6-thinking'); }); it('includes Claude Opus 4.6 Thinking', () => { @@ -131,9 +117,9 @@ describe('Model Catalog', () => { assert.strictEqual(flash.tier, undefined, 'AGY models should not have paid tier'); }); - it('has 7 models total', () => { + it('has 5 models total', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.agy.models.length, 7); + assert.strictEqual(MODEL_CATALOG.agy.models.length, 5); }); }); @@ -291,11 +277,11 @@ describe('Model Catalog', () => { }); describe('findModel', () => { - it('finds Claude Opus 4.7 Thinking in agy', () => { + it('finds Claude Opus 4.7 in claude', () => { const { findModel } = modelCatalog; - const model = findModel('agy', 'claude-opus-4-7-thinking'); + const model = findModel('claude', 'claude-opus-4-7'); assert(model, 'Should find model'); - assert.strictEqual(model.name, 'Claude Opus 4.7 Thinking'); + assert.strictEqual(model.name, 'Claude Opus 4.7'); }); it('finds Claude Opus 4.6 Thinking in agy', () => { @@ -342,10 +328,10 @@ describe('Model Catalog', () => { it('falls back to the next supported model when the default is excluded', () => { const { getSuggestedReplacementModel } = modelCatalog; - expect(getSuggestedReplacementModel('agy', 'claude-opus-4-7-thinking')).toBe( - 'claude-opus-4-7' + expect(getSuggestedReplacementModel('agy', 'claude-opus-4-6-thinking')).toBe( + 'claude-sonnet-4-6' ); - expect(getSuggestedReplacementModel('agy')).toBe('claude-opus-4-7-thinking'); + expect(getSuggestedReplacementModel('agy')).toBe('claude-opus-4-6-thinking'); }); it('returns undefined when no provider catalog exists', () => { @@ -406,10 +392,10 @@ describe('Model Catalog', () => { }); describe('Thinking models ordering', () => { - it('Claude Opus 4.7 Thinking is not deprecated', () => { + it('Claude Opus 4.6 Thinking is not deprecated', () => { const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-7-thinking'); - assert(opus, 'Should include Claude Opus 4.7 Thinking'); + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking'); + assert(opus, 'Should include Claude Opus 4.6 Thinking'); assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated'); }); @@ -429,14 +415,14 @@ describe('Model Catalog', () => { const models = MODEL_CATALOG.agy.models; // Find indices of thinking models - const opus47Idx = models.findIndex((m) => m.id === 'claude-opus-4-7-thinking'); + const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-6-thinking'); const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6'); // Find indices of the remaining non-Claude model const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-high'); // Primary Claude choices should appear ahead of Gemini fallback. - assert(opus47Idx < geminiIdx, 'Opus 4.7 should be above Gemini'); + assert(opusIdx < geminiIdx, 'Opus should be above Gemini'); assert(sonnetThinkingIdx < geminiIdx, 'Sonnet should be above Gemini'); }); }); @@ -444,7 +430,6 @@ describe('Model Catalog', () => { describe('isModelDeprecated', () => { it('returns false for active AGY Claude models', () => { const { isModelDeprecated } = modelCatalog; - assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-7-thinking'), false); assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-6-thinking'), false); assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false); }); @@ -463,7 +448,6 @@ describe('Model Catalog', () => { describe('getModelDeprecationReason', () => { it('returns undefined for active AGY Claude models', () => { const { getModelDeprecationReason } = modelCatalog; - assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-7-thinking'), undefined); assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-6-thinking'), undefined); assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined); }); diff --git a/tests/unit/cursor/cursor-models.test.ts b/tests/unit/cursor/cursor-models.test.ts index ce6d6635..dddea9e8 100644 --- a/tests/unit/cursor/cursor-models.test.ts +++ b/tests/unit/cursor/cursor-models.test.ts @@ -26,6 +26,13 @@ describe('DEFAULT_CURSOR_MODELS', () => { expect(providers.has('google')).toBe(true); }); + it('keeps both Claude 4.6 Opus and Claude 4.7 Opus in the fallback catalog', () => { + const ids = DEFAULT_CURSOR_MODELS.map((model) => model.id); + expect(ids).toContain('claude-4.6-opus'); + expect(ids).toContain('claude-4.7-opus'); + expect(ids).toContain('claude-4.7-opus-fast-mode'); + }); + it('has exactly one default model', () => { const defaults = DEFAULT_CURSOR_MODELS.filter((m) => m.isDefault); expect(defaults).toHaveLength(1); @@ -88,6 +95,7 @@ describe('detectProvider', () => { it('detects anthropic models', () => { expect(detectProvider('claude-4.5-sonnet')).toBe('anthropic'); expect(detectProvider('claude-4.6-opus')).toBe('anthropic'); + expect(detectProvider('claude-4.7-opus')).toBe('anthropic'); }); it('detects openai models', () => { diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 5abe6cb9..31ebb4c2 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -155,12 +155,6 @@ describe('model-pricing', () => { expect(opus47.outputPerMillion).toBe(25.0); }); - it('should return correct pricing for Claude Opus 4.7 thinking variant', () => { - const opus47t = getModelPricing('claude-opus-4-7-thinking'); - expect(opus47t.inputPerMillion).toBe(5.0); - expect(opus47t.outputPerMillion).toBe(25.0); - }); - it('should match date-stamped Claude Opus 4.7 to correct pricing', () => { const opus47dated = getModelPricing('claude-opus-4-7-20260401'); expect(opus47dated.inputPerMillion).toBe(5.0); From 21ec661106b649c0ff626501a71a461478a63edd Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 20 Apr 2026 13:34:59 -0400 Subject: [PATCH 17/17] test(model-pricing): align Opus 4.7 cache coverage --- tests/unit/model-pricing.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 31ebb4c2..2770439f 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -242,14 +242,14 @@ describe('model-pricing', () => { expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 }); - it('should calculate Claude Opus 4.7 Thinking cost including cache token rates', () => { + it('should calculate Claude Opus 4.7 cache cost consistently across repeat lookups', () => { const usage: TokenUsage = { inputTokens: 1_000_000, outputTokens: 1_000_000, cacheCreationTokens: 1_000_000, cacheReadTokens: 1_000_000, }; - const cost = calculateCost(usage, 'claude-opus-4-7-thinking'); + const cost = calculateCost(usage, 'claude-opus-4-7'); expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 }); });