mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 10:17:05 +00:00
feat(catalog): add Claude Fable 5 to Anthropic model registry
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user