From 1f9e869144592694bfbb692b0bb2bf109e8670b0 Mon Sep 17 00:00:00 2001 From: MaxHTu <129989478+MaxHTu@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:24:12 +0200 Subject: [PATCH] fix: make Claude 200k context window selection take effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code auto-enables the 1M-token context window for models that support it (claude-opus-5, claude-fable-5, claude-sonnet-5), so passing a bare model slug does not mean 200k — the session silently runs at 1M regardless of the selection. State the selection explicitly through CLAUDE_CODE_DISABLE_1M_CONTEXT ("1" when the catalog resolves the window to 200k tokens, "0" otherwise), so the session runs the window T3 displays regardless of what a user or project settings file sets. The rule lives in the model catalog (resolveClaudeCatalogContextWindowEnv), keyed off the manifest's contextWindowTokens rather than a literal option id, so remote manifest updates keep working. Models without catalog token data are left to the user's configuration. The value goes through the SDK settings option (the flag-settings layer) instead of the spawned process env, because an `env` block in a user or project settings file overrides the process env and would silently defeat the fix. Managed policy settings still outrank it. Because the flag-settings value also outranks an explicit `[1m]` model suffix (verified empirically), sendTurn restates it via applyFlagSettings whenever the resolved value changes, ahead of setModel, and refreshes the session's cached context window on success so streaming usage isn't clamped to the pre-switch window until the turn's result message arrives. A failure applying flag settings is logged rather than failing the turn, so an older CLI that does not know the control request loses only the window, not the turn. ClaudeTextGeneration spawns the same CLI with the same bare slug, so it gets the same treatment. Fixes #8405 Co-Authored-By: Claude Fable 5 --- .../src/provider/ClaudeModelCatalog.test.ts | 60 +++++- .../server/src/provider/ClaudeModelCatalog.ts | 20 ++ .../src/provider/Layers/ClaudeAdapter.test.ts | 203 +++++++++++++++++- .../src/provider/Layers/ClaudeAdapter.ts | 54 +++++ .../ClaudeTextGeneration.test.ts | 34 ++- .../textGeneration/ClaudeTextGeneration.ts | 3 + 6 files changed, 369 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/ClaudeModelCatalog.test.ts b/apps/server/src/provider/ClaudeModelCatalog.test.ts index b370c8e24d3d..71e5c19eb9bf 100644 --- a/apps/server/src/provider/ClaudeModelCatalog.test.ts +++ b/apps/server/src/provider/ClaudeModelCatalog.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest"; import { ProviderInstanceId } from "@t3tools/contracts"; +import { createModelSelection } from "@t3tools/shared/model"; import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts"; import type { ModelManifestData } from "./ModelManifest.ts"; @@ -7,6 +8,7 @@ import { formatClaudeVersionUpgradeMessage, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, + resolveClaudeCatalogContextWindowEnv, resolveClaudeModelCatalog, resolveClaudeModelsForVersion, resolveClaudeModelSlug, @@ -38,7 +40,10 @@ const manifest = (): ModelManifestData => ({ id: "contextWindow", label: "Context Window", type: "select", - options: [{ id: "large", label: "Large", isDefault: true }], + options: [ + { id: "small", label: "Small" }, + { id: "large", label: "Large", isDefault: true }, + ], }, ], }, @@ -46,6 +51,7 @@ const manifest = (): ModelManifestData => ({ claudeCode: { effortMap: { extreme: "high" }, modelSuffixes: { contextWindow: { large: "[large]" } }, + contextWindowTokens: { small: 200_000, large: 1_000_000 }, }, }, }, @@ -114,6 +120,58 @@ describe("Claude model catalog", () => { ); }); + it("states the Claude Code 1M context window opt-out per selection", () => { + const catalog = resolveClaudeModelCatalog(manifest()); + const instanceId = ProviderInstanceId.make("claudeAgent"); + assert.deepStrictEqual( + resolveClaudeCatalogContextWindowEnv( + catalog, + createModelSelection(instanceId, "synthetic", [{ id: "contextWindow", value: "small" }]), + ), + { CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" }, + ); + assert.deepStrictEqual( + resolveClaudeCatalogContextWindowEnv(catalog, { instanceId, model: "synthetic" }), + { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + ); + assert.strictEqual(resolveClaudeCatalogContextWindowEnv(catalog, undefined), undefined); + }); + + it("states the 1M context window opt-out for models with a fixed window", () => { + const base = manifest(); + const claudeAgent = base.providers!.claudeAgent!; + const input: ModelManifestData = { + ...base, + providers: { + ...base.providers, + claudeAgent: { + ...claudeAgent, + profiles: { + ...claudeAgent.profiles, + fixed: { adapter: { claudeCode: { fixedContextWindowTokens: 1_000_000 } } }, + }, + models: [ + ...claudeAgent.models, + { + slug: "claude-synthetic-fixed", + name: "Claude Synthetic Fixed", + status: "current", + profile: "fixed", + }, + ], + }, + }, + }; + const catalog = resolveClaudeModelCatalog(input); + assert.deepStrictEqual( + resolveClaudeCatalogContextWindowEnv(catalog, { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-synthetic-fixed", + }), + { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + ); + }); + it("rejects malformed adapter mappings", () => { const base = manifest(); const malformed: ModelManifestData = { diff --git a/apps/server/src/provider/ClaudeModelCatalog.ts b/apps/server/src/provider/ClaudeModelCatalog.ts index bd554f042f0b..c5fcb2d1ce9d 100644 --- a/apps/server/src/provider/ClaudeModelCatalog.ts +++ b/apps/server/src/provider/ClaudeModelCatalog.ts @@ -240,3 +240,23 @@ export function resolveClaudeCatalogContextWindowTokens( const contextWindow = resolveClaudeCatalogContextWindow(catalog, modelSelection); return contextWindow ? entry.runtime.contextWindowTokens?.[contextWindow] : undefined; } + +/** + * Claude Code auto-enables the 1M-token context window for every model with a + * native 1M window, so a bare model slug does not select 200k — the CLI only + * holds a session at 200k when `CLAUDE_CODE_DISABLE_1M_CONTEXT` is set, and a + * user or project settings file may already set it (live-test finding). State + * the window the catalog resolved in both directions, so the session runs the + * window T3 displays — for a fixed-window model as much as for a picked option. + * Managed policy settings still outrank this; the CLI's own usage report then + * corrects the meter. Models without catalog token data are left to the + * user's configuration. + */ +export function resolveClaudeCatalogContextWindowEnv( + catalog: ClaudeModelCatalog, + modelSelection: ModelSelection | undefined, +): Record | undefined { + const tokens = resolveClaudeCatalogContextWindowTokens(catalog, modelSelection); + if (tokens === undefined) return undefined; + return { CLAUDE_CODE_DISABLE_1M_CONTEXT: tokens <= 200_000 ? "1" : "0" }; +} diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 3b5c0f8586a7..4ec1ba32e4fe 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -63,6 +63,7 @@ class FakeClaudeQuery implements AsyncIterable { private failure: unknown | undefined; public readonly setModelCalls: Array = []; + public readonly applyFlagSettingsCalls: Array> = []; public readonly setPermissionModeCalls: Array = []; public readonly setMaxThinkingTokensCalls: Array = []; public closeCalls = 0; @@ -106,6 +107,10 @@ class FakeClaudeQuery implements AsyncIterable { this.setModelCalls.push(model); }; + readonly applyFlagSettings = async (settings: Record): Promise => { + this.applyFlagSettingsCalls.push(settings); + }; + readonly setPermissionMode = async (mode: PermissionMode): Promise => { this.setPermissionModeCalls.push(mode); }; @@ -555,7 +560,9 @@ describe("ClaudeAdapterLive", () => { }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.settings, undefined); + assert.deepEqual(createInput?.options.settings, { + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), @@ -580,6 +587,7 @@ describe("ClaudeAdapterLive", () => { const createInput = harness.getLastCreateQueryInput(); assert.deepEqual(createInput?.options.settings, { fastMode: true, + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), @@ -603,7 +611,132 @@ describe("ClaudeAdapterLive", () => { }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.settings, undefined); + assert.deepEqual(createInput?.options.settings, { + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + }); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("opts out of the 1M context window when the 200k window is selected", () => { + 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"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [{ id: "contextWindow", value: "standard" }], + ), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.model, SYNTHETIC_CLAUDE_CAPABLE_MODEL); + assert.deepEqual(createInput?.options.settings, { + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" }, + }); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect( + "updates cached usage after a mid-thread context-window switch so streaming usage is not clamped to the old window", + () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + + const runtimeEventsFiber = yield* Stream.takeUntil( + adapter.streamEvents, + (event) => event.type === "thread.token-usage.updated", + ).pipe(Stream.runCollect, Effect.forkChild); + + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + modelSelection: createModelSelection( + ProviderInstanceId.make("claudeAgent"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [{ id: "contextWindow", value: "standard" }], + ), + runtimeMode: "full-access", + }); + yield* adapter.sendTurn({ + threadId: THREAD_ID, + input: "hello", + modelSelection: createModelSelection( + ProviderInstanceId.make("claudeAgent"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [{ id: "contextWindow", value: "expanded" }], + ), + attachments: [], + }); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-window-switch", + uuid: "stream-window-switch", + parent_tool_use_id: null, + event: { + type: "message_delta", + usage: { + input_tokens: 500_000, + output_tokens: 100, + }, + }, + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + const usageEvent = runtimeEvents.find( + (event) => event.type === "thread.token-usage.updated", + ); + assert.equal(usageEvent?.type, "thread.token-usage.updated"); + if (usageEvent?.type === "thread.token-usage.updated") { + // Before the fix this clamped to the session's starting 200k + // window (usedTokens: 200000, maxTokens: 200000) because the + // model switch never refreshed the cache streaming usage reads. + assert.deepEqual(usageEvent.payload.usage, { + usedTokens: 500_100, + lastUsedTokens: 500_100, + inputTokens: 500_000, + outputTokens: 100, + maxTokens: 1_000_000, + }); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }, + ); + + it.effect("opts back into the 1M context window when the expanded window is selected", () => { + 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"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [{ id: "contextWindow", value: "expanded" }], + ), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.model, `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`); + assert.deepEqual(createInput?.options.settings, { + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), @@ -691,16 +824,75 @@ describe("ClaudeAdapterLive", () => { `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`, SYNTHETIC_CLAUDE_COLLIDING_ALIAS, ]); + assert.deepEqual(customHarness.query.applyFlagSettingsCalls, [ + { env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" } }, + { env: null }, + ]); assert.equal(customPrompt, "keep this prompt literal"); const builtInOptions = yield* start(builtInHarness, SYNTHETIC_CLAUDE_CAPABLE_MODEL); assert.equal(builtInOptions.model, `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`); assert.equal(builtInOptions.effort, "max"); - assert.deepEqual(builtInOptions.settings, { fastMode: true }); + assert.deepEqual(builtInOptions.settings, { + fastMode: true, + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" }, + }); }); }, ); + it.effect("leaves the 1M context window alone for models without the option", () => { + 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"), + SYNTHETIC_CLAUDE_THINKING_MODEL, + [], + ), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.settings, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("merges the 1M opt-out with other SDK settings", () => { + 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"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [ + { id: "fastMode", value: true }, + { id: "contextWindow", value: "standard" }, + ], + ), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.deepEqual(createInput?.options.settings, { + fastMode: true, + env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" }, + }); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("treats ultrathink as a prompt keyword instead of a session effort", () => { const harness = makeHarness(); return Effect.gen(function* () { @@ -4659,6 +4851,7 @@ describe("ClaudeAdapterLive", () => { }); assert.deepEqual(harness.query.setModelCalls, []); + assert.deepEqual(harness.query.applyFlagSettingsCalls, []); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), @@ -4702,6 +4895,10 @@ describe("ClaudeAdapterLive", () => { `${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded]`, SYNTHETIC_CLAUDE_CAPABLE_MODEL, ]); + assert.deepEqual(harness.query.applyFlagSettingsCalls, [ + { env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "0" } }, + { env: { CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" } }, + ]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 62763f947c7d..41ae79f6243c 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -13,6 +13,7 @@ import { type PermissionMode, type PermissionResult, type PermissionUpdate, + type Query as ClaudeSdkQuery, type SDKMessage, type SDKResultMessage, type SettingSource, @@ -89,6 +90,7 @@ import { isClaudeCatalogUltracodeEffort, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, + resolveClaudeCatalogContextWindowEnv, resolveClaudeCatalogContextWindowTokens, resolveClaudeCatalogEffort, resolveClaudeModelSlug, @@ -287,6 +289,7 @@ interface ClaudeSessionContext { readonly startedAt: string; readonly basePermissionMode: PermissionMode | undefined; currentApiModelId: string | undefined; + currentContextWindowEnv: Record | undefined; /** Effective effort for the session's turns; subagents without an explicit * effort override inherit this. */ currentEffort: string | undefined; @@ -327,6 +330,7 @@ interface ClaudeSessionContext { interface ClaudeQueryRuntime extends AsyncIterable { readonly setModel: (model?: string) => Promise; + readonly applyFlagSettings: ClaudeSdkQuery["applyFlagSettings"]; readonly setPermissionMode: (mode: PermissionMode) => Promise; readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise; readonly close: () => void; @@ -479,6 +483,15 @@ function maxClaudeContextWindowFromModelUsage( return maxContextWindow; } +function sameContextWindowEnv( + left: Record | undefined, + right: Record | undefined, +): boolean { + if (left === undefined || right === undefined) return left === right; + const keys = Object.keys(left); + return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]); +} + function selectedClaudeContextWindow( catalog: ClaudeModelCatalog, modelSelection: ModelSelection | undefined, @@ -4306,6 +4319,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const apiModelId = modelSelection ? resolveClaudeCatalogApiModelId(modelCatalog, modelSelection) : undefined; + const contextWindowEnv = resolveClaudeCatalogContextWindowEnv(modelCatalog, modelSelection); const initialContextWindow = selectedClaudeContextWindow(modelCatalog, modelSelection); const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort"); const effort = @@ -4341,6 +4355,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ...(claudeSettings.autoCompactWindow ? { autoCompactWindow: Number(claudeSettings.autoCompactWindow) } : {}), + // The context-window selection must be stated to the CLI (see + // `resolveClaudeCatalogContextWindowEnv`). Set it via flag settings + // rather than the process env: a user or project settings file's + // `env` block overrides the spawned process env, while flag settings + // outrank both (live-test finding; managed policy settings still win). + ...(contextWindowEnv ? { env: contextWindowEnv } : {}), }; const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); // The attachments dir grant lets the agent Read/copy pasted images at @@ -4460,6 +4480,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( startedAt, basePermissionMode: permissionMode, currentApiModelId: apiModelId, + currentContextWindowEnv: contextWindowEnv, currentEffort: effectiveEffort ?? undefined, resumeSessionId: sessionId, pendingApprovals, @@ -4580,6 +4601,39 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( if (modelSelection?.model) { const apiModelId = resolveClaudeCatalogApiModelId(modelCatalog, modelSelection); + const contextWindowEnv = resolveClaudeCatalogContextWindowEnv(modelCatalog, modelSelection); + const contextWindowTokens = selectedClaudeContextWindow(modelCatalog, modelSelection); + if (!sameContextWindowEnv(context.currentContextWindowEnv, contextWindowEnv)) { + // The context-window opt-out lives in the flag-settings layer, not in + // the model id, and it outranks an explicit `[1m]` suffix (live-test + // finding) — so it is restated whenever the resolved value changes, + // ahead of the model switch. `null` clears the key, leaving a model + // without catalog token data in the same state as a freshly started + // session. Successive calls replace the whole `env` object, so this + // helper must stay its only writer. A failure is logged instead of + // failing the turn: an older CLI that does not know the control + // request would otherwise lose the turn over a wrong context window, + // and the unchanged tracked value retries on the next turn. + const applied = yield* Effect.tryPromise({ + try: () => context.query.applyFlagSettings({ env: contextWindowEnv ?? null }), + catch: (cause) => toRequestError(input.threadId, "turn/applyFlagSettings", cause), + }).pipe( + Effect.as(true), + Effect.catch((cause) => + Effect.logWarning("Failed to apply Claude context-window flag settings.", { + cause, + }).pipe(Effect.as(false)), + ), + ); + if (applied) { + context.currentContextWindowEnv = contextWindowEnv; + // Streaming usage snapshots read this cache until the turn's + // result message reports the model's real window (completeTurn); + // without updating it here, usage on the new window is clamped to + // the old one for the rest of this turn. + context.lastKnownContextWindow = contextWindowTokens ?? context.lastKnownContextWindow; + } + } if (context.currentApiModelId !== apiModelId) { yield* Effect.tryPromise({ try: () => context.query.setModel(apiModelId), diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index 8dcaa3720295..39298312aadf 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -320,7 +320,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { body: "Body", }, }), - argsMustContain: `--model ${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded] --effort max --settings {"fastMode":true} --dangerously-skip-permissions`, + argsMustContain: `--model ${SYNTHETIC_CLAUDE_CAPABLE_MODEL}[expanded] --effort max --settings {"fastMode":true,"env":{"CLAUDE_CODE_DISABLE_1M_CONTEXT":"0"}} --dangerously-skip-permissions`, claudeConfig: { customModels: [SYNTHETIC_CLAUDE_COLLIDING_ALIAS] }, }, (textGeneration) => @@ -349,6 +349,38 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { ), ); + it.effect("opts out of the 1M context window for 200k selections", () => + withFakeClaudeEnv( + { + output: JSON.stringify({ + structured_output: { + subject: "Add important change", + body: "", + }, + }), + argsMustContain: `--model ${SYNTHETIC_CLAUDE_CAPABLE_MODEL} --effort high --settings {"env":{"CLAUDE_CODE_DISABLE_1M_CONTEXT":"1"}}`, + }, + (textGeneration) => + Effect.gen(function* () { + const generated = yield* textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/claude-effect", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md b/README.md", + modelSelection: { + ...createModelSelection( + ProviderInstanceId.make("claudeAgent"), + SYNTHETIC_CLAUDE_CAPABLE_MODEL, + [{ id: "contextWindow", value: "standard" }], + ), + }, + }); + + expect(generated.subject).toBe("Add important change"); + }), + ), + ); + it.effect("generates thread titles through the Claude provider", () => withFakeClaudeEnv( { diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index e5b13b70f1b7..001d8465d98b 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -43,6 +43,7 @@ import { isClaudeCatalogUltracodeEffort, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, + resolveClaudeCatalogContextWindowEnv, resolveClaudeCatalogEffort, resolveClaudeModelSlug, scopeClaudeModelCatalog, @@ -163,10 +164,12 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu thinkingDescriptor?.type === "boolean" ? thinkingDescriptor.currentValue : undefined; const fastMode = fastModeDescriptor?.type === "boolean" ? fastModeDescriptor.currentValue : undefined; + const contextWindowEnv = resolveClaudeCatalogContextWindowEnv(catalog, resolvedModelSelection); const settings = { ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), ...(ultracode ? { ultracode: true } : {}), + ...(contextWindowEnv ? { env: contextWindowEnv } : {}), }; const settingsJson = Object.keys(settings).length > 0