mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
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)
This commit is contained in:
1 parent
dea704c57d
commit
61058123e1
9 files changed
+127
-18
No files matched your search
@@ -3,7 +3,7 @@
|
|||||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/claude",
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/claude",
|
||||||
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||||
"ANTHROPIC_MODEL": "claude-sonnet-4-6",
|
"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_SONNET_MODEL": "claude-sonnet-4-6",
|
||||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001"
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface PRContext {
|
|||||||
// Config
|
// Config
|
||||||
const MAX_DIFF_LINES = 10000;
|
const MAX_DIFF_LINES = 10000;
|
||||||
const CLIPROXY_URL = process.env.CLIPROXY_URL || 'http://localhost:8317';
|
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
|
// 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.
|
const CODE_REVIEWER_SYSTEM_PROMPT = `You are the CCS AGY Code Reviewer, an expert AI assistant reviewing pull requests for the CCS CLI project.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs';
|
|||||||
* v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges
|
* v16: Narrow stale Gemini alias cleanup to broad multi-version guessed ranges
|
||||||
* v17: Persist routing.strategy from CCS unified config
|
* v17: Persist routing.strategy from CCS unified config
|
||||||
*/
|
*/
|
||||||
export const CLIPROXY_CONFIG_VERSION = 17;
|
export const CLIPROXY_CONFIG_VERSION = 18;
|
||||||
|
|
||||||
interface RegenerateConfigOptions {
|
interface RegenerateConfigOptions {
|
||||||
configPath?: string;
|
configPath?: string;
|
||||||
@@ -86,6 +86,7 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [
|
|||||||
// Backward compatibility: legacy sonnet thinking alias now routes to canonical model ID.
|
// 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-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-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(
|
const BUILT_IN_GEMINI_ALIAS_NAMES = new Set(
|
||||||
|
|||||||
@@ -87,12 +87,42 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
|||||||
agy: {
|
agy: {
|
||||||
provider: 'agy',
|
provider: 'agy',
|
||||||
displayName: 'Antigravity',
|
displayName: 'Antigravity',
|
||||||
defaultModel: 'claude-opus-4-6-thinking',
|
defaultModel: 'claude-opus-4-7-thinking',
|
||||||
models: [
|
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',
|
id: 'claude-opus-4-6-thinking',
|
||||||
name: 'Claude Opus 4.6 Thinking',
|
name: 'Claude Opus 4.6 Thinking',
|
||||||
description: 'Latest flagship, extended thinking',
|
description: 'Previous flagship, extended thinking',
|
||||||
nativeImageInput: true,
|
nativeImageInput: true,
|
||||||
thinking: {
|
thinking: {
|
||||||
type: 'budget',
|
type: 'budget',
|
||||||
@@ -290,10 +320,24 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
|||||||
displayName: 'Claude (Anthropic)',
|
displayName: 'Claude (Anthropic)',
|
||||||
defaultModel: 'claude-sonnet-4-6',
|
defaultModel: 'claude-sonnet-4-6',
|
||||||
models: [
|
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',
|
id: 'claude-opus-4-6',
|
||||||
name: 'Claude Opus 4.6',
|
name: 'Claude Opus 4.6',
|
||||||
description: 'Latest flagship model',
|
description: 'Previous flagship model',
|
||||||
nativeImageInput: true,
|
nativeImageInput: true,
|
||||||
thinking: {
|
thinking: {
|
||||||
type: 'budget',
|
type: 'budget',
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ export const DEFAULT_CURSOR_MODEL = 'gpt-5.3-codex';
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_CURSOR_MODELS: CursorModel[] = [
|
export const DEFAULT_CURSOR_MODELS: CursorModel[] = [
|
||||||
// Anthropic Models
|
// 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',
|
id: 'claude-4.6-opus',
|
||||||
name: 'Claude 4.6 Opus',
|
name: 'Claude 4.6 Opus',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class DroidAdapter implements TargetAdapter {
|
|||||||
model: creds.model,
|
model: creds.model,
|
||||||
});
|
});
|
||||||
const modelRef = await upsertCcsModel(creds.profile, {
|
const modelRef = await upsertCcsModel(creds.profile, {
|
||||||
model: creds.model || 'claude-opus-4-6',
|
model: creds.model || 'claude-opus-4-7',
|
||||||
displayName: `CCS ${creds.profile}`,
|
displayName: `CCS ${creds.profile}`,
|
||||||
baseUrl: creds.baseUrl,
|
baseUrl: creds.baseUrl,
|
||||||
apiKey: creds.apiKey,
|
apiKey: creds.apiKey,
|
||||||
|
|||||||
@@ -228,6 +228,19 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
|||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
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
|
// OpenAI Models - Source: better-ccusage
|
||||||
|
|||||||
@@ -66,7 +66,21 @@ describe('Model Catalog', () => {
|
|||||||
describe('AGY models', () => {
|
describe('AGY models', () => {
|
||||||
it('has correct default model', () => {
|
it('has correct default model', () => {
|
||||||
const { MODEL_CATALOG } = modelCatalog;
|
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', () => {
|
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');
|
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;
|
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', () => {
|
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', () => {
|
it('finds Claude Opus 4.6 Thinking in agy', () => {
|
||||||
const { findModel } = modelCatalog;
|
const { findModel } = modelCatalog;
|
||||||
const model = findModel('agy', 'claude-opus-4-6-thinking');
|
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', () => {
|
it('falls back to the next supported model when the default is excluded', () => {
|
||||||
const { getSuggestedReplacementModel } = modelCatalog;
|
const { getSuggestedReplacementModel } = modelCatalog;
|
||||||
|
|
||||||
expect(getSuggestedReplacementModel('agy', 'claude-opus-4-6-thinking')).toBe(
|
expect(getSuggestedReplacementModel('agy', 'claude-opus-4-7-thinking')).toBe(
|
||||||
'claude-sonnet-4-6'
|
'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', () => {
|
it('returns undefined when no provider catalog exists', () => {
|
||||||
@@ -374,10 +395,10 @@ describe('Model Catalog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Thinking models ordering', () => {
|
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 { MODEL_CATALOG } = modelCatalog;
|
||||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === '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.6 Thinking');
|
assert(opus, 'Should include Claude Opus 4.7 Thinking');
|
||||||
assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated');
|
assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,14 +418,14 @@ describe('Model Catalog', () => {
|
|||||||
const models = MODEL_CATALOG.agy.models;
|
const models = MODEL_CATALOG.agy.models;
|
||||||
|
|
||||||
// Find indices of thinking 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');
|
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6');
|
||||||
|
|
||||||
// Find indices of the remaining non-Claude model
|
// Find indices of the remaining non-Claude model
|
||||||
const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-high');
|
const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-high');
|
||||||
|
|
||||||
// Primary Claude choices should appear ahead of Gemini fallback.
|
// 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');
|
assert(sonnetThinkingIdx < geminiIdx, 'Sonnet should be above Gemini');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -412,6 +433,7 @@ describe('Model Catalog', () => {
|
|||||||
describe('isModelDeprecated', () => {
|
describe('isModelDeprecated', () => {
|
||||||
it('returns false for active AGY Claude models', () => {
|
it('returns false for active AGY Claude models', () => {
|
||||||
const { isModelDeprecated } = modelCatalog;
|
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-opus-4-6-thinking'), false);
|
||||||
assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false);
|
assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false);
|
||||||
});
|
});
|
||||||
@@ -430,6 +452,7 @@ describe('Model Catalog', () => {
|
|||||||
describe('getModelDeprecationReason', () => {
|
describe('getModelDeprecationReason', () => {
|
||||||
it('returns undefined for active AGY Claude models', () => {
|
it('returns undefined for active AGY Claude models', () => {
|
||||||
const { getModelDeprecationReason } = modelCatalog;
|
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-opus-4-6-thinking'), undefined);
|
||||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined);
|
assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,6 +148,24 @@ describe('model-pricing', () => {
|
|||||||
expect(opus46.inputPerMillion).toBe(5.0);
|
expect(opus46.inputPerMillion).toBe(5.0);
|
||||||
expect(opus46.outputPerMillion).toBe(25.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', () => {
|
describe('calculateCost', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user