From 61058123e10de6ed21cadc0d1450b08cd28df6db Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Mon, 20 Apr 2026 16:29:16 +0200 Subject: [PATCH] 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', () => {