Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a05830c
feat(experiments): add dynamic thinking effort experimental setting
easonliang28 Aug 21, 2026
1cf4f0d
test(experiments): cover explicit false and omitted dynamic thinking …
easonliang28 Aug 21, 2026
6ea45b3
feat(task): task-local thinking effort state, per-request override, a…
easonliang28 Aug 21, 2026
5db5cf4
Merge remote-tracking branch 'upstream/main' into feat/dte-1-experiment
easonliang28 Aug 21, 2026
9275aa1
Merge remote-tracking branch 'upstream/main' into feat/dte-2-task-state
easonliang28 Aug 22, 2026
14d1f35
fix(task): keep override restore value current across profile switches
easonliang28 Aug 22, 2026
90b47b0
docs(task): JSDoc for diff-touched functions flagged by CodeRabbit
easonliang28 Aug 22, 2026
d64a473
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 Aug 22, 2026
2d53e91
Merge remote-tracking branch 'origin/feat/dte-1-experiment' into feat…
easonliang28 Aug 22, 2026
fcc3cf4
feat(task): set_thinking_effort native tool
easonliang28 Aug 23, 2026
0ab4a60
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 Aug 23, 2026
19954d3
fix(task): harden set_thinking_effort per review feedback
easonliang28 Aug 23, 2026
396a9b1
fix(tool): reject capability arrays with no settable effort
easonliang28 Aug 24, 2026
e502417
test(webview): type thinking-effort test helpers
easonliang28 Aug 24, 2026
27dea9b
feat(i18n): translate dynamic thinking effort setting (11 locales)
easonliang28 Aug 24, 2026
1396736
feat(i18n): translate dynamic thinking effort setting (5 more locales)
easonliang28 Aug 24, 2026
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
19 changes: 19 additions & 0 deletions packages/types/src/__tests__/experiment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { experimentIds, experimentIdsSchema, experimentsSchema } from "../experiment.js"

describe("dynamicThinkingEffort experiment", () => {
it("is part of the experiment id enum", () => {
expect(experimentIds).toContain("dynamicThinkingEffort")
expect(experimentIdsSchema.safeParse("dynamicThinkingEffort").success).toBe(true)
})

it("parses enabled and disabled states", () => {
expect(experimentsSchema.parse({ dynamicThinkingEffort: true })).toEqual({ dynamicThinkingEffort: true })
expect(experimentsSchema.parse({ dynamicThinkingEffort: false })).toEqual({ dynamicThinkingEffort: false })
expect(experimentsSchema.parse({})).toEqual({})
})

it("rejects non-boolean values", () => {
expect(experimentsSchema.safeParse({ dynamicThinkingEffort: "yes" }).success).toBe(false)
expect(experimentIdsSchema.safeParse("dynamic-thinking-effort").success).toBe(false)
})
})
2 changes: 2 additions & 0 deletions packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const experimentIds = [
"runSlashCommand",
"customTools",
"parallelToolExecution",
"dynamicThinkingEffort",
] as const

export const experimentIdsSchema = z.enum(experimentIds)
Expand All @@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
runSlashCommand: z.boolean().optional(),
customTools: z.boolean().optional(),
parallelToolExecution: z.boolean().optional(),
dynamicThinkingEffort: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const toolNames = [
"run_slash_command",
"skill",
"generate_image",
"set_thinking_effort",
"custom_tool",
"invalid_tool_call",
] as const
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ export interface ClineSayTool {
| "runSlashCommand"
| "updateTodoList"
| "skill"
| "thinkingEffort"
path?: string
// For readCommandOutput
readStart?: number
Expand Down Expand Up @@ -904,6 +905,9 @@ export interface ClineSayTool {
description?: string
// Properties for skill tool
skill?: string
// Properties for thinkingEffort (DTE series 3/5)
effort?: string
refusal?: string
}

export interface ClineAskUseMcpServer {
Expand Down
9 changes: 9 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
retiredProviderIdentifiers,
type ProviderSettings,
type ModelInfo,
type ReasoningEffortExtended,
} from "@roo-code/types"

import { getRouterRemovalMessage } from "../core/config/routerRemoval"
Expand Down Expand Up @@ -115,6 +116,14 @@ export interface ApiHandlerCreateMessageMetadata {
* when the user clicks stop, preventing wasted API tokens/compute on the provider side.
*/
abortSignal?: AbortSignal
/**
* Per-request thinking effort override (DTE series 2/5).
* When defined, takes precedence over the settings-derived `reasoningEffort`
* wherever the effective effort is resolved (see `resolveEffectiveReasoningEffort`).
* Task-scoped and transient: it applies to this request only (the next request
* after being set — no mid-stream effect) and is never persisted to settings.
*/
reasoningEffort?: ReasoningEffortExtended
}

export interface ApiHandler {
Expand Down
297 changes: 297 additions & 0 deletions src/api/providers/__tests__/anthropic-adaptive-effort.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// npx vitest run src/api/providers/__tests__/anthropic-adaptive-effort.spec.ts
//
// DTE series 2/5 — per-request adaptive thinking effort envelope
// (output_config.effort) on the main Anthropic handler.
//
// Kept in a dedicated file (rather than anthropic.spec.ts) so the DTE series PRs
// stay mergeable while other series PRs extend the shared spec file.

import { AnthropicHandler } from "../anthropic"
import type { ApiHandlerOptions } from "../../../shared/api"
import type { ReasoningEffortExtended } from "@roo-code/types"
import { asyncStreamFrom, collectStream } from "../../../test-utils/stream"
import { clearAllMocks } from "../../../test-utils/reset"
import type { ApiHandlerCreateMessageMetadata } from "../../../api"

// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: vitest.fn(),
},
},
}))

