Skip to content

Commit bb8241a

Browse files
committed
Validate context window against model capabilities in resolveApiModelId
resolveApiModelId now accepts optional ModelCapabilities and uses resolveContextWindow to validate context window values before appending the [1m] suffix. This prevents invalid model IDs (e.g. claude-haiku-4-5[1m]) when a stale contextWindow option carries over from a model that supports 1M context to one that doesn't. All three callers (ClaudeAdapter session start, ClaudeAdapter sendTurn, ClaudeTextGeneration) now pass capabilities to resolveApiModelId.
1 parent 83a889f commit bb8241a

File tree

4 files changed

+48
-7
lines changed

4 files changed

+48
-7
lines changed

apps/server/src/git/Layers/ClaudeTextGeneration.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ import {
2727
sanitizePrTitle,
2828
toJsonSchemaObject,
2929
} from "../Utils.ts";
30-
import { normalizeClaudeModelOptions } from "../../provider/Layers/ClaudeProvider.ts";
30+
import {
31+
getClaudeModelCapabilities,
32+
normalizeClaudeModelOptions,
33+
} from "../../provider/Layers/ClaudeProvider.ts";
3134
import { ServerSettingsService } from "../../serverSettings.ts";
3235

3336
const CLAUDE_TIMEOUT_MS = 180_000;
@@ -104,7 +107,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
104107
"--json-schema",
105108
jsonSchemaStr,
106109
"--model",
107-
resolveApiModelId(modelSelection),
110+
resolveApiModelId(modelSelection, getClaudeModelCapabilities(modelSelection.model)),
108111
...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
109112
...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
110113
"--dangerously-skip-permissions",

apps/server/src/provider/Layers/ClaudeAdapter.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2732,9 +2732,9 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
27322732
const claudeBinaryPath = claudeSettings.binaryPath;
27332733
const modelSelection =
27342734
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
2735-
const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined;
2736-
const requestedEffort = trimOrNull(modelSelection?.options?.effort ?? null);
27372735
const caps = getClaudeModelCapabilities(modelSelection?.model);
2736+
const apiModelId = modelSelection ? resolveApiModelId(modelSelection, caps) : undefined;
2737+
const requestedEffort = trimOrNull(modelSelection?.options?.effort ?? null);
27382738
const effort =
27392739
requestedEffort && hasEffortLevel(caps, requestedEffort) ? requestedEffort : null;
27402740
const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode;
@@ -2899,7 +2899,8 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
28992899
}
29002900

29012901
if (modelSelection?.model) {
2902-
const apiModelId = resolveApiModelId(modelSelection);
2902+
const caps = getClaudeModelCapabilities(modelSelection.model);
2903+
const apiModelId = resolveApiModelId(modelSelection, caps);
29032904
yield* Effect.tryPromise({
29042905
try: () => context.query.setModel(apiModelId),
29052906
catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),

packages/shared/src/model.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,39 @@ describe("resolveApiModelId", () => {
179179
).toBe("claude-opus-4-6");
180180
});
181181

182+
it("strips unsupported context window when capabilities are provided", () => {
183+
const haikuCaps: ModelCapabilities = {
184+
reasoningEffortLevels: [],
185+
supportsFastMode: false,
186+
supportsThinkingToggle: true,
187+
contextWindowOptions: [],
188+
promptInjectedEffortLevels: [],
189+
};
190+
expect(
191+
resolveApiModelId(
192+
{
193+
provider: "claudeAgent",
194+
model: "claude-haiku-4-5",
195+
options: { contextWindow: "1m" },
196+
},
197+
haikuCaps,
198+
),
199+
).toBe("claude-haiku-4-5");
200+
});
201+
202+
it("appends suffix when capabilities confirm support", () => {
203+
expect(
204+
resolveApiModelId(
205+
{
206+
provider: "claudeAgent",
207+
model: "claude-opus-4-6",
208+
options: { contextWindow: "1m" },
209+
},
210+
claudeCaps,
211+
),
212+
).toBe("claude-opus-4-6[1m]");
213+
});
214+
182215
it("returns the model as-is for Codex selections", () => {
183216
expect(resolveApiModelId({ provider: "codex", model: "gpt-5.4" })).toBe("gpt-5.4");
184217
});

packages/shared/src/model.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,14 @@ export function trimOrNull<T extends string>(value: T | null | undefined): T | n
130130
return trimmed || null;
131131
}
132132

133-
export function resolveApiModelId(modelSelection: ModelSelection): string {
133+
export function resolveApiModelId(
134+
modelSelection: ModelSelection,
135+
capabilities?: ModelCapabilities,
136+
): string {
134137
switch (modelSelection.provider) {
135138
case "claudeAgent": {
136-
const contextWindow = modelSelection.options?.contextWindow;
139+
const raw = modelSelection.options?.contextWindow;
140+
const contextWindow = capabilities ? resolveContextWindow(capabilities, raw) : raw;
137141
switch (contextWindow) {
138142
case "1m":
139143
return `${modelSelection.model}[1m]`;

0 commit comments

Comments
 (0)