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:
Sergey Galuza
2026-05-30 17:16:27 +02:00
parent 5b8923c85e
commit 357ce8b07a
7 changed files with 92 additions and 17 deletions
@@ -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.
+17 -2
View File
@@ -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',
+7
View File
@@ -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
+27 -10
View File
@@ -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);
+14 -2
View File
@@ -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',
+5 -3
View File
@@ -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 () => {