const mockCreate = vitest.fn()

// Same SDK mock pattern as anthropic.spec.ts: createMessage resolves to a short
// finite stream so the handler's for-await loop terminates cleanly.
vitest.mock("@anthropic-ai/sdk", () => {
const mockAnthropicConstructor = vitest.fn().mockImplementation(function () {
return {
messages: {
create: mockCreate.mockImplementation(async (options: { stream?: boolean; model?: string }) => {
if (!options.stream) {
return {
id: "test-completion",
content: [{ type: "text", text: "Test response" }],
role: "assistant",
model: options.model,
usage: { input_tokens: 10, output_tokens: 5 },
}
}
return asyncStreamFrom([
{
type: "message_start",
message: {
usage: {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
},
},
},
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "Hello" },
},
{
type: "content_block_delta",
delta: { type: "text_delta", text: " world" },
},
])
}),
},
}
})

return {
Anthropic: mockAnthropicConstructor,
}
})

const userMessage = {
role: "user" as const,
content: [{ type: "text" as const, text: "Hi" }],
}

/** Runs createMessage to completion and returns the request params sent to the SDK. */
async function sentRequestParams(
handler: AnthropicHandler,
metadata?: ApiHandlerCreateMessageMetadata,
): Promise<Record<string, unknown>> {
const stream = handler.createMessage("system prompt", [userMessage], metadata)
await collectStream(stream)
const call = mockCreate.mock.calls.at(-1)
if (!call) {
throw new Error("Expected the SDK messages.create to have been called")
}
return call[0] as Record<string, unknown>
}

function makeHandler(options: {
apiModelId?: string
enableReasoningEffort?: boolean
reasoningEffort?: ApiHandlerOptions["reasoningEffort"]
}): AnthropicHandler {
return new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: options.apiModelId ?? "claude-opus-4-7",
enableReasoningEffort: options.enableReasoningEffort,
reasoningEffort: options.reasoningEffort,
})
}

