Skip to content
1 change: 1 addition & 0 deletions apps/server/src/mcp/toolkits/subagent/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ const buildModelSources = (runtime: SubagentRuntime) =>
driverKind: providerInstance.driverKind,
models: snapshot.models.map((providerModel) => ({
slug: providerModel.slug,
isCustom: providerModel.isCustom,
optionDescriptors: providerModel.capabilities?.optionDescriptors,
defaultOptions: buildProviderOptionSelectionsFromDescriptors(
providerModel.capabilities?.optionDescriptors,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ const makeActiveThreadStartRuntime = Effect.fn("ThreadToolkit.makeActiveRuntime"
driverKind: providerInstance.driverKind,
models: snapshot.models.map((providerModel) => ({
slug: providerModel.slug,
isCustom: providerModel.isCustom,
optionDescriptors: providerModel.capabilities?.optionDescriptors,
defaultOptions: buildProviderOptionSelectionsFromDescriptors(
providerModel.capabilities?.optionDescriptors,
Expand Down Expand Up @@ -728,6 +729,7 @@ const makeActiveThreadStartRuntime = Effect.fn("ThreadToolkit.makeActiveRuntime"
driverKind: providerInstance.driverKind,
models: snapshot.models.map((providerModel) => ({
slug: providerModel.slug,
isCustom: providerModel.isCustom,
optionDescriptors: providerModel.capabilities?.optionDescriptors,
defaultOptions: buildProviderOptionSelectionsFromDescriptors(
providerModel.capabilities?.optionDescriptors,
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";
Expand All @@ -32,6 +33,7 @@ import {
checkClaudeProviderStatus,
makePendingClaudeProvider,
probeClaudeCapabilities,
reconcileKnownClaudeModelsAfterVersionProbe,
} from "../Layers/ClaudeProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
Expand Down Expand Up @@ -160,13 +162,23 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
),
});
const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);
const knownVersionedModels = yield* Ref.make<ServerProvider["models"]>([]);

const checkProvider = checkClaudeProviderStatus(
effectiveConfig,
() => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
processEnv,
).pipe(
Effect.map(stampIdentity),
Effect.flatMap((nextSnapshot) =>
Ref.modify(knownVersionedModels, (knownModels) => {
const reconciled = reconcileKnownClaudeModelsAfterVersionProbe(
knownModels,
nextSnapshot,
);
return [reconciled.snapshot, reconciled.knownModels];
}),
),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(Path.Path, path),
);
Expand Down
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
148 changes: 100 additions & 48 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type ClaudeSettings,
type ModelCapabilities,
type ModelSelection,
type ServerProvider,
type ServerProviderModel,
type ServerProviderSlashCommand,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -47,10 +48,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 +108,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
}),
},
{
slug: "claude-opus-4-8",
name: "Claude Opus 4.8",
slug: "claude-opus-5",
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,
},
{
slug: "claude-opus-4-7",
Expand Down Expand Up @@ -268,12 +271,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 +287,69 @@ 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 isVersionGatedClaudeBuiltInModel(model: ServerProviderModel): boolean {
return (
!model.isCustom &&
(model.slug === "claude-opus-5" ||
model.slug === "claude-fable-5" ||
model.slug === "claude-opus-4-7")
);
}

export function reconcileKnownClaudeModelsAfterVersionProbe(
knownModels: ReadonlyArray<ServerProviderModel>,
nextSnapshot: ServerProvider,
): {
readonly snapshot: ServerProvider;
readonly knownModels: ReadonlyArray<ServerProviderModel>;
} {
if (!nextSnapshot.installed) {
return { snapshot: nextSnapshot, knownModels: [] };
}
if (nextSnapshot.version !== null) {
return {
snapshot: nextSnapshot,
knownModels: nextSnapshot.models.filter(isVersionGatedClaudeBuiltInModel),
};
}
if (nextSnapshot.status !== "error" || knownModels.length === 0) {
return { snapshot: nextSnapshot, knownModels };
}

const nextSlugs = new Set(nextSnapshot.models.map((model) => model.slug));
return {
snapshot: {
...nextSnapshot,
models: [
...nextSnapshot.models,
...knownModels.filter((model) => !nextSlugs.has(model.slug)),
Comment thread
wizzoapp[bot] marked this conversation as resolved.
],
},
knownModels,
};
}

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 +359,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 +406,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 +755,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),
Comment thread
wizzoapp[bot] marked this conversation as resolved.
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down Expand Up @@ -772,6 +823,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 +838,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 +849,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 +914,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