Skip to content
Draft
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 packages/agent/src/adapters/claude/claude-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
onProcessSpawned: this.options?.onProcessSpawned,
onProcessExited: this.options?.onProcessExited,
effort,
modelId: earlyModelId,
enrichmentDeps: this.enrichment?.deps,
enrichedReadCache: this.enrichedReadCache,
cloudMode: cloudRun,
Expand Down
9 changes: 9 additions & 0 deletions packages/agent/src/adapters/claude/session/models.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
getEffortOptions,
requiresAdaptiveThinking,
resolveModelPreference,
supports1MContext,
supportsEffort,
Expand Down Expand Up @@ -62,6 +63,14 @@ describe("model capability flags", () => {
it("keeps deprecated Haiku sessions excluded from MCP injection", () => {
expect(supportsMcpInjection("claude-haiku-4-5")).toBe(false);
});

it("flags only models that reject disabled thinking as requiring adaptive thinking", () => {
expect(requiresAdaptiveThinking("claude-fable-5")).toBe(true);
expect(requiresAdaptiveThinking("claude-opus-4-8")).toBe(false);
expect(requiresAdaptiveThinking("claude-opus-4-7")).toBe(false);
expect(requiresAdaptiveThinking("claude-sonnet-4-6")).toBe(false);
expect(requiresAdaptiveThinking("claude-haiku-4-5")).toBe(false);
});
Comment on lines +67 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The team prefers parameterised tests, and this test checks the same requiresAdaptiveThinking function against five different model IDs with expected booleans — a perfect fit for it.each. Without parameterisation, a future maintainer adding a new model would need to know to add a new expect line inside an existing test rather than extend a table, and a single failure obscures which model caused it.

Suggested change
it("flags only models that reject disabled thinking as requiring adaptive thinking", () => {
expect(requiresAdaptiveThinking("claude-fable-5")).toBe(true);
expect(requiresAdaptiveThinking("claude-opus-4-8")).toBe(false);
expect(requiresAdaptiveThinking("claude-opus-4-7")).toBe(false);
expect(requiresAdaptiveThinking("claude-sonnet-4-6")).toBe(false);
expect(requiresAdaptiveThinking("claude-haiku-4-5")).toBe(false);
});
it.each([
["claude-fable-5", true],
["claude-opus-4-8", false],
["claude-opus-4-7", false],
["claude-sonnet-4-6", false],
["claude-haiku-4-5", false],
] as const)(
"requiresAdaptiveThinking(%s) === %s",
(modelId, expected) => {
expect(requiresAdaptiveThinking(modelId)).toBe(expected);
},
);

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent/src/adapters/claude/session/models.test.ts
Line: 67-73

Comment:
The team prefers parameterised tests, and this test checks the same `requiresAdaptiveThinking` function against five different model IDs with expected booleans — a perfect fit for `it.each`. Without parameterisation, a future maintainer adding a new model would need to know to add a new `expect` line inside an existing test rather than extend a table, and a single failure obscures which model caused it.

```suggestion
  it.each([
    ["claude-fable-5", true],
    ["claude-opus-4-8", false],
    ["claude-opus-4-7", false],
    ["claude-sonnet-4-6", false],
    ["claude-haiku-4-5", false],
  ] as const)(
    "requiresAdaptiveThinking(%s) === %s",
    (modelId, expected) => {
      expect(requiresAdaptiveThinking(modelId)).toBe(expected);
    },
  );
```

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

});

