mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
feat(catalog): add Claude Fable 5 to Anthropic model registry
This commit is contained in:
1 parent
cf90f48240
commit
298c89f2c3
9 files changed
+85
-1
No files matched your search
@@ -168,6 +168,19 @@ describe('Model Catalog', () => {
|
|||||||
assert.strictEqual(opus48.extendedContext, true);
|
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', () => {
|
it('retains previous 4.5 snapshot models for explicit selection', () => {
|
||||||
const { MODEL_CATALOG } = modelCatalog;
|
const { MODEL_CATALOG } = modelCatalog;
|
||||||
const ids = MODEL_CATALOG.claude.models.map((m) => m.id);
|
const ids = MODEL_CATALOG.claude.models.map((m) => m.id);
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ describe('Thinking Validator', () => {
|
|||||||
expect(result.warning).toBeUndefined();
|
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)', () => {
|
it('should still alias max -> xhigh for models without a max level (backcompat)', () => {
|
||||||
// Codex catalog uses ['low','medium','high','xhigh'] with maxLevel 'xhigh'.
|
// Codex catalog uses ['low','medium','high','xhigh'] with maxLevel 'xhigh'.
|
||||||
// User input "max" should map down to xhigh rather than be rejected.
|
// 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 {
|
export function detectTierFromModel(modelName: string): ModelTier {
|
||||||
const lower = modelName.toLowerCase();
|
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('opus')) return 'opus';
|
||||||
if (lower.includes('haiku')) return 'haiku';
|
if (lower.includes('haiku')) return 'haiku';
|
||||||
return 'sonnet'; // Default to sonnet (most common)
|
return 'sonnet'; // Default to sonnet (most common)
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ describe('detectTierFromModel', () => {
|
|||||||
expect(result).toBe('sonnet');
|
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"', () => {
|
it('should detect haiku from model name containing "haiku"', () => {
|
||||||
const result = detectTierFromModel('claude-haiku-4-5-20251001');
|
const result = detectTierFromModel('claude-haiku-4-5-20251001');
|
||||||
expect(result).toBe('haiku');
|
expect(result).toBe('haiku');
|
||||||
|
|||||||
@@ -369,6 +369,21 @@ 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-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',
|
id: 'claude-opus-4-8',
|
||||||
name: 'Claude Opus 4.8',
|
name: 'Claude Opus 4.8',
|
||||||
|
|||||||
@@ -316,6 +316,13 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
|||||||
fast: OPUS_48_FAST_RATES,
|
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
|
// OpenAI Models - Source: better-ccusage
|
||||||
|
|||||||
@@ -245,6 +245,14 @@ describe('model-pricing', () => {
|
|||||||
expect(opus48.outputPerMillion).toBe(25.0);
|
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', () => {
|
it('should not map unknown future model families onto known family pricing', () => {
|
||||||
const fallback = getModelPricing('unknown-model-xyz');
|
const fallback = getModelPricing('unknown-model-xyz');
|
||||||
|
|
||||||
@@ -355,6 +363,17 @@ describe('model-pricing', () => {
|
|||||||
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
|
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)', () => {
|
it('should calculate fast-tier Claude Opus 4.8 cost (2x premium)', () => {
|
||||||
const usage: TokenUsage = {
|
const usage: TokenUsage = {
|
||||||
inputTokens: 1_000_000,
|
inputTokens: 1_000_000,
|
||||||
|
|||||||
@@ -752,6 +752,18 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
displayName: 'Claude (Anthropic)',
|
displayName: 'Claude (Anthropic)',
|
||||||
defaultModel: 'claude-sonnet-4-6',
|
defaultModel: 'claude-sonnet-4-6',
|
||||||
models: [
|
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',
|
id: 'claude-opus-4-8',
|
||||||
name: 'Claude Opus 4.8',
|
name: 'Claude Opus 4.8',
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ describe('claude preset utils', () => {
|
|||||||
vi.restoreAllMocks();
|
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 claudeCatalog = MODEL_CATALOGS.claude;
|
||||||
const ids = claudeCatalog.models.map((model) => model.id);
|
const ids = claudeCatalog.models.map((model) => model.id);
|
||||||
|
|
||||||
expect(claudeCatalog.defaultModel).toBe('claude-sonnet-4-6');
|
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-8');
|
||||||
expect(ids).toContain('claude-opus-4-7');
|
expect(ids).toContain('claude-opus-4-7');
|
||||||
expect(ids).toContain('claude-sonnet-4-6');
|
expect(ids).toContain('claude-sonnet-4-6');
|
||||||
|
|||||||
Reference in new issue
Block a user