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
1 change: 1 addition & 0 deletions apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
[
"exec",
"--ephemeral",
"--skip-git-repo-check",
"-s",
"read-only",
"--model",
Expand Down
47 changes: 47 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,53 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("defaults Claude Opus 4.7 sessions to xhigh effort", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: "claudeAgent",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-7",
},
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("forwards xhigh effort for Claude Opus 4.7", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: "claudeAgent",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-7",
options: {
effort: "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
16 changes: 8 additions & 8 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
ThreadId,
TurnId,
type UserInputQuestion,
ClaudeCodeEffort,
ClaudeAgentEffort,
type ModelCapabilities,
RuntimeMode,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -221,10 +221,10 @@ function isSyntheticClaudeThreadId(value: string): boolean {
return value.startsWith("claude-thread-");
}

function getEffectiveClaudeCodeEffort(
function getEffectiveClaudeAgentEffort(
caps: ModelCapabilities,
rawEffort: string | null | undefined,
): Exclude<ClaudeCodeEffort, "ultrathink"> | null {
): Exclude<ClaudeAgentEffort, "ultrathink"> | null {
const promptInjected = new Set(caps.promptInjectedEffortLevels);
const supportedNonPromptLevels = caps.reasoningEffortLevels
.map((level) => level.value)
Expand All @@ -233,13 +233,13 @@ function getEffectiveClaudeCodeEffort(
const trimmed = trimOrNull(rawEffort);

if (trimmed && supportedNonPromptLevels.includes(trimmed)) {
return trimmed as Exclude<ClaudeCodeEffort, "ultrathink">;
return trimmed as Exclude<ClaudeAgentEffort, "ultrathink">;
}

if (!trimmed) {
const defaultValue = caps.reasoningEffortLevels.find((level) => level.isDefault)?.value;
return defaultValue && !promptInjected.has(defaultValue)
? (defaultValue as Exclude<ClaudeCodeEffort, "ultrathink">)
? (defaultValue as Exclude<ClaudeAgentEffort, "ultrathink">)
: null;
}

Expand All @@ -249,7 +249,7 @@ function getEffectiveClaudeCodeEffort(
return null;
}
return supportedNonPromptLevels[supportedNonPromptLevels.length - 1] as Exclude<
ClaudeCodeEffort,
ClaudeAgentEffort,
"ultrathink"
>;
}
Expand Down Expand Up @@ -315,7 +315,7 @@ function maxClaudeContextWindowFromModelUsage(
}

function normalizeClaudeTokenUsage(
value: Record<string, unknown> | undefined,
value: unknown,
contextWindow?: number,
): ThreadTokenUsageSnapshot | undefined {
if (!value || typeof value !== "object") {
Expand Down Expand Up @@ -2956,7 +2956,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle
? modelSelection.options.thinking
: undefined;
const effectiveEffort = getEffectiveClaudeCodeEffort(caps, modelSelection?.options?.effort);
const effectiveEffort = getEffectiveClaudeAgentEffort(caps, modelSelection?.options?.effort);
const runtimeModeToPermission: Record<RuntimeMode, PermissionMode> = {
"approval-required": "default",
"auto-accept-edits": "acceptEdits",
Expand Down
48 changes: 41 additions & 7 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
spawnAndCollect,
type CommandResult,
} from "../providerSnapshot";
import { compareCliVersions } from "../cliVersion";
import { makeManagedServerProvider } from "../makeManagedServerProvider";
import { ClaudeProvider } from "../Services/ClaudeProvider";
import { ServerSettingsService } from "../../serverSettings";
Expand All @@ -43,6 +44,7 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = {
};

const PROVIDER = "claudeAgent" as const;
const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: "claude-opus-4-7",
Expand All @@ -53,7 +55,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "xhigh", label: "XHigh", isDefault: true },
{ value: "xhigh", label: "Extra High", isDefault: true },
{ value: "max", label: "Max" },
{ value: "ultrathink", label: "Ultrathink" },
],
Expand Down Expand Up @@ -121,6 +123,24 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
},
];

function supportsClaudeOpus47(version: string | null | undefined): boolean {
return version ? compareCliVersions(version, MINIMUM_CLAUDE_OPUS_4_7_VERSION) >= 0 : false;
}

function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
if (supportsClaudeOpus47(version)) {
return BUILT_IN_MODELS;
}
return BUILT_IN_MODELS.filter((model) => model.slug !== "claude-opus-4-7");
}

function formatClaudeOpus47UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Opus 4.7. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_7_VERSION} or newer to access it.`;
Comment on lines +126 to +141
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

supportsClaudeOpus47() treats null/undefined versions as unsupported. When parseGenericCliVersion() fails (returns null), this will both hide Opus 4.7 and surface a "too old" upgrade message even though the version is unknown. Consider treating an unparseable/unknown version as "unknown support" (e.g., keep all built-in models and omit the upgrade message), or improve version parsing so parsedVersion is only null on truly missing output.

Suggested change
function supportsClaudeOpus47(version: string | null | undefined): boolean {
return version ? compareCliVersions(version, MINIMUM_CLAUDE_OPUS_4_7_VERSION) >= 0 : false;
}
function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
if (supportsClaudeOpus47(version)) {
return BUILT_IN_MODELS;
}
return BUILT_IN_MODELS.filter((model) => model.slug !== "claude-opus-4-7");
}
function formatClaudeOpus47UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Opus 4.7. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_7_VERSION} or newer to access it.`;
function getClaudeOpus47Support(
version: string | null | undefined,
): "supported" | "unsupported" | "unknown" {
if (!version) {
return "unknown";
}
return compareCliVersions(version, MINIMUM_CLAUDE_OPUS_4_7_VERSION) >= 0
? "supported"
: "unsupported";
}
function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
const opus47Support = getClaudeOpus47Support(version);
if (opus47Support !== "unsupported") {
return BUILT_IN_MODELS;
}
return BUILT_IN_MODELS.filter((model) => model.slug !== "claude-opus-4-7");
}
function formatClaudeOpus47UpgradeMessage(version: string | null): string | undefined {
if (getClaudeOpus47Support(version) !== "unsupported") {
return undefined;
}
return `Claude Code v${version} is too old for Claude Opus 4.7. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_7_VERSION} or newer to access it.`;

Copilot uses AI. Check for mistakes.
}

