Add Claude Fable 5.1 (#3711)

Register claude-fable-5-1 in the Claude catalog and gate it in the
provider the same way as Fable 5: no sampling params, effort via
output_config, adaptive thinking with summarized display. The bare
claude-fable / claude-fable-latest aliases move to 5.1, matching how
the Opus aliases moved when Opus 5 landed.

Fable 5.1 keeps Fable 5's $10/$50 per MTok, 1M context and 128K output,
but bills cache reads at 0.025x input instead of the 0.1x every other
Claude model uses, so the catalog row carries its own rate and a test
pins it.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
404oops
2026-09-01 15:34:49 -04:00
committed by GitHub
co-authored by Claude Fable 5.1
parent fdd86ceb03
commit 6478a8f47d
3 changed files with 134 additions and 11 deletions
@@ -617,6 +617,45 @@ describe('ClaudeProvider.complete request shape', () => {
});
});
it('omits temperature for fable 5.1 (rejects non-default sampling)', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
await withTestActor(() =>
provider.complete({
model: 'claude-fable-5-1',
messages: [{ role: 'user', content: 'hi' }],
temperature: 0.5,
}),
);
const [args] = messagesCreateMock.mock.calls[0]!;
expect('temperature' in args).toBe(false);
});
it('forwards reasoning_effort as adaptive thinking + output_config effort on fable 5.1', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
await withTestActor(() =>
provider.complete({
model: 'claude-fable-5-1',
messages: [{ role: 'user', content: 'hi' }],
reasoning_effort: 'high',
} as never),
);
const [args] = messagesCreateMock.mock.calls[0]!;
// Fable 5.1 rejects `budget_tokens` and thinking is always on, so the
// only accepted config is adaptive; effort rides in output_config.
expect(args.thinking).toEqual({
type: 'adaptive',
display: 'summarized',
});
expect(args.output_config).toEqual({ effort: 'high' });
expect('temperature' in args).toBe(false);
});
it('builds an enabled thinking budget from reasoning_effort on older Sonnet models', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
@@ -670,6 +709,22 @@ describe('ClaudeProvider model resolution', () => {
);
});
it('routes the bare claude-fable alias to fable 5.1 rather than fable 5', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
await withTestActor(() =>
provider.complete({
model: 'claude-fable',
messages: [{ role: 'user', content: 'hi' }],
}),
);
expect(messagesCreateMock.mock.calls[0]![0].model).toBe(
'claude-fable-5-1',
);
});
it('falls back to the default model when given an unknown id', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
@@ -736,6 +791,46 @@ describe('ClaudeProvider.complete non-stream output', () => {
);
});
it('meters fable 5.1 cache reads at its reduced rate, not the 0.1x used elsewhere', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce({
content: [{ type: 'text', text: 'ok' }],
usage: {
input_tokens: 100,
output_tokens: 50,
cache_read_input_tokens: 1000,
},
});
await withTestActor(() =>
provider.complete({
model: 'claude-fable-5-1',
messages: [{ role: 'user', content: 'hi' }],
}),
);
const fable51 = CLAUDE_MODELS.find((m) => m.id === 'claude-fable-5-1')!;
const fable5 = CLAUDE_MODELS.find((m) => m.id === 'claude-fable-5')!;
// Same per-token price as Fable 5 except cache reads at a quarter of the rate.
expect(fable51.costs.input_tokens).toBe(fable5.costs.input_tokens);
expect(fable51.costs.output_tokens).toBe(fable5.costs.output_tokens);
expect(Number(fable51.costs.cache_read_input_tokens)).toBeCloseTo(
Number(fable5.costs.cache_read_input_tokens) / 4,
);
const [, , prefix, overrides] = recordSpy.mock.calls[0]!;
expect(prefix).toBe('claude:claude-fable-5-1');
expect(overrides.input_tokens).toBe(
100 * Number(fable51.costs.input_tokens),
);
expect(overrides.output_tokens).toBe(
50 * Number(fable51.costs.output_tokens),
);
expect(overrides.cache_read_input_tokens).toBeCloseTo(
1000 * Number(fable51.costs.cache_read_input_tokens),
);
});
it('bills the compaction pass by summing usage.iterations', async () => {
const { provider } = makeProvider();
// Per Anthropic: top-level input/output reflect only the message pass;
@@ -298,9 +298,10 @@ export class ClaudeProvider implements IChatProvider {
reasoningEffort: requestedReasoningEffort,
maxTokens: max_tokens,
});
// Fable 5 and Opus 4.7/4.8 error on non-default sampling params; omit temperature entirely.
// Fable 5/5.1, Sonnet 5, and Opus 4.7+ reject non-default sampling; omit temperature entirely.
// Other models require temperature=1 when thinking is enabled.
const omitsTemperature = [
'claude-fable-5-1',
'claude-fable-5',
'claude-sonnet-5',
'claude-opus-4-7',
@@ -313,6 +314,7 @@ export class ClaudeProvider implements IChatProvider {
? 1
: (temperature ?? 0);
const supportsEffort = [
'claude-fable-5-1',
'claude-fable-5',
'claude-sonnet-5',
'claude-opus-5',
@@ -717,12 +719,13 @@ export class ClaudeProvider implements IChatProvider {
}) {
if (!reasoningEffort) return undefined;
// Fable 5, Opus 4.7/4.8, 4.6, and Sonnet 4.6 use adaptive thinking
// Fable 5/5.1, Opus 4.7+, 4.6, and Sonnet 4.6 use adaptive thinking
// (`budget_tokens` is deprecated on 4.6/Sonnet 4.6, removed on
// Fable 5 and 4.7+). Fable 5 and Opus 4.7/4.8 omit thinking content
// by default; `display: 'summarized'` restores visible reasoning in
// the stream.
// Fable 5+ and Opus 4.7+). Fable 5/5.1 and Opus 4.7+ omit thinking
// content by default; `display: 'summarized'` restores visible
// reasoning in the stream.
if (
modelId === 'claude-fable-5-1' ||
modelId === 'claude-fable-5' ||
modelId === 'claude-opus-5' ||
modelId === 'claude-opus-4-8' ||
@@ -21,6 +21,36 @@ import type { IChatModel } from '../../types.js';
// Hardcoded from https://models.dev/api.json
export const CLAUDE_MODELS: IChatModel[] = [
{
puterId: 'anthropic:anthropic/claude-fable-5-1',
id: 'claude-fable-5-1',
modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
open_weights: false,
tool_call: true,
release_date: '2026-09-01',
aliases: [
'claude-fable',
'claude-fable-latest',
'claude-fable-5-1-latest',
'claude-fable-5.1',
'anthropic/claude-fable-5-1',
],
name: 'Claude Fable 5.1',
costs_currency: 'usd-cents',
input_cost_key: 'input_tokens',
output_cost_key: 'output_tokens',
costs: {
tokens: 1_000_000,
input_tokens: 1000,
ephemeral_5m_input_tokens: 1000 * 1.25,
ephemeral_1h_input_tokens: 1000 * 2,
// Fable 5.1 bills cache reads at 0.025x input; every other Claude model is 0.1x.
cache_read_input_tokens: 1000 * 0.025,
output_tokens: 5000,
},
context: 1000000,
max_tokens: 128000,
},
{
puterId: 'anthropic:anthropic/claude-fable-5',
id: 'claude-fable-5',
@@ -28,12 +58,7 @@ export const CLAUDE_MODELS: IChatModel[] = [
open_weights: false,
tool_call: true,
release_date: '2026-06-09',
aliases: [
'claude-fable',
'claude-fable-latest',
'claude-fable-5-latest',
'anthropic/claude-fable-5',
],
aliases: ['claude-fable-5-latest', 'anthropic/claude-fable-5'],
name: 'Claude Fable 5',
costs_currency: 'usd-cents',
input_cost_key: 'input_tokens',