describe("AnthropicHandler adaptive effort envelope (DTE series 2/5)", () => {
beforeEach(() => {
clearAllMocks()
})

describe("output_config.effort on adaptive-thinking requests", () => {
const inRangeEfforts: ReasoningEffortExtended[] = ["low", "medium", "high", "xhigh", "max"]

it.each(inRangeEfforts)(
"sends the settings effort %s as output_config.effort for an adaptive model",
async (effort) => {
const handler = makeHandler({ enableReasoningEffort: true, reasoningEffort: effort })

const params = await sentRequestParams(handler)

expect(params.thinking).toEqual({ type: "adaptive" })
expect(params.output_config).toEqual({ effort })
},
)

it("sends the envelope from the first (cache-control) requestParams branch", async () => {
// claude-opus-4-8 takes the first (cache-control) requestParams branch;
// the default branch is covered below via an unknown model id.
const handler = makeHandler({
apiModelId: "claude-opus-4-8",
enableReasoningEffort: true,
reasoningEffort: "xhigh",
})

const params = await sentRequestParams(handler)

expect(params.thinking).toEqual({ type: "adaptive" })
expect(params.output_config).toEqual({ effort: "xhigh" })
})

it("sends the envelope from the default requestParams branch", async () => {
// Unknown model id -> falls through to the default switch branch, while the
// guessed model info (claude-opus-4-7 substring) is adaptive-capable.
const handler = makeHandler({
apiModelId: "claude-opus-4-7-custom",
enableReasoningEffort: true,
reasoningEffort: "high",
})

const params = await sentRequestParams(handler)

expect(params.model).toBe("claude-opus-4-7-custom")
expect(params.thinking).toEqual({ type: "adaptive" })
expect(params.output_config).toEqual({ effort: "high" })
})
})

describe("envelope omission (out-of-range or non-adaptive)", () => {
const settingsEfforts: ApiHandlerOptions["reasoningEffort"][] = ["none", "minimal", "disable"]

it.each(settingsEfforts)(
"omits output_config when the settings effort is %s on an adaptive model",
async (effort) => {
const handler = makeHandler({ enableReasoningEffort: true, reasoningEffort: effort })

const params = await sentRequestParams(handler)

// Adaptive thinking is still requested, but no envelope is sent so the
// API applies its own default effort.
expect(params.thinking).toEqual({ type: "adaptive" })
expect(params).not.toHaveProperty("output_config")
},
)

it("omits output_config when no effort is set anywhere on an adaptive model", async () => {
const handler = makeHandler({ enableReasoningEffort: true })

const params = await sentRequestParams(handler)

expect(params.thinking).toEqual({ type: "adaptive" })
expect(params).not.toHaveProperty("output_config")
})

it("omits output_config for a non-adaptive model even with an in-range effort", async () => {
// Budget-based extended thinking (type: "enabled") never carries the
// adaptive envelope.
const handler = makeHandler({
apiModelId: "claude-sonnet-4-5",
enableReasoningEffort: true,
reasoningEffort: "xhigh",
})

const params = await sentRequestParams(handler)

expect(params.thinking).toMatchObject({ type: "enabled" })
expect(params).not.toHaveProperty("output_config")
})

it("omits output_config when adaptive thinking itself is not requested", async () => {
// enableReasoningEffort=false -> thinking is undefined -> no envelope even
// with an in-range settings effort.
const handler = makeHandler({ enableReasoningEffort: false, reasoningEffort: "xhigh" })

const params = await sentRequestParams(handler)

expect(params.thinking).toBeUndefined()
expect(params).not.toHaveProperty("output_config")
})

it("keeps the pre-DTE request shape for a plain model with no reasoning settings", async () => {
// Guard: no reasoning settings and no metadata -> no output_config.
const handler = makeHandler({ apiModelId: "claude-3-5-haiku-20241022" })

const params = await sentRequestParams(handler)

expect(params.thinking).toBeUndefined()
expect(params).not.toHaveProperty("output_config")
})
})

describe("per-request override (metadata.reasoningEffort) precedence", () => {
const baseOptions: {
apiModelId?: string
enableReasoningEffort?: boolean
reasoningEffort?: ApiHandlerOptions["reasoningEffort"]
} = {
apiModelId: "claude-opus-4-7",
enableReasoningEffort: true,
}

it("lets metadata.reasoningEffort override the settings value", async () => {
const handler = makeHandler({ ...baseOptions, reasoningEffort: "low" })

const params = await sentRequestParams(handler, {
taskId: "task-1",
reasoningEffort: "xhigh",
})

expect(params.output_config).toEqual({ effort: "xhigh" })
})

it("suppresses the envelope when the metadata override is out-of-range", async () => {
// Settings would send "high"; the override wins and is out-of-range, so
// the envelope is omitted entirely.
const handler = makeHandler({ ...baseOptions, reasoningEffort: "high" })

const params = await sentRequestParams(handler, {
taskId: "task-1",
reasoningEffort: "minimal",
})

expect(params.thinking).toEqual({ type: "adaptive" })
expect(params).not.toHaveProperty("output_config")
})

const overrideEfforts: ReasoningEffortExtended[] = ["none", "minimal"]

it.each(overrideEfforts)(
"suppresses the envelope for metadata override %s even with an in-range settings value",
async (effort) => {
const handler = makeHandler({ ...baseOptions, reasoningEffort: "max" })

const params = await sentRequestParams(handler, {
taskId: "task-1",
reasoningEffort: effort,
})

expect(params).not.toHaveProperty("output_config")
},
)

it("applies the settings value when metadata carries no override", async () => {
const handler = makeHandler({ ...baseOptions, reasoningEffort: "medium" })

const params = await sentRequestParams(handler, { taskId: "task-1" })

expect(params.output_config).toEqual({ effort: "medium" })
})

it("keeps non-adaptive requests envelope-free even with a metadata override", async () => {
const handler = makeHandler({
apiModelId: "claude-sonnet-4-5",
enableReasoningEffort: true,
reasoningEffort: "low",
})

const params = await sentRequestParams(handler, {
taskId: "task-1",
reasoningEffort: "xhigh",
})

expect(params.thinking).toMatchObject({ type: "enabled" })
expect(params).not.toHaveProperty("output_config")
})
})
})
Loading
Loading