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. 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/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index f4187873..6c19ab7c 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -290,10 +290,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..01cc05c6 100644 --- a/src/cursor/cursor-default-models.ts +++ b/src/cursor/cursor-default-models.ts @@ -10,10 +10,20 @@ 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 + { + 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/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index d8e40679..93da148b 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -228,6 +228,13 @@ 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, + }, // --------------------------------------------------------------------------- // 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..5023bf32 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -142,6 +142,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); @@ -266,6 +277,13 @@ describe('Model Catalog', () => { }); describe('findModel', () => { + it('finds Claude Opus 4.7 in claude', () => { + const { findModel } = modelCatalog; + const model = findModel('claude', 'claude-opus-4-7'); + assert(model, 'Should find model'); + assert.strictEqual(model.name, 'Claude Opus 4.7'); + }); + it('finds Claude Opus 4.6 Thinking in agy', () => { const { findModel } = modelCatalog; const model = findModel('agy', 'claude-opus-4-6-thinking'); 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 470ced04..2770439f 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -148,6 +148,18 @@ 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 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', () => { @@ -218,6 +230,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 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'); + expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5 + }); }); describe('getKnownModels', () => {