export function getClaudeModelCapabilities(model: string | null | undefined): ModelCapabilities {
const slug = model?.trim();
return (
Expand Down Expand Up @@ -509,7 +529,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
Effect.map((settings) => settings.providers.claudeAgent),
);
const checkedAt = new Date().toISOString();
const models = providerModelsFromSettings(
const allModels = providerModelsFromSettings(
BUILT_IN_MODELS,
PROVIDER,
claudeSettings.customModels,
Expand All @@ -524,7 +544,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
provider: PROVIDER,
enabled: false,
checkedAt,
models,
models: allModels,
probe: {
installed: false,
version: null,
Expand All @@ -547,7 +567,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
provider: PROVIDER,
enabled: claudeSettings.enabled,
checkedAt,
models,
models: allModels,
probe: {
installed: !isCommandMissingCause(error),
version: null,
Expand All @@ -565,7 +585,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
provider: PROVIDER,
enabled: claudeSettings.enabled,
checkedAt,
models,
models: allModels,
probe: {
installed: true,
version: null,
Expand All @@ -585,7 +605,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
provider: PROVIDER,
enabled: claudeSettings.enabled,
checkedAt,
models,
models: allModels,
probe: {
installed: true,
version: parsedVersion,
Expand All @@ -598,6 +618,16 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
});
}

const models = providerModelsFromSettings(
getBuiltInClaudeModelsForVersion(parsedVersion),
PROVIDER,
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
const opus47UpgradeMessage = supportsClaudeOpus47(parsedVersion)
? undefined
: formatClaudeOpus47UpgradeMessage(parsedVersion);

const slashCommands =
(resolveSlashCommands
? yield* resolveSlashCommands(claudeSettings.binaryPath).pipe(
Expand Down Expand Up @@ -684,7 +714,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
...parsed.auth,
...(authMetadata ? authMetadata : {}),
},
...(parsed.message ? { message: parsed.message } : {}),
...(parsed.message
? { message: parsed.message }
: opus47UpgradeMessage
? { message: opus47UpgradeMessage }
: {}),
},
...(cachedUsageLimits ? { usageLimits: cachedUsageLimits } : {}),
});
Expand Down
63 changes: 63 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,69 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest()))(
),
);

it.effect(
"includes Claude Opus 4.7 with xhigh as the default effort on supported versions",
() =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus();
const opus47 = status.models.find((model) => model.slug === "claude-opus-4-7");
if (!opus47) {
assert.fail("Expected Claude Opus 4.7 to be present for Claude Code v2.1.111.");
}
if (!opus47.capabilities) {
assert.fail(
"Expected Claude Opus 4.7 capabilities to be present for Claude Code v2.1.111.",
);
}
assert.deepStrictEqual(
opus47.capabilities.reasoningEffortLevels.find((level) => level.isDefault),
{ value: "xhigh", label: "Extra High", isDefault: true },
);
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.111\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 Opus 4.7 on older Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus();
assert.strictEqual(
status.models.some((model) => model.slug === "claude-opus-4-7"),
false,
);
assert.strictEqual(
status.message,
"Claude Code v2.1.110 is too old for Claude Opus 4.7. Upgrade to v2.1.111 or newer to access it.",
);
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.110\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("returns a display label for claude subscription types", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(() => Effect.succeed("maxplan"));
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/provider/cliVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { assert, describe, it } from "@effect/vitest";

import { compareCliVersions, normalizeCliVersion } from "./cliVersion";

describe("cliVersion", () => {
it("normalizes versions with a missing patch segment", () => {
assert.strictEqual(normalizeCliVersion("2.1"), "2.1.0");
});

it("compares prerelease versions before stable versions", () => {
assert.isTrue(compareCliVersions("2.1.111-beta.1", "2.1.111") < 0);
});

it("rejects malformed numeric segments", () => {
assert.isTrue(compareCliVersions("1.2.3abc", "1.2.10") > 0);
});
});
Loading
Loading