Skip to content
Closed
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
29 changes: 26 additions & 3 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,29 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("preserves xhigh effort for Claude Opus 5", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"claude-opus-5",
[{ id: "effort", value: "xhigh" }],
),
runtimeMode: "full-access",
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, "xhigh");
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down Expand Up @@ -1435,7 +1458,7 @@ describe("ClaudeAdapterLive", () => {
const adapter = yield* ClaudeAdapter;
const requestedModel = createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"claude-fable-5",
"claude-opus-5",
);
const runtimeEventsFiber = yield* Stream.takeUntil(
adapter.streamEvents,
Expand Down Expand Up @@ -1615,7 +1638,7 @@ describe("ClaudeAdapterLive", () => {
parent_tool_use_id: null,
message: {
id: "assistant-message-effective-model-alias",
model: "claude-opus-4-8-20260718",
model: "claude-opus-5-20260718",
content: [{ type: "text", text: "Alias answer" }],
},
} as unknown as SDKMessage);
Expand All @@ -1628,7 +1651,7 @@ describe("ClaudeAdapterLive", () => {
session_id: "sdk-session-effective-model-alias",
uuid: "result-effective-model-alias",
modelUsage: {
"claude-opus-4-8-20260718": {
"claude-opus-5-20260718": {
contextWindow: 1_000_000,
maxOutputTokens: 128_000,
},
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ function selectedClaudeContextWindow(
modelSelection: ModelSelection | undefined,
): number | undefined {
switch (modelSelection?.model) {
case "claude-opus-5":
case "claude-opus-4-8":
case "claude-opus-4-7":
// Always 1M at the API; these models expose no contextWindow option.
Expand Down
105 changes: 57 additions & 48 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,33 @@ const CLAUDE_PRESENTATION = {
displayName: "Claude",
showInteractionModeToggle: true,
} as const;
const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
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_OPUS_CAPABILITIES = createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: "effort",
label: "Reasoning",
options: [
{ 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" },
],
promptInjectedValues: ["ultrathink"],
}),
buildBooleanOptionDescriptor({
id: "fastMode",
label: "Fast Mode",
}),
],
});

const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: "claude-fable-5",
Expand Down Expand Up @@ -84,31 +107,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
}),
},
{
slug: "claude-opus-4-8",
name: "Claude Opus 4.8",
slug: "claude-opus-5",
Comment thread
wizzoapp[bot] marked this conversation as resolved.
Comment thread
wizzoapp[bot] marked this conversation as resolved.
name: "Claude Opus 5",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: "effort",
label: "Reasoning",
options: [
{ 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" },
],
promptInjectedValues: ["ultrathink"],
}),
buildBooleanOptionDescriptor({
id: "fastMode",
label: "Fast Mode",
}),
],
}),
capabilities: CLAUDE_OPUS_CAPABILITIES,
Comment thread
wizzoapp[bot] marked this conversation as resolved.
},
{
slug: "claude-opus-4-7",
Expand Down Expand Up @@ -268,12 +270,12 @@ 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 supportsClaudeOpus5(version: string | null | undefined): boolean {
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
}

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

function supportsClaudeOpus47(version: string | null | undefined): boolean {
Expand All @@ -284,27 +286,27 @@ function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
return BUILT_IN_MODELS.filter((model) => {
if (model.slug === "claude-opus-5") {
return supportsClaudeOpus5(version);
}
if (model.slug === "claude-fable-5") {
return supportsClaudeFable5(version);
}
if (model.slug === "claude-opus-4-8") {
return supportsClaudeOpus48(version);
}
if (model.slug === "claude-opus-4-7") {
return supportsClaudeOpus47(version);
}
return true;
});
}

function formatClaudeFable5UpgradeMessage(version: string | null): string {
function formatClaudeOpus5UpgradeMessage(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.`;
return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`;
}

function formatClaudeOpus48UpgradeMessage(version: string | null): string {
function formatClaudeFable5UpgradeMessage(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.`;
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 formatClaudeOpus47UpgradeMessage(version: string | null): string {
Expand All @@ -314,6 +316,11 @@ function formatClaudeOpus47UpgradeMessage(version: string | null): string {

export function getClaudeModelCapabilities(model: string | null | undefined): ModelCapabilities {
const slug = model?.trim();
// Persisted 4.8 selections remain runnable, but 4.8 is intentionally absent
// from the advertised model catalog now that Opus 5 replaces it.
if (slug === "claude-opus-4-8") {
return CLAUDE_OPUS_CAPABILITIES;
}
return (
BUILT_IN_MODELS.find((candidate) => candidate.slug === slug)?.capabilities ??
DEFAULT_CLAUDE_MODEL_CAPABILITIES
Expand Down Expand Up @@ -356,6 +363,7 @@ export function normalizeClaudeCliEffort(
if (
effort === "xhigh" &&
model !== "claude-fable-5" &&
model !== "claude-opus-5" &&
model !== "claude-opus-4-8" &&
model !== "claude-sonnet-5"
) {
Expand Down Expand Up @@ -704,7 +712,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
const resolvedEnvironment = environment ?? process.env;
const checkedAt = DateTime.formatIso(yield* DateTime.now);
const allModels = providerModelsFromSettings(
BUILT_IN_MODELS,
getBuiltInClaudeModelsForVersion(null),
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down Expand Up @@ -772,6 +780,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(

const version = versionProbe.success.value;
const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`);
const versionedModels = providerModelsFromSettings(
getBuiltInClaudeModelsForVersion(parsedVersion),
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
if (version.code !== 0) {
yield* Effect.logWarning("Claude Agent CLI version probe exited with a non-zero status.", {
exitCode: version.code,
Expand All @@ -782,7 +795,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
presentation: CLAUDE_PRESENTATION,
enabled: claudeSettings.enabled,
checkedAt,
models: allModels,
models: versionedModels,
probe: {
installed: true,
version: parsedVersion,
Expand All @@ -793,17 +806,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
});
}

const models = providerModelsFromSettings(
getBuiltInClaudeModelsForVersion(parsedVersion),
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)
const models = versionedModels;
const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
? undefined
: supportsClaudeOpus48(parsedVersion)
? formatClaudeFable5UpgradeMessage(parsedVersion)
: supportsClaudeFable5(parsedVersion)
? formatClaudeOpus5UpgradeMessage(parsedVersion)
: supportsClaudeOpus47(parsedVersion)
? formatClaudeOpus48UpgradeMessage(parsedVersion)
? formatClaudeFable5UpgradeMessage(parsedVersion)
: formatClaudeOpus47UpgradeMessage(parsedVersion);

const capabilities = resolveCapabilities
Expand Down Expand Up @@ -862,7 +871,7 @@ export const makePendingClaudeProvider = (
Effect.gen(function* () {
const checkedAt = yield* nowIso;
const models = providerModelsFromSettings(
BUILT_IN_MODELS,
getBuiltInClaudeModelsForVersion(null),
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down
Loading
Loading