Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/types/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export const modelInfoSchema = z.object({
// Capability flag to indicate whether the model supports an output verbosity parameter
supportsVerbosity: z.boolean().optional(),
supportsReasoningBudget: z.boolean().optional(),
// Capability flag to indicate whether the model supports adaptive thinking (Claude Sonnet 4.6 / Opus 4.6+)
supportsAdaptiveThinking: z.boolean().optional(),
// Capability flag to indicate whether the model supports "max" effort in adaptive thinking (Opus 4.6 only)
supportsAdaptiveThinkingMaxEffort: z.boolean().optional(),
// Capability flag to indicate whether the model supports simple on/off binary reasoning
supportsReasoningBinary: z.boolean().optional(),
// Capability flag to indicate whether the model supports temperature parameter
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ const baseProviderSettingsSchema = z.object({
reasoningEffort: reasoningEffortSettingSchema.optional(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
// Adaptive thinking (Claude Sonnet 4.6 / Opus 4.6 only)
useAdaptiveThinking: z.boolean().optional(),
adaptiveThinkingEffort: z.enum(["low", "medium", "high", "max"]).optional(),

// Model verbosity.
verbosity: verbosityLevelsSchema.optional(),
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const anthropicModels = {
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
supportsAdaptiveThinking: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
tiers: [
{
Expand Down Expand Up @@ -80,6 +81,8 @@ export const anthropicModels = {
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheReadsPrice: 0.5, // $0.50 per million tokens
supportsReasoningBudget: true,
supportsAdaptiveThinking: true,
supportsAdaptiveThinkingMaxEffort: true, // "max" effort is only available for Opus 4.6
// Tiered pricing for extended context (requires beta flag)
tiers: [
{
Expand Down
2 changes: 2 additions & 0 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
maxTokens,
temperature,
reasoning: thinking,
outputConfig,
} = this.getModel()

// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
Expand Down Expand Up @@ -119,6 +120,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
temperature,
thinking,
...(outputConfig ? { output_config: outputConfig } : {}),
// Setting cache breakpoint for system prompt so new tasks can reuse it.
system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }],
messages: sanitizedMessages.map((message, index) => {
Expand Down
171 changes: 171 additions & 0 deletions src/api/transform/__tests__/reasoning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ModelInfo, ProviderSettings, ReasoningEffortWithMinimal } from "@r
import {
getOpenRouterReasoning,
getAnthropicReasoning,
getAnthropicOutputConfig,
getOpenAiReasoning,
getRooReasoning,
getGeminiReasoning,
Expand Down Expand Up @@ -458,6 +459,176 @@ describe("reasoning.ts", () => {

expect(result).toBeUndefined()
})

it("should return adaptive thinking params when useAdaptiveThinking is true and model supports it", () => {
const modelWithAdaptive: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
}

const settingsWithAdaptive: ProviderSettings = {
useAdaptiveThinking: true,
}

const options = {
...baseOptions,
model: modelWithAdaptive,
settings: settingsWithAdaptive,
}

const result = getAnthropicReasoning(options)

expect(result).toEqual({ type: "adaptive" })
})

it("should return {type: 'adaptive'} regardless of effort (effort goes in output_config)", () => {
const modelWithAdaptive: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
}

const settingsWithEffort: ProviderSettings = {
useAdaptiveThinking: true,
adaptiveThinkingEffort: "high",
}

const options = {
...baseOptions,
model: modelWithAdaptive,
settings: settingsWithEffort,
}

const result = getAnthropicReasoning(options)

// output_config is a SEPARATE top-level param; thinking only contains {type: "adaptive"}
expect(result).toEqual({ type: "adaptive" })
})

it("should not return adaptive thinking when model does not support it", () => {
const settingsWithAdaptive: ProviderSettings = {
useAdaptiveThinking: true,
}

const options = {
...baseOptions,
settings: settingsWithAdaptive,
}

const result = getAnthropicReasoning(options)

// Falls through to manual mode, but base model has no reasoning budget support
expect(result).toBeUndefined()
})

it("should not return adaptive thinking when useAdaptiveThinking is false", () => {
const modelWithAdaptive: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
}

const settingsWithDisabled: ProviderSettings = {
useAdaptiveThinking: false,
}

const options = {
...baseOptions,
model: modelWithAdaptive,
settings: settingsWithDisabled,
}

const result = getAnthropicReasoning(options)

// Falls through to manual mode, but base model has no reasoning budget support
expect(result).toBeUndefined()
})

it("should prioritize adaptive thinking over manual budget when both settings present", () => {
const modelWithBoth: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
supportsReasoningBudget: true,
}

const settingsWithBoth: ProviderSettings = {
useAdaptiveThinking: true,
enableReasoningEffort: true,
adaptiveThinkingEffort: "medium",
}

const options = {
...baseOptions,
model: modelWithBoth,
settings: settingsWithBoth,
}

const result = getAnthropicReasoning(options)

// output_config is separate; thinking only contains {type: "adaptive"}
expect(result).toEqual({ type: "adaptive" })
})
})

describe("getAnthropicOutputConfig", () => {
it("should return undefined when adaptive thinking is not enabled", () => {
const result = getAnthropicOutputConfig({ model: baseModel, settings: {} })
expect(result).toBeUndefined()
})

it("should return undefined when model does not support adaptive thinking", () => {
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "high" }
const result = getAnthropicOutputConfig({ model: baseModel, settings })
expect(result).toBeUndefined()
})

it("should return undefined when no effort is specified", () => {
const modelWithAdaptive: ModelInfo = { ...baseModel, supportsAdaptiveThinking: true }
const settings: ProviderSettings = { useAdaptiveThinking: true }
const result = getAnthropicOutputConfig({ model: modelWithAdaptive, settings })
expect(result).toBeUndefined()
})

it("should return effort when adaptiveThinkingEffort is set", () => {
const modelWithAdaptive: ModelInfo = { ...baseModel, supportsAdaptiveThinking: true }
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "high" }
const result = getAnthropicOutputConfig({ model: modelWithAdaptive, settings })
expect(result).toEqual({ effort: "high" })
})

it("should fallback to high when max is set but model does not support it", () => {
const modelNoMax: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
supportsAdaptiveThinkingMaxEffort: false,
}
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "max" }
const result = getAnthropicOutputConfig({ model: modelNoMax, settings })
expect(result).toEqual({ effort: "high" })
})

it("should return max effort when model supports it", () => {
const modelWithMax: ModelInfo = {
...baseModel,
supportsAdaptiveThinking: true,
supportsAdaptiveThinkingMaxEffort: true,
}
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "max" }
const result = getAnthropicOutputConfig({ model: modelWithMax, settings })
expect(result).toEqual({ effort: "max" })
})

it("should return low effort", () => {
const modelWithAdaptive: ModelInfo = { ...baseModel, supportsAdaptiveThinking: true }
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "low" }
const result = getAnthropicOutputConfig({ model: modelWithAdaptive, settings })
expect(result).toEqual({ effort: "low" })
})

