diff --git a/src/backends/claude-code/models.ts b/src/backends/claude-code/models.ts index bab70d9f..0a2379a3 100644 --- a/src/backends/claude-code/models.ts +++ b/src/backends/claude-code/models.ts @@ -1,4 +1,5 @@ export const CLAUDE_CODE_MODELS = [ + { value: 'claude-fable-5', label: 'Claude Fable 5' }, { value: 'claude-opus-4-8', label: 'Claude Opus 4.8' }, { value: 'claude-opus-4-8[1m]', label: 'Claude Opus 4.8 (1M context)' }, { value: 'claude-opus-4-7', label: 'Claude Opus 4.7' }, diff --git a/src/backends/codex/models.ts b/src/backends/codex/models.ts index 7d3d9612..beab7fce 100644 --- a/src/backends/codex/models.ts +++ b/src/backends/codex/models.ts @@ -1,4 +1,7 @@ export const CODEX_MODELS = [ + { value: 'gpt-5.6-sol', label: 'GPT-5.6 Sol' }, + { value: 'gpt-5.6-terra', label: 'GPT-5.6 Terra' }, + { value: 'gpt-5.6-luna', label: 'GPT-5.6 Luna' }, { value: 'gpt-5.5', label: 'GPT-5.5' }, { value: 'gpt-5.4', label: 'GPT-5.4' }, { value: 'gpt-5.4-mini', label: 'GPT-5.4 mini' }, diff --git a/src/config/rateLimits.ts b/src/config/rateLimits.ts index 91988d99..93b79019 100644 --- a/src/config/rateLimits.ts +++ b/src/config/rateLimits.ts @@ -19,6 +19,13 @@ export const MODEL_RATE_LIMITS: ModelRateLimits = { safetyMargin: 0.8, // Conservative - start throttling at 80% }, + // Claude Fable 5 (Tier 1: 50 RPM, 10K TPM — priciest Anthropic model, throttle-sensitive) + 'anthropic:claude-fable-5': { + requestsPerMinute: 50, + tokensPerMinute: 10_000, + safetyMargin: 0.85, + }, + // Claude Opus 4.8 (Tier 1: 50 RPM, 10K TPM — Opus is throttle-sensitive) 'anthropic:claude-opus-4-8': { requestsPerMinute: 50, diff --git a/src/utils/llmMetrics.ts b/src/utils/llmMetrics.ts index c55c9980..0e70a4cf 100644 --- a/src/utils/llmMetrics.ts +++ b/src/utils/llmMetrics.ts @@ -9,6 +9,11 @@ import type { TokenUsage } from 'llmist'; * Prices as of January 2026. */ const MODEL_PRICING: Record = { + // Anthropic Claude Fable 5 — 1M context by default (max = default), priced at 2× Opus. + // Key matches toPricingKey('claude-fable-5') = 'anthropic:claude-fable-5' (no trailing + // date to strip). cachedInput follows the 0.1× convention used by every Anthropic row. + 'anthropic:claude-fable-5': { input: 10.0, output: 50.0, cachedInput: 1.0 }, + // Anthropic Claude 4 family 'anthropic:claude-opus-4-8': { input: 5.0, output: 25.0, cachedInput: 0.5 }, 'anthropic:claude-opus-4-8[1m]': { input: 5.0, output: 25.0, cachedInput: 0.5 }, @@ -28,6 +33,12 @@ const MODEL_PRICING: Record { describe('CLAUDE_CODE_MODELS constants', () => { it('contains the expected models', () => { - expect(CLAUDE_CODE_MODELS).toHaveLength(10); + expect(CLAUDE_CODE_MODELS).toHaveLength(11); }); it('includes Opus 4.8, Opus 4.7, and the 1M context variants', () => { @@ -307,6 +307,12 @@ describe('CLAUDE_CODE_MODELS constants', () => { expect(CLAUDE_CODE_MODEL_IDS).toContain('claude-opus-4-6[1m]'); }); + it('includes Claude Fable 5 (1M context by default — no [1m] variant)', () => { + expect(CLAUDE_CODE_MODEL_IDS).toContain('claude-fable-5'); + // Fable 5 is 1M-context by default, so a [1m] suffix would be redundant. + expect(CLAUDE_CODE_MODEL_IDS).not.toContain('claude-fable-5[1m]'); + }); + it('has value/label pairs', () => { for (const m of CLAUDE_CODE_MODELS) { expect(m.value).toBeTruthy(); @@ -325,6 +331,7 @@ describe('CLAUDE_CODE_MODELS constants', () => { describe('resolveClaudeModel', () => { it('passes through known Claude Code model IDs', () => { + expect(resolveClaudeModel('claude-fable-5')).toBe('claude-fable-5'); expect(resolveClaudeModel('claude-opus-4-8')).toBe('claude-opus-4-8'); expect(resolveClaudeModel('claude-opus-4-8[1m]')).toBe('claude-opus-4-8[1m]'); expect(resolveClaudeModel('claude-opus-4-7')).toBe('claude-opus-4-7'); diff --git a/tests/unit/backends/codex.test.ts b/tests/unit/backends/codex.test.ts index 3080bbee..547092f7 100644 --- a/tests/unit/backends/codex.test.ts +++ b/tests/unit/backends/codex.test.ts @@ -146,6 +146,13 @@ describe('resolveCodexModel', () => { expect(resolveCodexModel(`openai:${DEFAULT_CODEX_MODEL}`)).toBe(DEFAULT_CODEX_MODEL); }); + it('passes through the GPT-5.6 Sol/Terra/Luna tiers (bare and openai:-prefixed)', () => { + for (const id of ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) { + expect(resolveCodexModel(id)).toBe(id); + expect(resolveCodexModel(`openai:${id}`)).toBe(id); + } + }); + it('throws for incompatible models', () => { expect(() => resolveCodexModel('openrouter:google/gemini-3-flash-preview')).toThrow( 'not compatible with the Codex engine',