mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
feat(catalog): add Claude Opus 4.8 to Anthropic model registry
Register `claude-opus-4-8` in the cliproxy model catalog, web-server pricing table, and UI catalog as an additive entry next to 4.7. Defaults are unchanged (claude provider stays on sonnet-4-6); users opt into 4.8 explicitly via ANTHROPIC_MODEL or the dashboard. Configuration mirrors 4.7: - adaptive thinking levels (low through max), max distinct from xhigh - extendedContext (1M) available - nativeImageInput - pricing $5/$25 in/out, $6.25/$0.5 cache Built [OnSteroids](https://onsteroids.ai)
This commit is contained in:
@@ -155,6 +155,19 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(opus47.extendedContext, true);
|
||||
});
|
||||
|
||||
it('includes Claude Opus 4.8 with adaptive levels and extended context', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const opus48 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-4-8');
|
||||
assert(opus48, 'Should include Claude Opus 4.8');
|
||||
assert.strictEqual(opus48.name, 'Claude Opus 4.8');
|
||||
// Mirrors 4.7: Anthropic only accepts adaptive thinking levels on the
|
||||
// current Opus generation; budget_tokens is rejected with 400.
|
||||
assert.strictEqual(opus48.thinking.type, 'levels');
|
||||
assert.deepStrictEqual(opus48.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']);
|
||||
assert.strictEqual(opus48.thinking.maxLevel, 'max');
|
||||
assert.strictEqual(opus48.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);
|
||||
|
||||
@@ -51,6 +51,15 @@ describe('Thinking Validator', () => {
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should treat max as a distinct top tier on Opus 4.8', () => {
|
||||
// Opus 4.8 inherits 4.7's adaptive thinking surface; max must remain
|
||||
// distinct from xhigh.
|
||||
const result = validateThinking('claude', 'claude-opus-4-8', 'max');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.value).toBe('max');
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should still alias max -> xhigh for models without a max level (backcompat)', () => {
|
||||
// Codex catalog uses ['low','medium','high','xhigh'] with maxLevel 'xhigh'.
|
||||
// User input "max" should map down to xhigh rather than be rejected.
|
||||
|
||||
@@ -307,10 +307,25 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
displayName: 'Claude (Anthropic)',
|
||||
defaultModel: 'claude-sonnet-4-6',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-8',
|
||||
name: 'Claude Opus 4.8',
|
||||
description: 'Latest flagship model',
|
||||
nativeImageInput: true,
|
||||
// Mirrors 4.7: Anthropic accepts only adaptive thinking levels on the
|
||||
// current Opus generation; manual budget_tokens is rejected with 400.
|
||||
thinking: {
|
||||
type: 'levels',
|
||||
levels: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||||
maxLevel: 'max',
|
||||
dynamicAllowed: true,
|
||||
},
|
||||
extendedContext: true,
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-7',
|
||||
name: 'Claude Opus 4.7',
|
||||
description: 'Latest flagship model',
|
||||
description: 'Previous flagship model',
|
||||
nativeImageInput: true,
|
||||
// Opus 4.7 only supports adaptive thinking on the Anthropic API; manual
|
||||
// thinking.type: "enabled" with budget_tokens is rejected with 400.
|
||||
@@ -327,7 +342,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
name: 'Claude Opus 4.6',
|
||||
description: 'Previous flagship model',
|
||||
description: 'Older flagship model',
|
||||
nativeImageInput: true,
|
||||
thinking: {
|
||||
type: 'budget',
|
||||
|
||||
@@ -243,6 +243,13 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
||||
cacheCreationPerMillion: 6.25,
|
||||
cacheReadPerMillion: 0.5,
|
||||
},
|
||||
// Claude 4.8 Opus ($5/$25)
|
||||
'claude-opus-4-8': {
|
||||
inputPerMillion: 5.0,
|
||||
outputPerMillion: 25.0,
|
||||
cacheCreationPerMillion: 6.25,
|
||||
cacheReadPerMillion: 0.5,
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAI Models - Source: better-ccusage
|
||||
|
||||
@@ -168,6 +168,14 @@ describe('model-pricing', () => {
|
||||
expect(opus47dated.outputPerMillion).toBe(25.0);
|
||||
});
|
||||
|
||||
it('should return correct pricing for Claude Opus 4.8', () => {
|
||||
const opus48 = getModelPricing('claude-opus-4-8');
|
||||
expect(opus48.inputPerMillion).toBe(5.0);
|
||||
expect(opus48.outputPerMillion).toBe(25.0);
|
||||
expect(opus48.cacheCreationPerMillion).toBe(6.25);
|
||||
expect(opus48.cacheReadPerMillion).toBe(0.5);
|
||||
});
|
||||
|
||||
it('should not map unknown future model families onto known family pricing', () => {
|
||||
const fallback = getModelPricing('unknown-model-xyz');
|
||||
|
||||
@@ -266,6 +274,17 @@ describe('model-pricing', () => {
|
||||
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.8 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-8');
|
||||
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
|
||||
});
|
||||
});
|
||||
|
||||
describe('getKnownModels', () => {
|
||||
@@ -469,17 +488,15 @@ describe('model-pricing', () => {
|
||||
});
|
||||
|
||||
it('gracefully ignores malformed cached model entries', () => {
|
||||
setCachedModelsDevRegistry(
|
||||
{
|
||||
openai: {
|
||||
id: 'openai',
|
||||
models: {
|
||||
'null-entry': null,
|
||||
'gpt-5.5': { id: 'gpt-5.5', cost: { input: 5, output: 30 } },
|
||||
},
|
||||
setCachedModelsDevRegistry({
|
||||
openai: {
|
||||
id: 'openai',
|
||||
models: {
|
||||
'null-entry': null,
|
||||
'gpt-5.5': { id: 'gpt-5.5', cost: { input: 5, output: 30 } },
|
||||
},
|
||||
} as unknown as Parameters<typeof setCachedModelsDevRegistry>[0]
|
||||
);
|
||||
},
|
||||
} as unknown as Parameters<typeof setCachedModelsDevRegistry>[0]);
|
||||
|
||||
expect(() => getModelPricing('openai/gpt-5.5')).not.toThrow();
|
||||
expect(getModelPricing('openai/gpt-5.5').inputPerMillion).toBe(5);
|
||||
|
||||
@@ -630,10 +630,22 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
displayName: 'Claude (Anthropic)',
|
||||
defaultModel: 'claude-sonnet-4-6',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-8',
|
||||
name: 'Claude Opus 4.8',
|
||||
description: 'Latest flagship model',
|
||||
extendedContext: true,
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-8',
|
||||
opus: 'claude-opus-4-8',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-haiku-4-5-20251001',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-7',
|
||||
name: 'Claude Opus 4.7',
|
||||
description: 'Latest flagship model',
|
||||
description: 'Previous flagship model',
|
||||
extendedContext: true,
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-7',
|
||||
@@ -645,7 +657,7 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
name: 'Claude Opus 4.6',
|
||||
description: 'Previous flagship model',
|
||||
description: 'Older flagship model',
|
||||
extendedContext: true,
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-6',
|
||||
|
||||
@@ -16,12 +16,14 @@ describe('claude preset utils', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('keeps the claude catalog default on Sonnet 4.6 while exposing Opus 4.7', () => {
|
||||
it('keeps the claude catalog default on Sonnet 4.6 while exposing Opus 4.7 and 4.8', () => {
|
||||
const claudeCatalog = MODEL_CATALOGS.claude;
|
||||
const ids = claudeCatalog.models.map((model) => model.id);
|
||||
|
||||
expect(claudeCatalog.defaultModel).toBe('claude-sonnet-4-6');
|
||||
expect(claudeCatalog.models.map((model) => model.id)).toContain('claude-opus-4-7');
|
||||
expect(claudeCatalog.models.map((model) => model.id)).toContain('claude-sonnet-4-6');
|
||||
expect(ids).toContain('claude-opus-4-8');
|
||||
expect(ids).toContain('claude-opus-4-7');
|
||||
expect(ids).toContain('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('applies the default claude preset from the catalog default model mapping', async () => {
|
||||
|
||||
Reference in New Issue
Block a user