it("should return medium effort", () => {
const modelWithAdaptive: ModelInfo = { ...baseModel, supportsAdaptiveThinking: true }
const settings: ProviderSettings = { useAdaptiveThinking: true, adaptiveThinkingEffort: "medium" }
const result = getAnthropicOutputConfig({ model: modelWithAdaptive, settings })
expect(result).toEqual({ effort: "medium" })
})
})

describe("getOpenAiReasoning", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/api/transform/model-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {

import {
type AnthropicReasoningParams,
type AnthropicOutputConfig,
type OpenAiReasoningParams,
type GeminiReasoningParams,
type OpenRouterReasoningParams,
getAnthropicReasoning,
getAnthropicOutputConfig,
getOpenAiReasoning,
getGeminiReasoning,
getOpenRouterReasoning,
Expand Down Expand Up @@ -48,6 +50,7 @@ type BaseModelParams = {
type AnthropicModelParams = {
format: "anthropic"
reasoning: AnthropicReasoningParams | undefined
outputConfig: AnthropicOutputConfig | undefined
} & BaseModelParams

type OpenAiModelParams = {
Expand Down Expand Up @@ -151,6 +154,7 @@ export function getModelParams({
format,
...params,
reasoning: getAnthropicReasoning({ model, reasoningBudget, reasoningEffort, settings }),
outputConfig: getAnthropicOutputConfig({ model, settings }),
}
} else if (format === "openai") {
// Special case for o1 and o3-mini, which don't support temperature.
Expand Down
39 changes: 37 additions & 2 deletions src/api/transform/reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type RooReasoningParams = {

export type AnthropicReasoningParams = BetaThinkingConfigParam

export type AnthropicOutputConfig = { effort: "low" | "medium" | "high" | "max" }

export type OpenAiReasoningParams = { reasoning_effort: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] }

// Valid Gemini thinking levels for effort-based reasoning
Expand Down Expand Up @@ -108,8 +110,41 @@ export const getAnthropicReasoning = ({
model,
reasoningBudget,
settings,
}: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>
shouldUseReasoningBudget({ model, settings }) ? { type: "enabled", budget_tokens: reasoningBudget! } : undefined
}: GetModelReasoningOptions): AnthropicReasoningParams | undefined => {
// Adaptive thinking: Claude determines dynamically when and how much to use extended thinking.
// Supported on claude-sonnet-4-6 and claude-opus-4-6 only.
if (settings?.useAdaptiveThinking && model.supportsAdaptiveThinking) {
return { type: "adaptive" as any } as any
}

// Manual mode: fixed budget_tokens
return shouldUseReasoningBudget({ model, settings })
? { type: "enabled", budget_tokens: reasoningBudget! }
: undefined
}

/**
* Returns the `output_config` top-level parameter for Anthropic API calls when adaptive thinking
* is enabled with an effort level. This is a SEPARATE top-level parameter from `thinking`.
* See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#adaptive-thinking
*/
export const getAnthropicOutputConfig = ({
model,
settings,
}: Pick<GetModelReasoningOptions, "model" | "settings">): AnthropicOutputConfig | undefined => {
if (!settings?.useAdaptiveThinking || !model.supportsAdaptiveThinking) {
return undefined
}
const effort = settings.adaptiveThinkingEffort
if (!effort) {
return undefined
}
// "max" effort is only supported on claude-opus-4-6; fall back to "high" otherwise
if (effort === "max" && !model.supportsAdaptiveThinkingMaxEffort) {
return { effort: "high" }
}
return { effort }
}

export const getOpenAiReasoning = ({
model,
Expand Down
Loading