Skip to content
6 changes: 5 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,25 @@ export function getModelParams({
model,
defaultMaxTokens,
defaultTemperature = 0,
defaultReasoningEffort,
}: {
options: ApiHandlerOptions
model: ModelInfo
defaultMaxTokens?: number
defaultTemperature?: number
defaultReasoningEffort?: "low" | "medium" | "high"
}) {
const {
modelMaxTokens: customMaxTokens,
modelMaxThinkingTokens: customMaxThinkingTokens,
modelTemperature: customTemperature,
reasoningEffort: customReasoningEffort,
} = options

let maxTokens = model.maxTokens ?? defaultMaxTokens
let thinking: BetaThinkingConfigParam | undefined = undefined
let temperature = customTemperature ?? defaultTemperature
const reasoningEffort = customReasoningEffort ?? defaultReasoningEffort

if (model.thinking) {
// Only honor `customMaxTokens` for thinking models.
Expand All @@ -118,5 +122,5 @@ export function getModelParams({
temperature = 1.0
}

return { maxTokens, thinking, temperature }
return { maxTokens, thinking, temperature, reasoningEffort }
}
15 changes: 12 additions & 3 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import axios, { AxiosRequestConfig } from "axios"
import axios from "axios"
import OpenAI from "openai"
import delay from "delay"

import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { parseApiPrice } from "../../utils/cost"
Expand All @@ -22,6 +21,12 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
thinking?: BetaThinkingConfigParam
// https://openrouter.ai/docs/use-cases/reasoning-tokens
reasoning?: {
effort?: "high" | "medium" | "low"
max_tokens?: number
exclude?: boolean
}
}

export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler {
Expand All @@ -42,7 +47,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): AsyncGenerator<ApiStreamChunk> {
let { id: modelId, maxTokens, thinking, temperature, topP } = this.getModel()
let { id: modelId, maxTokens, thinking, temperature, topP, reasoningEffort } = this.getModel()

// Convert Anthropic messages to OpenAI format.
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
Expand Down Expand Up @@ -70,13 +75,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
},
],
}

// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)

lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}

if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
Expand Down Expand Up @@ -113,6 +121,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
}),
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
...((this.options.openRouterUseMiddleOutTransform ?? true) && { transforms: ["middle-out"] }),
...(reasoningEffort && { reasoning: { effort: reasoningEffort } }),
}

const stream = await this.client.chat.completions.create(completionParams)
Expand Down
3 changes: 2 additions & 1 deletion src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ type ProviderSettings = {
cachableFields?: string[] | undefined
} | null)
| undefined
modelTemperature?: (number | null) | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
modelTemperature?: (number | null) | undefined
reasoningEffort?: ("low" | "medium" | "high") | undefined
rateLimitSeconds?: number | undefined
fakeAi?: unknown | undefined
}
Expand Down
3 changes: 2 additions & 1 deletion src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,11 @@ type ProviderSettings = {
cachableFields?: string[] | undefined
} | null)
| undefined
modelTemperature?: (number | null) | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
modelTemperature?: (number | null) | undefined
reasoningEffort?: ("low" | "medium" | "high") | undefined
rateLimitSeconds?: number | undefined
fakeAi?: unknown | undefined
}
Expand Down
18 changes: 15 additions & 3 deletions src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ export const telemetrySettingsSchema = z.enum(telemetrySettings)

export type TelemetrySetting = z.infer<typeof telemetrySettingsSchema>

/**
* ReasoningEffort
*/

export const reasoningEfforts = ["low", "medium", "high"] as const

export const reasoningEffortsSchema = z.enum(reasoningEfforts)

export type ReasoningEffort = z.infer<typeof reasoningEffortsSchema>

