Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### ✨ New Features

- **feat(providers):** add Claude Fable 5 (`claude-fable-5`) — wires the new flagship model across the full pipeline: `cc` and `kiro` provider registries (1M context, 128k output), pricing constants, model spec (adaptive thinking, vision, tool use), fast mode, 1M-context beta header, fallback chain (`claude-fable-5 → claude-opus-4-8 → claude-opus-4-7 → claude-sonnet-4-6`), and cost data.

---

## [3.8.20] — Unreleased
Expand Down
12 changes: 12 additions & 0 deletions open-sse/config/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,12 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
tokenUrl: "https://api.anthropic.com/v1/oauth/token",
},
models: [
{
id: "claude-fable-5",
name: "Claude Fable 5",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-opus-4-8",
name: "Claude Opus 4.8",
Expand Down Expand Up @@ -1142,6 +1148,12 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
},
models: [
{ id: "auto-kiro", name: "Auto (Kiro picks best model)" },
{
id: "claude-fable-5",
name: "Claude Fable 5",
contextLength: 1000000,
maxOutputTokens: 128000,
},
{
id: "claude-opus-4.8",
name: "Claude Opus 4.8",
Expand Down
2 changes: 1 addition & 1 deletion open-sse/services/claudeCodeCompatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [
text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
},
];
const CONTEXT_1M_SUPPORTED_MODELS = ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"];
const CONTEXT_1M_SUPPORTED_MODELS = ["claude-fable-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"];
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS = getStainlessTimeoutSeconds(
process.env
);
Expand Down
27 changes: 23 additions & 4 deletions open-sse/services/modelFamilyFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { getModelContextLimit } from "../../src/lib/modelCapabilities";
import { parseModel } from "./model.ts";
import { CONTEXT_OVERFLOW_REGEX } from "./errorClassifier.ts";
import { getRegistryEntry } from "../config/providerRegistry.ts";

// ── Model Family Definitions ─────────────────────────────────────────────────

Expand Down Expand Up @@ -72,6 +73,10 @@ const MODEL_FAMILIES: Record<string, string[]> = {
"gemini-2.5-pro": ["gemini-2.5-pro-preview-06-05", "gemini-2.5-pro-exp-03-25"],
"gemini-2.5-pro-preview-06-05": ["gemini-2.5-pro", "gemini-2.5-pro-exp-03-25"],

// Claude Mythos family (Fable 5) — flagship falls to the next-best Opus
// tiers before the cheaper Sonnet, matching the Opus family ordering.
"claude-fable-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-4-6"],

// Claude Opus family
"claude-opus-4-8": ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"],
"claude-opus-4-7": ["claude-opus-4-6", "claude-opus-4-5-20251101", "claude-sonnet-4-6"],
Expand Down Expand Up @@ -142,14 +147,28 @@ export function getNextFamilyFallback(
): string | null {
const parsed = parseModel(currentModel);
const bareModel = parsed.model || currentModel;
const prefix =
parsed.provider || parsed.providerAlias ? `${parsed.provider || parsed.providerAlias}/` : "";
const provider = parsed.provider || parsed.providerAlias || "";
const prefix = provider ? `${provider}/` : "";

const family = MODEL_FAMILIES[bareModel];
// Normalize dots to hyphens for the lookup so kiro/claude-opus-4.8 finds the right entry
const lookupKey = bareModel.replace(/\./g, "-");
const family = MODEL_FAMILIES[lookupKey];
if (!family) return null;

// Resolve the provider's supported model IDs so we can match notation (dot vs hyphen)
const registryEntry = provider ? getRegistryEntry(provider) : null;
const supportedIds = registryEntry ? new Set(registryEntry.models.map((m) => m.id)) : null;

for (const candidate of family) {
const fullCandidate = `${prefix}${candidate}`;
let resolvedCandidate = candidate;
if (supportedIds && !supportedIds.has(candidate)) {
// Try dot-notation variants: claude-opus-4-8 → claude-opus-4.8
const dotVariant = candidate.replace(/-(\d+)-(\d+)$/, "-$1.$2");
const dotVariant2 = candidate.replace(/-(\d+)-(\d+)-/, "-$1.$2-");
if (supportedIds.has(dotVariant)) resolvedCandidate = dotVariant;
else if (supportedIds.has(dotVariant2)) resolvedCandidate = dotVariant2;
}
const fullCandidate = `${prefix}${resolvedCandidate}`;
if (!triedModels.has(fullCandidate)) {
return fullCandidate;
}
Expand Down
1 change: 1 addition & 0 deletions open-sse/services/providerCostData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ModelPricing {
export const KNOWN_MODEL_PRICING: Record<string, ModelPricing> = {
"gpt-4o": { inputCostPer1M: 2.5, outputCostPer1M: 10.0, isFree: false },
"gpt-4o-mini": { inputCostPer1M: 0.15, outputCostPer1M: 0.6, isFree: false },
"claude-fable-5": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false },
"claude-opus-4-8": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false },
"claude-opus-4-7": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false },
"claude-sonnet-4-6": { inputCostPer1M: 3.0, outputCostPer1M: 15.0, isFree: false },
Expand Down
2 changes: 1 addition & 1 deletion src/lib/db/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function getSettings() {
codexServiceTier: { enabled: false },
claudeFastMode: {
enabled: false,
supportedModels: ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
supportedModels: ["claude-fable-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
},
codexSessionAffinityTtlMs: 0,
responsesPreviousResponseIdMode: DEFAULT_RESPONSES_PREVIOUS_RESPONSE_ID_MODE,
Expand Down
1 change: 1 addition & 0 deletions src/lib/providers/claudeFastMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type JsonRecord = Record<string, unknown>;
* only the latest Opus tiers can request the priority service path.
*/
export const CLAUDE_FAST_MODE_DEFAULT_MODELS = [
"claude-fable-5",
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
Expand Down
1 change: 1 addition & 0 deletions src/lib/providers/staticModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
],
antigravity: () => ANTIGRAVITY_PUBLIC_MODELS.map((model) => ({ ...model })),
claude: () => [
{ id: "claude-fable-5", name: "Claude Fable 5" },
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
Expand Down
12 changes: 12 additions & 0 deletions src/shared/constants/modelSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-7", "claude-opus-4.7"),
},

// ── Claude Fable 5 ──────────────────────────────────────────────
"claude-fable-5": {
maxOutputTokens: 128000,
contextWindow: 1000000,
defaultThinkingBudget: 32000,
thinkingBudgetCap: 120000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
aliases: BEDROCK_CLAUDE_ALIASES("claude-fable-5"),
},

// ── Claude Opus 4.8 ─────────────────────────────────────────────
"claude-opus-4-8": {
maxOutputTokens: 128000,
Expand Down
23 changes: 23 additions & 0 deletions src/shared/constants/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const GPT_5_5_PRICING = {
cache_creation: 5.0,
};

const CLAUDE_FABLE_5_PRICING = {
input: 15.0,
output: 75.0,
cached: 7.5,
reasoning: 112.5,
cache_creation: 15.0,
};

const CLAUDE_OPUS_4_PRICING = {
input: 15.0,
output: 75.0,
Expand Down Expand Up @@ -129,6 +137,13 @@ export const DEFAULT_PRICING = {

// Claude Code (cc)
cc: {
"claude-fable-5": {
input: 5.0,
output: 25.0,
cached: 2.5,
reasoning: 37.5,
cache_creation: 5.0,
},
"claude-opus-4-8": {
input: 5.0,
output: 25.0,
Expand Down Expand Up @@ -717,6 +732,7 @@ export const DEFAULT_PRICING = {
// Common model IDs (without dates) used across providers
// Intentional duplicates of dot-notation variants (e.g. claude-opus-4.6)
// to cover hyphen-notation IDs (claude-opus-4-6) used by some clients
"claude-fable-5": CLAUDE_FABLE_5_PRICING,
"claude-opus-4.8": CLAUDE_OPUS_4_PRICING,
"claude-opus-4-8": CLAUDE_OPUS_4_PRICING,
"claude-opus-4-7": CLAUDE_OPUS_4_PRICING,
Expand Down Expand Up @@ -1282,6 +1298,13 @@ export const DEFAULT_PRICING = {
},

kiro: {
"claude-fable-5": {
input: 15.0,
output: 75.0,
cached: 7.5,
reasoning: 112.5,
cache_creation: 15.0,
},
"claude-sonnet-4.5": {
input: 3.0,
output: 15.0,
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/catalog-updates-v3x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ test("NVIDIA catalog includes the verified 2026 additions and GPT OSS 20B alias
});
});

test("Fable 5 catalog exposes claude-fable-5 in cc and kiro providers with matching pricing", () => {
const ccIds = new Set(getModelsByProviderId("cc").map((m) => m.id));
assert.ok(ccIds.has("claude-fable-5"), "cc must expose claude-fable-5");

const kiroModels = getModelsByProviderId("kiro");
const kiroIds = new Set(kiroModels.map((m) => m.id));
assert.ok(kiroIds.has("claude-fable-5"), "kiro must expose claude-fable-5");

const fable = kiroModels.find((m) => m.id === "claude-fable-5");
assert.equal(fable?.contextLength, 1000000);
assert.equal(fable?.maxOutputTokens, 128000);

const ccPricing = (DEFAULT_PRICING as Record<string, Record<string, unknown>>).cc;
assert.ok(ccPricing["claude-fable-5"], "cc pricing must include claude-fable-5");

const kiroPricing = (DEFAULT_PRICING as Record<string, Record<string, unknown>>).kiro;
assert.ok(kiroPricing["claude-fable-5"], "kiro pricing must include claude-fable-5");
});

test("Kiro catalog exposes Claude Opus 4.8 alongside 4.7 with matching pricing", () => {
const models = getModelsByProviderId("kiro");
const ids = new Set(models.map((model) => model.id));
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/model-family-fallback-notation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert/strict";

// Covers getNextFamilyFallback's dot-vs-hyphen notation resolution (the hunk
// added alongside Claude Fable 5 in #3524 that affects ALL families): the
// lookup normalizes dots→hyphens, and each candidate is resolved to the
// notation the provider's registry actually exposes (kiro uses dot notation
// `claude-opus-4.8`, cc uses hyphen `claude-opus-4-8`).
const { getNextFamilyFallback } = await import("../../open-sse/services/modelFamilyFallback.ts");

test("Fable 5 falls back to the next-best Opus tier first (not Sonnet) — cc→claude", () => {
// `cc` is an alias parseModel normalizes to the `claude` provider.
const next = getNextFamilyFallback("cc/claude-fable-5", new Set(["cc/claude-fable-5"]));
assert.equal(next, "claude/claude-opus-4-8");
});

test("Fable 5 fallback resolves to kiro's dot-notation model id", () => {
// kiro registry exposes `claude-opus-4.8` (dot), not `claude-opus-4-8`.
const next = getNextFamilyFallback("kiro/claude-fable-5", new Set(["kiro/claude-fable-5"]));
assert.equal(next, "kiro/claude-opus-4.8");
});

test("dot-notation current model is normalized for the family lookup", () => {
// kiro/claude-opus-4.8 must find the claude-opus-4-8 family entry.
const next = getNextFamilyFallback("kiro/claude-opus-4.8", new Set(["kiro/claude-opus-4.8"]));
assert.equal(next, "kiro/claude-opus-4.7");
});

test("skips already-tried candidates and advances down the Fable chain", () => {
const next = getNextFamilyFallback(
"cc/claude-fable-5",
new Set(["cc/claude-fable-5", "claude/claude-opus-4-8"])
);
assert.equal(next, "claude/claude-opus-4-7");
});

test("returns null for an unknown family", () => {
assert.equal(getNextFamilyFallback("cc/not-a-real-model", new Set()), null);
});