feat(catalog): add Claude Fable 5 to Anthropic model registry

This commit is contained in:
Sergey
2026-06-10 13:03:38 -04:00
committed by GitHub
parent cf90f48240
commit 298c89f2c3
9 changed files with 85 additions and 1 deletions
@@ -168,6 +168,19 @@ describe('Model Catalog', () => {
assert.strictEqual(opus48.extendedContext, true);
});
it('includes Claude Fable 5 with adaptive levels and extended context', () => {
const { MODEL_CATALOG } = modelCatalog;
const fable5 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-fable-5');
assert(fable5, 'Should include Claude Fable 5');
assert.strictEqual(fable5.name, 'Claude Fable 5');
// New top tier above Opus; same adaptive thinking surface as Opus 4.8:
// Anthropic only accepts effort levels, budget_tokens is rejected with 400.
assert.strictEqual(fable5.thinking.type, 'levels');
assert.deepStrictEqual(fable5.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']);
assert.strictEqual(fable5.thinking.maxLevel, 'max');
assert.strictEqual(fable5.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);
@@ -60,6 +60,15 @@ describe('Thinking Validator', () => {
expect(result.warning).toBeUndefined();
});
it('should treat max as a distinct top tier on Claude Fable 5', () => {
// Fable 5 shares Opus 4.8's adaptive thinking surface; max must remain
// distinct from xhigh.
const result = validateThinking('claude', 'claude-fable-5', '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.
+1
View File
@@ -63,6 +63,7 @@ function shouldShowWarnings(thinkingConfig: ThinkingConfig): boolean {
*/
export function detectTierFromModel(modelName: string): ModelTier {
const lower = modelName.toLowerCase();
if (lower.includes('fable')) return 'opus'; // Fable is the top tier; thinking defaults match Opus
if (lower.includes('opus')) return 'opus';
if (lower.includes('haiku')) return 'haiku';
return 'sonnet'; // Default to sonnet (most common)
@@ -65,6 +65,13 @@ describe('detectTierFromModel', () => {
expect(result).toBe('sonnet');
});
it('should map Claude Fable 5 to the opus tier', () => {
// Fable is the top tier above Opus; auto-mode thinking defaults should
// match the opus tier (effort high), not the sonnet fallback.
const result = detectTierFromModel('claude-fable-5');
expect(result).toBe('opus');
});
it('should detect haiku from model name containing "haiku"', () => {
const result = detectTierFromModel('claude-haiku-4-5-20251001');
expect(result).toBe('haiku');
+15
View File
@@ -369,6 +369,21 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
displayName: 'Claude (Anthropic)',
defaultModel: 'claude-sonnet-4-6',
models: [
{
id: 'claude-fable-5',
name: 'Claude Fable 5',
description: 'Most powerful model',
nativeImageInput: true,
// New tier above Opus. Same adaptive-thinking surface as Opus 4.8:
// Anthropic accepts only effort levels; 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-8',
name: 'Claude Opus 4.8',
+7
View File
@@ -316,6 +316,13 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
fast: OPUS_48_FAST_RATES,
},
},
// Claude Fable 5 ($10/$50) — most powerful tier, above Opus
'claude-fable-5': {
inputPerMillion: 10.0,
outputPerMillion: 50.0,
cacheCreationPerMillion: 12.5,
cacheReadPerMillion: 1.0,
},
// ---------------------------------------------------------------------------
// OpenAI Models - Source: better-ccusage
+19
View File
@@ -245,6 +245,14 @@ describe('model-pricing', () => {
expect(opus48.outputPerMillion).toBe(25.0);
});
it('should return correct pricing for Claude Fable 5', () => {
const fable5 = getModelPricing('claude-fable-5');
expect(fable5.inputPerMillion).toBe(10.0);
expect(fable5.outputPerMillion).toBe(50.0);
expect(fable5.cacheCreationPerMillion).toBe(12.5);
expect(fable5.cacheReadPerMillion).toBe(1.0);
});
it('should not map unknown future model families onto known family pricing', () => {
const fallback = getModelPricing('unknown-model-xyz');
@@ -355,6 +363,17 @@ describe('model-pricing', () => {
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
});
it('should calculate Claude Fable 5 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-fable-5');
expect(cost).toBe(73.5); // 10 + 50 + 12.5 + 1.0
});
it('should calculate fast-tier Claude Opus 4.8 cost (2x premium)', () => {
const usage: TokenUsage = {
inputTokens: 1_000_000,
+12
View File
@@ -752,6 +752,18 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
displayName: 'Claude (Anthropic)',
defaultModel: 'claude-sonnet-4-6',
models: [
{
id: 'claude-fable-5',
name: 'Claude Fable 5',
description: 'Most powerful model',
extendedContext: true,
presetMapping: {
default: 'claude-fable-5',
opus: 'claude-fable-5',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
},
},
{
id: 'claude-opus-4-8',
name: 'Claude Opus 4.8',
+2 -1
View File
@@ -16,11 +16,12 @@ describe('claude preset utils', () => {
vi.restoreAllMocks();
});
it('keeps the claude catalog default on Sonnet 4.6 while exposing Opus 4.7 and 4.8', () => {
it('keeps the claude catalog default on Sonnet 4.6 while exposing Opus 4.7/4.8 and Fable 5', () => {
const claudeCatalog = MODEL_CATALOGS.claude;
const ids = claudeCatalog.models.map((model) => model.id);
expect(claudeCatalog.defaultModel).toBe('claude-sonnet-4-6');
expect(ids).toContain('claude-fable-5');
expect(ids).toContain('claude-opus-4-8');
expect(ids).toContain('claude-opus-4-7');
expect(ids).toContain('claude-sonnet-4-6');