From c54bd2b89e86fe7ab181a42a73fd36870ade8799 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:31:35 +0530 Subject: [PATCH 1/4] fix(server): drop stale text generation options when resetting text-gen model selection (#2076) (cherry picked from commit 7a08fcf2e832da5969aeb373c46d5249b4e820b7) --- apps/server/src/serverSettings.test.ts | 29 +++++++++++ apps/server/src/serverSettings.ts | 20 +++++++- apps/web/src/hooks/useSettings.ts | 4 +- packages/shared/src/serverSettings.test.ts | 59 ++++++++++++++++++++++ packages/shared/src/serverSettings.ts | 34 ++++++++++++- 5 files changed, 141 insertions(+), 5 deletions(-) diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d8a992f0ec3..26479d61bd6 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -142,6 +142,35 @@ it.layer(NodeServices.layer)("server settings", (it) => { }).pipe(Effect.provide(makeServerSettingsLayer())), ); + it.effect("drops stale text generation options when resetting model selection", () => + Effect.gen(function* () { + const serverSettings = yield* ServerSettingsService; + + yield* serverSettings.updateSettings({ + textGenerationModelSelection: { + provider: "codex", + model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, + options: { + reasoningEffort: "high", + fastMode: true, + }, + }, + }); + + const next = yield* serverSettings.updateSettings({ + textGenerationModelSelection: { + provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider, + model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, + }, + }); + + assert.deepEqual(next.textGenerationModelSelection, { + provider: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.provider, + model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model, + }); + }).pipe(Effect.provide(makeServerSettingsLayer())), + ); + it.effect("trims provider path settings when updates are applied", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 2d32b63190d..ded3d25f8f8 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -42,6 +42,7 @@ import * as Semaphore from "effect/Semaphore"; import { ServerConfig } from "./config"; import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; +import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; export interface ServerSettingsShape { /** Start the settings runtime and attach file watching. */ @@ -80,7 +81,20 @@ export class ServerSettingsService extends Context.Service< getSettings: Ref.get(currentSettingsRef), updateSettings: (patch) => Ref.get(currentSettingsRef).pipe( - Effect.map((currentSettings) => deepMerge(currentSettings, patch)), + Effect.flatMap((currentSettings) => + Schema.decodeEffect(ServerSettings)( + applyServerSettingsPatch(currentSettings, patch), + ).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath: "", + detail: `failed to normalize server settings: ${SchemaIssue.makeFormatterDefault()(cause.issue)}`, + cause, + }), + ), + ), + ), Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)), ), streamChanges: Stream.empty, @@ -314,7 +328,9 @@ const makeServerSettings = Effect.gen(function* () { writeSemaphore.withPermits(1)( Effect.gen(function* () { const current = yield* getSettingsFromCache; - const next = yield* Schema.decodeEffect(ServerSettings)(deepMerge(current, patch)).pipe( + const next = yield* Schema.decodeEffect(ServerSettings)( + applyServerSettingsPatch(current, patch), + ).pipe( Effect.mapError( (cause) => new ServerSettingsError({ diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 3dc2cf9b201..664d5ee3bb0 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -19,7 +19,7 @@ import { } from "@t3tools/contracts/settings"; import { ensureLocalApi } from "~/localApi"; import { Struct } from "effect"; -import { deepMerge } from "@t3tools/shared/Struct"; +import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; import { applySettingsUpdated, getServerConfig, useServerSettings } from "~/rpc/serverState"; const CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE = "[CLIENT_SETTINGS]"; @@ -154,7 +154,7 @@ export function useUpdateSettings() { if (Object.keys(serverPatch).length > 0) { const currentServerConfig = getServerConfig(); if (currentServerConfig) { - applySettingsUpdated(deepMerge(currentServerConfig.settings, serverPatch)); + applySettingsUpdated(applyServerSettingsPatch(currentServerConfig.settings, serverPatch)); } // Fire-and-forget RPC — push will reconcile on success void ensureLocalApi().server.updateSettings(serverPatch); diff --git a/packages/shared/src/serverSettings.test.ts b/packages/shared/src/serverSettings.test.ts index 0ac5e415dff..3d4a0da0bb1 100644 --- a/packages/shared/src/serverSettings.test.ts +++ b/packages/shared/src/serverSettings.test.ts @@ -1,5 +1,7 @@ +import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; import { + applyServerSettingsPatch, extractPersistedServerObservabilitySettings, normalizePersistedServerSettingString, parsePersistedServerObservabilitySettings, @@ -50,4 +52,61 @@ describe("serverSettings helpers", () => { otlpMetricsUrl: undefined, }); }); + + it("replaces text generation selection when provider/model are provided", () => { + const current = { + ...DEFAULT_SERVER_SETTINGS, + textGenerationModelSelection: { + provider: "codex" as const, + model: "gpt-5.4-mini", + options: { + reasoningEffort: "high" as const, + fastMode: true, + }, + }, + }; + + expect( + applyServerSettingsPatch(current, { + textGenerationModelSelection: { + provider: "codex", + model: "gpt-5.4-mini", + }, + }).textGenerationModelSelection, + ).toEqual({ + provider: "codex", + model: "gpt-5.4-mini", + }); + }); + + it("still deep merges text generation selection when only options are provided", () => { + const current = { + ...DEFAULT_SERVER_SETTINGS, + textGenerationModelSelection: { + provider: "codex" as const, + model: "gpt-5.4-mini", + options: { + reasoningEffort: "high" as const, + fastMode: true, + }, + }, + }; + + expect( + applyServerSettingsPatch(current, { + textGenerationModelSelection: { + options: { + fastMode: false, + }, + }, + }).textGenerationModelSelection, + ).toEqual({ + provider: "codex", + model: "gpt-5.4-mini", + options: { + reasoningEffort: "high", + fastMode: false, + }, + }); + }); }); diff --git a/packages/shared/src/serverSettings.ts b/packages/shared/src/serverSettings.ts index e7b25606dc1..db9bdcc591e 100644 --- a/packages/shared/src/serverSettings.ts +++ b/packages/shared/src/serverSettings.ts @@ -1,5 +1,6 @@ -import { ServerSettings } from "@t3tools/contracts"; +import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts"; import { Schema } from "effect"; +import { deepMerge } from "./Struct"; import { fromLenientJson } from "./schemaJson"; const ServerSettingsJson = fromLenientJson(ServerSettings); @@ -38,3 +39,34 @@ export function parsePersistedServerObservabilitySettings( return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } } + +function shouldReplaceTextGenerationModelSelection( + patch: ServerSettingsPatch["textGenerationModelSelection"] | undefined, +): boolean { + return Boolean(patch && (patch.provider !== undefined || patch.model !== undefined)); +} + +/** + * Applies a server settings patch while treating textGenerationModelSelection as + * replace-on-provider/model updates. This prevents stale nested options from + * surviving a reset patch that intentionally omits options. + */ +export function applyServerSettingsPatch( + current: ServerSettings, + patch: ServerSettingsPatch, +): ServerSettings { + const selectionPatch = patch.textGenerationModelSelection; + const next = deepMerge(current, patch); + if (!selectionPatch || !shouldReplaceTextGenerationModelSelection(selectionPatch)) { + return next; + } + + return { + ...next, + textGenerationModelSelection: { + provider: selectionPatch.provider ?? current.textGenerationModelSelection.provider, + model: selectionPatch.model ?? current.textGenerationModelSelection.model, + ...(selectionPatch.options ? { options: selectionPatch.options } : {}), + }, + }; +} From 8e53c296d37d26a32b8a3fe0cb02ecf8de83dc21 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:43:59 +0530 Subject: [PATCH 2/4] fix(web): prevent composer controls overlap on narrow windows (make plan sidebar responsive) (#1198) (cherry picked from commit 19d47408a53181446a02fac967846cc15d77d5b8) --- apps/web/src/components/ChatView.tsx | 34 +++++++++++++---- apps/web/src/components/PlanSidebar.tsx | 11 +++++- apps/web/src/components/RightPanelSheet.tsx | 30 +++++++++++++++ apps/web/src/rightPanelLayout.ts | 2 + .../routes/_chat.$environmentId.$threadId.tsx | 38 +++---------------- 5 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 apps/web/src/components/RightPanelSheet.tsx create mode 100644 apps/web/src/rightPanelLayout.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 682e57e70bf..8bd9c9161b5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -93,6 +93,8 @@ import { useTheme } from "../hooks/useTheme"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; +import { useMediaQuery } from "../hooks/useMediaQuery"; +import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { BranchToolbar } from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; @@ -172,6 +174,7 @@ import { } from "~/rpc/serverState"; import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; import { retainThreadDetailSubscription } from "../environments/runtime/service"; +import { RightPanelSheet } from "./RightPanelSheet"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; @@ -676,6 +679,7 @@ export default function ChatView(props: ChatViewProps) { const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = useState>({}); const [planSidebarOpen, setPlanSidebarOpen] = useState(false); + const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); // Tracks whether the user explicitly dismissed the sidebar for the active turn. const planSidebarDismissedForTurnRef = useRef(null); // When set, the thread-change reset effect will open the sidebar instead of closing it. @@ -1897,6 +1901,11 @@ export default function ChatView(props: ChatViewProps) { return !open; }); }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); + const closePlanSidebar = useCallback(() => { + setPlanSidebarOpen(false); + planSidebarDismissedForTurnRef.current = + activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; + }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); const persistThreadSettingsForNextTurn = useCallback( async (input: { @@ -3397,7 +3406,7 @@ export default function ChatView(props: ChatViewProps) { {/* end chat column */} {/* Plan sidebar */} - {planSidebarOpen ? ( + {planSidebarOpen && !shouldUsePlanSidebarSheet ? ( { - setPlanSidebarOpen(false); - // Track that the user explicitly dismissed for this turn so auto-open won't fight them. - planSidebarDismissedForTurnRef.current = - activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; - }} + mode="sidebar" + onClose={closePlanSidebar} /> ) : null} @@ -3433,6 +3438,21 @@ export default function ChatView(props: ChatViewProps) { onAddTerminalContext={addTerminalContextToDraft} /> ))} + {shouldUsePlanSidebarSheet ? ( + + + + ) : null} {expandedImage && ( diff --git a/apps/web/src/components/PlanSidebar.tsx b/apps/web/src/components/PlanSidebar.tsx index 489e38f48d9..00b9da2b0c8 100644 --- a/apps/web/src/components/PlanSidebar.tsx +++ b/apps/web/src/components/PlanSidebar.tsx @@ -59,6 +59,7 @@ interface PlanSidebarProps { markdownCwd: string | undefined; workspaceRoot: string | undefined; timestampFormat: TimestampFormat; + mode?: "sheet" | "sidebar"; onClose: () => void; } @@ -70,6 +71,7 @@ const PlanSidebar = memo(function PlanSidebar({ markdownCwd, workspaceRoot, timestampFormat, + mode = "sidebar", onClose, }: PlanSidebarProps) { const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); @@ -123,7 +125,14 @@ const PlanSidebar = memo(function PlanSidebar({ }, [environmentId, planMarkdown, workspaceRoot]); return ( -
+
{/* Header */}
diff --git a/apps/web/src/components/RightPanelSheet.tsx b/apps/web/src/components/RightPanelSheet.tsx new file mode 100644 index 00000000000..ebc4aa0a698 --- /dev/null +++ b/apps/web/src/components/RightPanelSheet.tsx @@ -0,0 +1,30 @@ +import { type ReactNode } from "react"; + +import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout"; +import { Sheet, SheetPopup } from "./ui/sheet"; + +export function RightPanelSheet(props: { + children: ReactNode; + open: boolean; + onClose: () => void; +}) { + return ( + { + if (!open) { + props.onClose(); + } + }} + > + + {props.children} + + + ); +} diff --git a/apps/web/src/rightPanelLayout.ts b/apps/web/src/rightPanelLayout.ts new file mode 100644 index 00000000000..c94f52a9cb2 --- /dev/null +++ b/apps/web/src/rightPanelLayout.ts @@ -0,0 +1,2 @@ +export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; +export const RIGHT_PANEL_SHEET_CLASS_NAME = "w-[min(88vw,820px)] max-w-[820px] p-0"; diff --git a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx index fa3f59b93f3..ff20673e2de 100644 --- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx +++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx @@ -1,5 +1,5 @@ import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; -import { Suspense, lazy, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react"; import ChatView from "../components/ChatView"; import { threadHasStarted } from "../components/ChatView.logic"; @@ -17,45 +17,19 @@ import { stripDiffSearchParams, } from "../diffRouteSearch"; import { useMediaQuery } from "../hooks/useMediaQuery"; +import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; import { createThreadSelectorByRef } from "../storeSelectors"; import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; -import { Sheet, SheetPopup } from "../components/ui/sheet"; +import { RightPanelSheet } from "../components/RightPanelSheet"; import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; const DiffPanel = lazy(() => import("../components/DiffPanel")); -const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; -const DiffPanelSheet = (props: { - children: ReactNode; - diffOpen: boolean; - onCloseDiff: () => void; -}) => { - return ( - { - if (!open) { - props.onCloseDiff(); - } - }} - > - - {props.children} - - - ); -}; - const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { return ( }> @@ -192,7 +166,7 @@ function ChatThreadRouteView() { const serverThreadStarted = threadHasStarted(serverThread); const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads; const diffOpen = search.diff === "1"; - const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null; const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({ threadKey: currentThreadKey, @@ -293,9 +267,9 @@ function ChatThreadRouteView() { routeKind="server" /> - + {shouldRenderDiffContent ? : null} - + ); } From 1d358ef606945ac864fe47b8868c8a64bb251241 Mon Sep 17 00:00:00 2001 From: Ibrahim Elkamali <126423069+Marve10s@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:11:59 +0300 Subject: [PATCH 3/4] feat: add Claude Opus 4.7 to built-in models (#2072) Co-authored-by: Julius Marminge (cherry picked from commit 3e07f5a6a412be551e1d74fe4600a31e5ef557c4) --- .../src/git/Layers/CodexTextGeneration.ts | 1 + .../src/provider/Layers/ClaudeAdapter.test.ts | 47 +++++++ .../src/provider/Layers/ClaudeAdapter.ts | 14 +- .../src/provider/Layers/ClaudeProvider.ts | 48 ++++++- .../provider/Layers/ProviderRegistry.test.ts | 63 +++++++++ apps/server/src/provider/cliVersion.test.ts | 17 +++ apps/server/src/provider/cliVersion.ts | 123 +++++++++++++++++ apps/server/src/provider/codexCliVersion.ts | 124 +----------------- apps/web/src/components/ChatView.tsx | 4 +- apps/web/src/composerDraftStore.ts | 36 ++--- apps/web/src/modelSelection.ts | 4 +- packages/contracts/src/model.ts | 18 +-- packages/shared/src/claudeTierDefaults.ts | 4 +- packages/shared/src/model.ts | 4 +- 14 files changed, 332 insertions(+), 175 deletions(-) create mode 100644 apps/server/src/provider/cliVersion.test.ts create mode 100644 apps/server/src/provider/cliVersion.ts diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 52ddf554532..be1c6798c94 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -166,6 +166,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { [ "exec", "--ephemeral", + "--skip-git-repo-check", "-s", "read-only", "--model", diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 5f86b798e3d..afa0527764e 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -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* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 7ea4b6be2b4..9571a279ac6 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -41,7 +41,7 @@ import { ThreadId, TurnId, type UserInputQuestion, - ClaudeCodeEffort, + ClaudeAgentEffort, RuntimeMode, } from "@t3tools/contracts"; import { @@ -221,9 +221,9 @@ function isSyntheticClaudeThreadId(value: string): boolean { return value.startsWith("claude-thread-"); } -function getEffectiveClaudeCodeEffort( - effort: ClaudeCodeEffort | null | undefined, -): Exclude | null { +function getEffectiveClaudeAgentEffort( + effort: ClaudeAgentEffort | null | undefined, +): Exclude | null { if (!effort) { return null; } @@ -291,7 +291,7 @@ function maxClaudeContextWindowFromModelUsage( } function normalizeClaudeTokenUsage( - value: Record | undefined, + value: unknown, contextWindow?: number, ): ThreadTokenUsageSnapshot | undefined { if (!value || typeof value !== "object") { @@ -2928,13 +2928,13 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const caps = getClaudeModelCapabilities(modelSelection?.model); const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined; const effort = (resolveEffort(caps, modelSelection?.options?.effort) ?? - null) as ClaudeCodeEffort | null; + null) as ClaudeAgentEffort | null; const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode; const thinking = typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle ? modelSelection.options.thinking : undefined; - const effectiveEffort = getEffectiveClaudeCodeEffort(effort); + const effectiveEffort = getEffectiveClaudeAgentEffort(effort); const runtimeModeToPermission: Record = { "approval-required": "default", "auto-accept-edits": "acceptEdits", diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 833e88b7bcb..2a33152971a 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -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"; @@ -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 = [ { slug: "claude-opus-4-7", @@ -53,7 +55,7 @@ const BUILT_IN_MODELS: ReadonlyArray = [ { 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" }, ], @@ -121,6 +123,24 @@ const BUILT_IN_MODELS: ReadonlyArray = [ }, ]; +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 { + 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.`; +} + export function getClaudeModelCapabilities(model: string | null | undefined): ModelCapabilities { const slug = model?.trim(); return ( @@ -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, @@ -524,7 +544,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( provider: PROVIDER, enabled: false, checkedAt, - models, + models: allModels, probe: { installed: false, version: null, @@ -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, @@ -565,7 +585,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( provider: PROVIDER, enabled: claudeSettings.enabled, checkedAt, - models, + models: allModels, probe: { installed: true, version: null, @@ -585,7 +605,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( provider: PROVIDER, enabled: claudeSettings.enabled, checkedAt, - models, + models: allModels, probe: { installed: true, version: parsedVersion, @@ -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( @@ -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 } : {}), }); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 5db22ca4139..b00c0087266 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -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")); diff --git a/apps/server/src/provider/cliVersion.test.ts b/apps/server/src/provider/cliVersion.test.ts new file mode 100644 index 00000000000..a9c1721c4e8 --- /dev/null +++ b/apps/server/src/provider/cliVersion.test.ts @@ -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); + }); +}); diff --git a/apps/server/src/provider/cliVersion.ts b/apps/server/src/provider/cliVersion.ts new file mode 100644 index 00000000000..6308a2ff525 --- /dev/null +++ b/apps/server/src/provider/cliVersion.ts @@ -0,0 +1,123 @@ +interface ParsedCliSemver { + readonly major: number; + readonly minor: number; + readonly patch: number; + readonly prerelease: ReadonlyArray; +} + +const CLI_VERSION_NUMBER_SEGMENT = /^\d+$/; + +export function normalizeCliVersion(version: string): string { + const [main, prerelease] = version.trim().split("-", 2); + const segments = (main ?? "") + .split(".") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0); + + if (segments.length === 2) { + segments.push("0"); + } + + return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join("."); +} + +function parseCliSemver(version: string): ParsedCliSemver | null { + const normalized = normalizeCliVersion(version); + const [main = "", prerelease] = normalized.split("-", 2); + const segments = main.split("."); + if (segments.length !== 3) { + return null; + } + + const [majorSegment, minorSegment, patchSegment] = segments; + if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) { + return null; + } + if ( + !CLI_VERSION_NUMBER_SEGMENT.test(majorSegment) || + !CLI_VERSION_NUMBER_SEGMENT.test(minorSegment) || + !CLI_VERSION_NUMBER_SEGMENT.test(patchSegment) + ) { + return null; + } + + const major = Number.parseInt(majorSegment, 10); + const minor = Number.parseInt(minorSegment, 10); + const patch = Number.parseInt(patchSegment, 10); + if (![major, minor, patch].every(Number.isInteger)) { + return null; + } + + return { + major, + minor, + patch, + prerelease: + prerelease + ?.split(".") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0) ?? [], + }; +} + +function comparePrereleaseIdentifier(left: string, right: string): number { + const leftNumeric = /^\d+$/.test(left); + const rightNumeric = /^\d+$/.test(right); + + if (leftNumeric && rightNumeric) { + return Number.parseInt(left, 10) - Number.parseInt(right, 10); + } + if (leftNumeric) { + return -1; + } + if (rightNumeric) { + return 1; + } + return left.localeCompare(right); +} + +export function compareCliVersions(left: string, right: string): number { + const parsedLeft = parseCliSemver(left); + const parsedRight = parseCliSemver(right); + if (!parsedLeft || !parsedRight) { + return left.localeCompare(right); + } + + if (parsedLeft.major !== parsedRight.major) { + return parsedLeft.major - parsedRight.major; + } + if (parsedLeft.minor !== parsedRight.minor) { + return parsedLeft.minor - parsedRight.minor; + } + if (parsedLeft.patch !== parsedRight.patch) { + return parsedLeft.patch - parsedRight.patch; + } + + if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) { + return 0; + } + if (parsedLeft.prerelease.length === 0) { + return 1; + } + if (parsedRight.prerelease.length === 0) { + return -1; + } + + const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length); + for (let index = 0; index < length; index += 1) { + const leftIdentifier = parsedLeft.prerelease[index]; + const rightIdentifier = parsedRight.prerelease[index]; + if (leftIdentifier === undefined) { + return -1; + } + if (rightIdentifier === undefined) { + return 1; + } + const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier); + if (comparison !== 0) { + return comparison; + } + } + + return 0; +} diff --git a/apps/server/src/provider/codexCliVersion.ts b/apps/server/src/provider/codexCliVersion.ts index 544020016c6..87194833501 100644 --- a/apps/server/src/provider/codexCliVersion.ts +++ b/apps/server/src/provider/codexCliVersion.ts @@ -1,121 +1,10 @@ +import { compareCliVersions, normalizeCliVersion } from "./cliVersion"; + const CODEX_VERSION_PATTERN = /\bv?(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)\b/; export const MINIMUM_CODEX_CLI_VERSION = "0.37.0"; -interface ParsedSemver { - readonly major: number; - readonly minor: number; - readonly patch: number; - readonly prerelease: ReadonlyArray; -} - -function normalizeCodexVersion(version: string): string { - const [main, prerelease] = version.trim().split("-", 2); - const segments = (main ?? "") - .split(".") - .map((segment) => segment.trim()) - .filter((segment) => segment.length > 0); - - if (segments.length === 2) { - segments.push("0"); - } - - return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join("."); -} - -function parseSemver(version: string): ParsedSemver | null { - const normalized = normalizeCodexVersion(version); - const [main = "", prerelease] = normalized.split("-", 2); - const segments = main.split("."); - if (segments.length !== 3) { - return null; - } - - const [majorSegment, minorSegment, patchSegment] = segments; - if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) { - return null; - } - - const major = Number.parseInt(majorSegment, 10); - const minor = Number.parseInt(minorSegment, 10); - const patch = Number.parseInt(patchSegment, 10); - if (![major, minor, patch].every(Number.isInteger)) { - return null; - } - - return { - major, - minor, - patch, - prerelease: - prerelease - ?.split(".") - .map((segment) => segment.trim()) - .filter((segment) => segment.length > 0) ?? [], - }; -} - -function comparePrereleaseIdentifier(left: string, right: string): number { - const leftNumeric = /^\d+$/.test(left); - const rightNumeric = /^\d+$/.test(right); - - if (leftNumeric && rightNumeric) { - return Number.parseInt(left, 10) - Number.parseInt(right, 10); - } - if (leftNumeric) { - return -1; - } - if (rightNumeric) { - return 1; - } - return left.localeCompare(right); -} - -export function compareCodexCliVersions(left: string, right: string): number { - const parsedLeft = parseSemver(left); - const parsedRight = parseSemver(right); - if (!parsedLeft || !parsedRight) { - return left.localeCompare(right); - } - - if (parsedLeft.major !== parsedRight.major) { - return parsedLeft.major - parsedRight.major; - } - if (parsedLeft.minor !== parsedRight.minor) { - return parsedLeft.minor - parsedRight.minor; - } - if (parsedLeft.patch !== parsedRight.patch) { - return parsedLeft.patch - parsedRight.patch; - } - - if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) { - return 0; - } - if (parsedLeft.prerelease.length === 0) { - return 1; - } - if (parsedRight.prerelease.length === 0) { - return -1; - } - - const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length); - for (let index = 0; index < length; index += 1) { - const leftIdentifier = parsedLeft.prerelease[index]; - const rightIdentifier = parsedRight.prerelease[index]; - if (leftIdentifier === undefined) { - return -1; - } - if (rightIdentifier === undefined) { - return 1; - } - const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier); - if (comparison !== 0) { - return comparison; - } - } - - return 0; -} +export const compareCodexCliVersions = compareCliVersions; export function parseCodexCliVersion(output: string): string | null { const match = CODEX_VERSION_PATTERN.exec(output); @@ -123,12 +12,7 @@ export function parseCodexCliVersion(output: string): string | null { return null; } - const parsed = parseSemver(match[1]); - if (!parsed) { - return null; - } - - return normalizeCodexVersion(match[1]); + return normalizeCliVersion(match[1]); } export function isCodexCliVersionSupported(version: string): boolean { diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 8bd9c9161b5..34c04466c17 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2,7 +2,7 @@ import { type ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_PROVIDER_KIND, - type ClaudeCodeEffort, + type ClaudeAgentEffort, type EnvironmentId, type MessageId, type ModelSelection, @@ -307,7 +307,7 @@ function formatOutgoingPrompt(params: { }): string { const caps = getProviderModelCapabilities(params.models, params.model, params.provider); if (params.effort && caps.promptInjectedEffortLevels.includes(params.effort)) { - return applyClaudePromptEffortPrefix(params.text, params.effort as ClaudeCodeEffort | null); + return applyClaudePromptEffortPrefix(params.text, params.effort as ClaudeAgentEffort | null); } return params.text; } diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 0c5e5c9bc32..4cb88629dff 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -1,7 +1,6 @@ import { - CODEX_REASONING_EFFORT_OPTIONS, - type ClaudeCodeEffort, - type CodexReasoningEffort, + ClaudeAgentEffort, + CodexReasoningEffort, DEFAULT_MODEL_BY_PROVIDER, type EnvironmentId, ModelSelection, @@ -105,7 +104,7 @@ const PersistedComposerThreadDraftState = Schema.Struct({ type PersistedComposerThreadDraftState = typeof PersistedComposerThreadDraftState.Type; const LegacyCodexFields = Schema.Struct({ - effort: Schema.optionalKey(Schema.Literals(CODEX_REASONING_EFFORT_OPTIONS)), + effort: Schema.optionalKey(CodexReasoningEffort), codexFastMode: Schema.optionalKey(Schema.Boolean), serviceTier: Schema.optionalKey(Schema.String), }); @@ -546,19 +545,13 @@ function normalizeProviderModelOptions( ? (candidate.claudeAgent as Record) : null; - const codexReasoningEffort: CodexReasoningEffort | undefined = - codexCandidate?.reasoningEffort === "low" || - codexCandidate?.reasoningEffort === "medium" || - codexCandidate?.reasoningEffort === "high" || - codexCandidate?.reasoningEffort === "xhigh" - ? codexCandidate.reasoningEffort - : provider === "codex" && - (legacy?.effort === "low" || - legacy?.effort === "medium" || - legacy?.effort === "high" || - legacy?.effort === "xhigh") + const codexReasoningEffort = Schema.is(CodexReasoningEffort)(codexCandidate?.reasoningEffort) + ? codexCandidate.reasoningEffort + : provider === "codex" + ? Schema.is(CodexReasoningEffort)(legacy?.effort) ? legacy.effort - : undefined; + : undefined + : undefined; const codexFastMode = codexCandidate?.fastMode === true ? true @@ -582,14 +575,9 @@ function normalizeProviderModelOptions( : claudeCandidate?.thinking === false ? false : undefined; - const claudeEffort: ClaudeCodeEffort | undefined = - claudeCandidate?.effort === "low" || - claudeCandidate?.effort === "medium" || - claudeCandidate?.effort === "high" || - claudeCandidate?.effort === "max" || - claudeCandidate?.effort === "ultrathink" - ? claudeCandidate.effort - : undefined; + const claudeEffort = Schema.is(ClaudeAgentEffort)(claudeCandidate?.effort) + ? claudeCandidate.effort + : undefined; const claudeFastMode = claudeCandidate?.fastMode === true ? true diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index eb5e84dc9b0..4fc6efb7ede 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -1,5 +1,5 @@ import { - type ClaudeCodeEffort, + type ClaudeAgentEffort, DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, type ModelSelection, type ProviderKind, @@ -173,7 +173,7 @@ export function getCustomModelOptionsByProvider( export function initialClaudeSelection(providers: ReadonlyArray): { readonly model: string; - readonly effort: ClaudeCodeEffort; + readonly effort: ClaudeAgentEffort; } { const claude = providers.find((p) => p.provider === "claudeAgent"); const { model, effort } = defaultClaudeSelectionForAuth(claude?.auth ?? { status: "unknown" }); diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 25f611284ff..61c6be3929b 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -2,28 +2,28 @@ import { Schema } from "effect"; import { TrimmedNonEmptyString } from "./baseSchemas"; import type { ProviderKind } from "./orchestration"; -export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; -export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; -export const CLAUDE_CODE_EFFORT_OPTIONS = [ +export const CodexReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low"]); +export type CodexReasoningEffort = typeof CodexReasoningEffort.Type; +export const ClaudeAgentEffort = Schema.Literals([ "low", "medium", "high", "xhigh", "max", "ultrathink", -] as const; -export type ClaudeCodeEffort = (typeof CLAUDE_CODE_EFFORT_OPTIONS)[number]; -export type ProviderReasoningEffort = CodexReasoningEffort | ClaudeCodeEffort; +]); +export type ClaudeAgentEffort = typeof ClaudeAgentEffort.Type; +export type ProviderReasoningEffort = CodexReasoningEffort | ClaudeAgentEffort; export const CodexModelOptions = Schema.Struct({ - reasoningEffort: Schema.optional(Schema.Literals(CODEX_REASONING_EFFORT_OPTIONS)), + reasoningEffort: Schema.optional(CodexReasoningEffort), fastMode: Schema.optional(Schema.Boolean), }); export type CodexModelOptions = typeof CodexModelOptions.Type; export const ClaudeModelOptions = Schema.Struct({ thinking: Schema.optional(Schema.Boolean), - effort: Schema.optional(Schema.Literals(CLAUDE_CODE_EFFORT_OPTIONS)), + effort: Schema.optional(ClaudeAgentEffort), fastMode: Schema.optional(Schema.Boolean), contextWindow: Schema.optional(Schema.String), }); @@ -81,10 +81,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record Date: Fri, 17 Apr 2026 02:09:55 +0200 Subject: [PATCH 4/4] test(server): align Sonnet 4.6 default effort assertions with fork Upstream test assumed Sonnet 4.6 defaults to "high"; our fork intentionally defaults to "medium" per Anthropic's recommendations (a6bbf4b7). --- apps/server/src/provider/Layers/ClaudeAdapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index afa0527764e..8f502baab20 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -418,7 +418,7 @@ describe("ClaudeAdapterLive", () => { }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "high"); + assert.equal(createInput?.options.effort, "medium"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), @@ -585,7 +585,7 @@ describe("ClaudeAdapterLive", () => { }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "high"); + assert.equal(createInput?.options.effort, "medium"); const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); }).pipe(