describe("getEffortOptions", () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/agent/src/adapters/claude/session/models.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { EffortLevel } from "../types";

export const DEFAULT_MODEL = "opus";

// Effort level applied when a model needs an explicit effort but the user has
// not chosen one — e.g. models requiring adaptive thinking. Mirrors the
// "medium" fallback used for the effort config option in claude-agent.ts.
export const DEFAULT_EFFORT: EffortLevel = "medium";

const GATEWAY_TO_SDK_MODEL: Record<string, string> = {
"claude-opus-4-7": "opus",
"claude-opus-4-8": "opus",
Expand Down Expand Up @@ -48,6 +55,19 @@ export function supportsMcpInjection(modelId: string): boolean {
return !MODELS_TO_EXCLUDE_MCP_TOOLS.has(modelId);
}

// Models that only support adaptive thinking and reject the SDK's default
// `thinking: { type: "disabled" }` request shape. When effort is set the SDK
// emits `thinking: { type: "adaptive" }` + `output_config: { effort }`; when it
// is absent the SDK falls back to `thinking: { type: "disabled" }`, which these
// models reject with `invalid_request_error: "thinking.type.disabled" is not
// supported for this model`. For them we must always send an effort level so
// adaptive thinking is requested even when the user has not picked one.
const MODELS_REQUIRING_ADAPTIVE_THINKING = new Set(["claude-fable-5"]);

export function requiresAdaptiveThinking(modelId: string): boolean {
return MODELS_REQUIRING_ADAPTIVE_THINKING.has(modelId);
}

interface EffortOption {
value: string;
name: string;
Expand Down
38 changes: 38 additions & 0 deletions packages/agent/src/adapters/claude/session/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,42 @@ describe("buildSessionOptions", () => {
expect(headers).toBe(expected);
});
});

describe("effort", () => {
it("forwards an explicit effort level unchanged", () => {
const options = buildSessionOptions({
...makeParams(),
effort: "high",
modelId: "claude-fable-5",
});

expect(options.effort).toBe("high");
});

it("defaults effort for models that require adaptive thinking", () => {
const options = buildSessionOptions({
...makeParams(),
modelId: "claude-fable-5",
});

// Without an effort level the SDK would send `thinking: { type: "disabled" }`,
// which claude-fable-5 rejects. A default effort forces adaptive thinking.
expect(options.effort).toBe("medium");
});

it("leaves effort unset for models that accept disabled thinking", () => {
const options = buildSessionOptions({
...makeParams(),
modelId: "claude-opus-4-8",
});

expect(options.effort).toBeUndefined();
});

it("leaves effort unset when no model id is provided", () => {
const options = buildSessionOptions(makeParams());

expect(options.effort).toBeUndefined();
});
});
});
14 changes: 13 additions & 1 deletion packages/agent/src/adapters/claude/session/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import type { CodeExecutionMode } from "../tools";
import type { EffortLevel } from "../types";
import { APPENDED_INSTRUCTIONS } from "./instructions";
import { loadUserClaudeJsonMcpServers } from "./mcp-config";
import { DEFAULT_MODEL } from "./models";
import {
DEFAULT_EFFORT,
DEFAULT_MODEL,
requiresAdaptiveThinking,
} from "./models";
import type { SettingsManager } from "./settings";

export interface ProcessSpawnedInfo {
Expand Down Expand Up @@ -56,6 +60,9 @@ export interface BuildOptionsParams {
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
onProcessExited?: (pid: number) => void;
effort?: EffortLevel;
/** Resolved gateway model id, used to gate model-specific request shaping
* (e.g. forcing adaptive thinking for models that reject disabled thinking). */
modelId?: string;
enrichmentDeps?: FileEnrichmentDeps;
enrichedReadCache?: EnrichedReadCache;
/** Records PostHog product usage from MCP exec calls (deduped, session-wide). */
Expand Down Expand Up @@ -453,6 +460,11 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {

if (params.effort) {
options.effort = params.effort;
} else if (params.modelId && requiresAdaptiveThinking(params.modelId)) {
// Without an effort level the SDK emits `thinking: { type: "disabled" }`,
// which these models reject outright. Default to adaptive thinking by
// sending an effort level so the SDK requests `thinking: { type: "adaptive" }`.
options.effort = DEFAULT_EFFORT;
}

clearStatsigCache();
Expand Down
Loading