Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
63 changes: 59 additions & 4 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,20 @@ const CLAUDE_PRESENTATION = {
displayName: "Claude",
showInteractionModeToggle: true,
} as const;
const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";

const CLAUDE_EFFORT_OPTIONS = {
fable48: [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be fable5 (or maybe fable50)?

{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
{ value: "max", label: "Max" },
{ value: "ultracode", label: "Ultracode" },
{ value: "ultrathink", label: "Ultrathink" },
],
opus48: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
Expand Down Expand Up @@ -92,6 +102,29 @@ const CLAUDE_EFFORT_OPTIONS = {
} as const;

const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: "claude-fable-5",
name: "Claude Fable 5",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: "effort",
label: "Reasoning",
options: CLAUDE_EFFORT_OPTIONS.fable48,
promptInjectedValues: ["ultrathink"],
Comment thread
cursor[bot] marked this conversation as resolved.
}),
buildSelectOptionDescriptor({
id: "contextWindow",
label: "Context Window",
options: [
{ value: "200k", label: "200k", isDefault: true },
{ value: "1m", label: "1M" },
],
}),
],
}),
Comment thread
cursor[bot] marked this conversation as resolved.
},
{
slug: "claude-opus-4-8",
name: "Claude Opus 4.8",
Expand All @@ -104,6 +137,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
options: CLAUDE_EFFORT_OPTIONS.opus48,
promptInjectedValues: ["ultrathink"],
}),
buildBooleanOptionDescriptor({
id: "fastMode",
label: "Fast Mode",
}),
buildSelectOptionDescriptor({
id: "contextWindow",
label: "Context Window",
Expand All @@ -127,6 +164,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
options: CLAUDE_EFFORT_OPTIONS.opus47,
promptInjectedValues: ["ultrathink"],
}),
buildBooleanOptionDescriptor({
id: "fastMode",
label: "Fast Mode",
}),
buildSelectOptionDescriptor({
id: "contextWindow",
label: "Context Window",
Expand Down Expand Up @@ -221,6 +262,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
},
];

function supportsClaudeFable5(version: string | null | undefined): boolean {
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
}

function supportsClaudeOpus48(version: string | null | undefined): boolean {
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false;
}
Expand All @@ -233,6 +278,9 @@ function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
return BUILT_IN_MODELS.filter((model) => {
if (model.slug === "claude-fable-5") {
return supportsClaudeFable5(version);
}
if (model.slug === "claude-opus-4-8") {
return supportsClaudeOpus48(version);
}
Expand All @@ -243,6 +291,11 @@ function getBuiltInClaudeModelsForVersion(
});
}

function formatClaudeFable5UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`;
}

function formatClaudeOpus48UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Opus 4.8. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_8_VERSION} or newer to access it.`;
Expand Down Expand Up @@ -688,11 +741,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
const versionUpgradeMessage = supportsClaudeOpus48(parsedVersion)
const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)
? undefined
: supportsClaudeOpus47(parsedVersion)
? formatClaudeOpus48UpgradeMessage(parsedVersion)
: formatClaudeOpus47UpgradeMessage(parsedVersion);
: supportsClaudeOpus48(parsedVersion)
? formatClaudeFable5UpgradeMessage(parsedVersion)
: supportsClaudeOpus47(parsedVersion)
? formatClaudeOpus48UpgradeMessage(parsedVersion)
: formatClaudeOpus47UpgradeMessage(parsedVersion);

const capabilities = resolveCapabilities
? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
Expand Down
56 changes: 56 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
),
);

it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities(),
);
const fable5 = status.models.find((model) => model.slug === "claude-fable-5");
assert.strictEqual(fable5?.name, "Claude Fable 5");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.169\n", stderr: "", code: 0 };
if (joined === "auth status")
return {
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stderr: "",
code: 0,
};
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("hides Claude Fable 5 on older Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities(),
);
assert.strictEqual(
status.models.some((model) => model.slug === "claude-fable-5"),
false,
);
assert.strictEqual(
status.message,
"Claude Code v2.1.168 is too old for Claude Fable 5. Upgrade to v2.1.169 or newer to access it.",
);
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.168\n", stderr: "", code: 0 };
if (joined === "auth status")
return {
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stderr: "",
code: 0,
};
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect(
"includes Claude Opus 4.7 with xhigh as the default effort on supported versions",
() =>
Expand Down
Loading