/**
* ModelInfo
*/
Expand All @@ -110,7 +120,7 @@ export const modelInfoSchema = z.object({
cacheWritesPrice: z.number().optional(),
cacheReadsPrice: z.number().optional(),
description: z.string().optional(),
reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
reasoningEffort: reasoningEffortsSchema.optional(),
thinking: z.boolean().optional(),
minTokensPerCachePoint: z.number().optional(),
maxCachePoints: z.number().optional(),
Expand Down Expand Up @@ -383,11 +393,12 @@ export const providerSettingsSchema = z.object({
requestyModelId: z.string().optional(),
requestyModelInfo: modelInfoSchema.nullish(),
// Claude 3.7 Sonnet Thinking
modelTemperature: z.number().nullish(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
// Generic
includeMaxTokens: z.boolean().optional(),
modelTemperature: z.number().nullish(),
reasoningEffort: reasoningEffortsSchema.optional(),
rateLimitSeconds: z.number().optional(),
// Fake AI
fakeAi: z.unknown().optional(),
Expand Down Expand Up @@ -470,11 +481,12 @@ const providerSettingsRecord: ProviderSettingsRecord = {
requestyModelId: undefined,
requestyModelInfo: undefined,
// Claude 3.7 Sonnet Thinking
modelTemperature: undefined,
modelMaxTokens: undefined,
modelMaxThinkingTokens: undefined,
// Generic
includeMaxTokens: undefined,
modelTemperature: undefined,
reasoningEffort: undefined,
rateLimitSeconds: undefined,
// Fake AI
fakeAi: undefined,
Expand Down
12 changes: 11 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
OPENROUTER_DEFAULT_PROVIDER_NAME,
} from "@/components/ui/hooks/useOpenRouterModelProviders"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator, Button } from "@/components/ui"
import { MODELS_BY_PROVIDER, PROVIDERS, VERTEX_REGIONS } from "./constants"
import { MODELS_BY_PROVIDER, PROVIDERS, VERTEX_REGIONS, REASONING_MODELS } from "./constants"
import { AWS_REGIONS } from "../../../../src/shared/aws_regions"
import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
import { ModelInfoView } from "./ModelInfoView"
Expand All @@ -58,6 +58,7 @@ import { ThinkingBudget } from "./ThinkingBudget"
import { R1FormatSetting } from "./R1FormatSetting"
import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
import { ReasoningEffort } from "./ReasoningEffort"

interface ApiOptionsProps {
uriScheme: string | undefined
Expand Down Expand Up @@ -1519,6 +1520,13 @@ const ApiOptions = ({
</div>
)}

{selectedProvider === "openrouter" && REASONING_MODELS.has(selectedModelId) && (
<ReasoningEffort
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}

{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration}
Expand Down Expand Up @@ -1646,12 +1654,14 @@ const ApiOptions = ({
})()}
</>
)}

<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
/>

<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
Expand Down
41 changes: 41 additions & 0 deletions webview-ui/src/components/settings/ReasoningEffort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useAppTranslation } from "@/i18n/TranslationContext"

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator } from "@/components/ui"

import { ApiConfiguration } from "../../../../src/shared/api"
import { reasoningEfforts } from "../../../../src/schemas"

interface ReasoningEffortProps {
apiConfiguration: ApiConfiguration
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
}

export const ReasoningEffort = ({ apiConfiguration, setApiConfigurationField }: ReasoningEffortProps) => {
const { t } = useAppTranslation()

return (
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<label className="block font-medium mb-1">Model Reasoning Effort</label>
</div>
<Select
value={apiConfiguration.reasoningEffort}
onValueChange={(value) =>
setApiConfigurationField("reasoningEffort", value as "high" | "medium" | "low")
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="openrouter">OpenRouter</SelectItem>
<SelectSeparator />
{reasoningEfforts.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
2 changes: 2 additions & 0 deletions webview-ui/src/components/settings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ export const VERTEX_REGIONS = [
{ value: "europe-west4", label: "europe-west4" },
{ value: "asia-southeast1", label: "asia-southeast1" },
]

export const REASONING_MODELS = new Set(["x-ai/grok-3-mini-beta"])