Add GPT-5.1 models and clean up reasoning effort logic (#9252)

* Reasoning effort: capability-driven; add disable/none/minimal; remove GPT-5 minimal special-casing; document UI semantics; remove temporary logs

* Remove Unused supportsReasoningNone

* Roo reasoning: omit field on 'disable'; UI: do not flip enableReasoningEffort when selecting 'disable'

* Update packages/types/src/model.ts

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>

* Update webview-ui/src/components/settings/SimpleThinkingBudget.tsx

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>

---------

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
This commit is contained in:
Hannes Rudolph 2025-11-13 18:54:41 -07:00 committed by GitHub
parent c49f9ab846
commit 01cb12f167
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 522 additions and 246 deletions

View file

@ -18,6 +18,22 @@ export const reasoningEffortWithMinimalSchema = z.union([reasoningEffortsSchema,
export type ReasoningEffortWithMinimal = z.infer<typeof reasoningEffortWithMinimalSchema>
/**
* Extended Reasoning Effort (includes "none" and "minimal")
* Note: "disable" is a UI/control value, not a value sent as effort
*/
export const reasoningEffortsExtended = ["none", "minimal", "low", "medium", "high"] as const
export const reasoningEffortExtendedSchema = z.enum(reasoningEffortsExtended)
export type ReasoningEffortExtended = z.infer<typeof reasoningEffortExtendedSchema>
/**
* Reasoning Effort user setting (includes "disable")
*/
export const reasoningEffortSettingValues = ["disable", "none", "minimal", "low", "medium", "high"] as const
export const reasoningEffortSettingSchema = z.enum(reasoningEffortSettingValues)
/**
* Verbosity
*/
@ -67,7 +83,9 @@ export const modelInfoSchema = z.object({
supportsTemperature: z.boolean().optional(),
defaultTemperature: z.number().optional(),
requiredReasoningBudget: z.boolean().optional(),
supportsReasoningEffort: z.boolean().optional(),
supportsReasoningEffort: z
.union([z.boolean(), z.array(z.enum(["disable", "none", "minimal", "low", "medium", "high"]))])
.optional(),
requiredReasoningEffort: z.boolean().optional(),
preserveReasoning: z.boolean().optional(),
supportedParameters: z.array(modelParametersSchema).optional(),
@ -76,7 +94,8 @@ export const modelInfoSchema = z.object({
cacheWritesPrice: z.number().optional(),
cacheReadsPrice: z.number().optional(),
description: z.string().optional(),
reasoningEffort: reasoningEffortsSchema.optional(),
// Default effort value for models that support reasoning effort
reasoningEffort: reasoningEffortExtendedSchema.optional(),
minTokensPerCachePoint: z.number().optional(),
maxCachePoints: z.number().optional(),
cachableFields: z.array(z.string()).optional(),

View file

@ -1,6 +1,6 @@
import { z } from "zod"
import { modelInfoSchema, reasoningEffortWithMinimalSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js"
import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js"
import { codebaseIndexProviderSchema } from "./codebase-index.js"
import {
anthropicModels,
@ -176,7 +176,7 @@ const baseProviderSettingsSchema = z.object({
// Model reasoning.
enableReasoningEffort: z.boolean().optional(),
reasoningEffort: reasoningEffortWithMinimalSchema.optional(),
reasoningEffort: reasoningEffortSettingSchema.optional(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),

View file

@ -3,85 +3,128 @@ import type { ModelInfo } from "../model.js"
// https://openai.com/api/pricing/
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07"
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.1"
export const openAiNativeModels = {
"gpt-5-chat-latest": {
"gpt-5.1": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: false,
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.13,
description: "GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks",
},
"gpt-5-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: true,
supportsReasoningEffort: ["none", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.13,
description: "GPT-5: The best model for coding and agentic tasks across domains",
// supportsVerbosity is a new capability; ensure ModelInfo includes it
cacheReadsPrice: 0.125,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
],
description: "GPT-5.1: The best model for coding and agentic tasks across domains",
},
"gpt-5-mini-2025-08-07": {
"gpt-5.1-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.125,
supportsTemperature: false,
tiers: [{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 }],
description: "GPT-5.1 Codex: A version of GPT-5.1 optimized for agentic coding in Codex",
},
"gpt-5.1-codex-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 0.25,
outputPrice: 2.0,
cacheReadsPrice: 0.03,
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
cacheReadsPrice: 0.025,
supportsTemperature: false,
description: "GPT-5.1 Codex mini: A version of GPT-5.1 optimized for agentic coding in Codex",
},
"gpt-5": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.125,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
],
description: "GPT-5: The best model for coding and agentic tasks across domains",
},
"gpt-5-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 0.25,
outputPrice: 2.0,
cacheReadsPrice: 0.025,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [
{ name: "flex", contextWindow: 400000, inputPrice: 0.125, outputPrice: 1.0, cacheReadsPrice: 0.0125 },
{ name: "priority", contextWindow: 400000, inputPrice: 0.45, outputPrice: 3.6, cacheReadsPrice: 0.045 },
],
},
"gpt-5-nano-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: true,
reasoningEffort: "medium",
inputPrice: 0.05,
outputPrice: 0.4,
cacheReadsPrice: 0.01,
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
supportsVerbosity: true,
supportsTemperature: false,
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
},
"gpt-5-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.13,
cacheReadsPrice: 0.125,
supportsTemperature: false,
tiers: [{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 }],
description: "GPT-5-Codex: A version of GPT-5 optimized for agentic coding in Codex",
},
"gpt-5-nano": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 0.05,
outputPrice: 0.4,
cacheReadsPrice: 0.005,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
},
"gpt-5-chat-latest": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.125,
description: "GPT-5 Chat: Optimized for conversational AI and non-reasoning tasks",
},
"gpt-4.1": {
maxTokens: 32_768,
@ -130,7 +173,7 @@ export const openAiNativeModels = {
inputPrice: 2.0,
outputPrice: 8.0,
cacheReadsPrice: 0.5,
supportsReasoningEffort: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
supportsTemperature: false,
tiers: [
@ -168,7 +211,7 @@ export const openAiNativeModels = {
inputPrice: 1.1,
outputPrice: 4.4,
cacheReadsPrice: 0.275,
supportsReasoningEffort: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
supportsTemperature: false,
tiers: [
@ -206,7 +249,7 @@ export const openAiNativeModels = {
inputPrice: 1.1,
outputPrice: 4.4,
cacheReadsPrice: 0.55,
supportsReasoningEffort: true,
supportsReasoningEffort: ["low", "medium", "high"],
reasoningEffort: "medium",
supportsTemperature: false,
},
@ -295,11 +338,63 @@ export const openAiNativeModels = {
supportsPromptCache: false,
inputPrice: 1.5,
outputPrice: 6,
cacheReadsPrice: 0,
cacheReadsPrice: 0.375,
supportsTemperature: false,
description:
"Codex Mini: Cloud-based software engineering agent powered by codex-1, a version of o3 optimized for coding tasks. Trained with reinforcement learning to generate human-style code, adhere to instructions, and iteratively run tests.",
},
// Dated clones (snapshots) preserved for backward compatibility
"gpt-5-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.125,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
],
description: "GPT-5: The best model for coding and agentic tasks across domains",
},
"gpt-5-mini-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 0.25,
outputPrice: 2.0,
cacheReadsPrice: 0.025,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [
{ name: "flex", contextWindow: 400000, inputPrice: 0.125, outputPrice: 1.0, cacheReadsPrice: 0.0125 },
{ name: "priority", contextWindow: 400000, inputPrice: 0.45, outputPrice: 3.6, cacheReadsPrice: 0.045 },
],
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
},
"gpt-5-nano-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
inputPrice: 0.05,
outputPrice: 0.4,
cacheReadsPrice: 0.005,
supportsVerbosity: true,
supportsTemperature: false,
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
},
} as const satisfies Record<string, ModelInfo>
export const openAiModelInfoSaneDefaults: ModelInfo = {
@ -316,6 +411,5 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
export const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0
export const GPT5_DEFAULT_TEMPERATURE = 1.0
export const OPENAI_AZURE_AI_INFERENCE_PATH = "/models/chat/completions"

View file

@ -202,7 +202,7 @@ describe("OpenAiNativeHandler", () => {
openAiNativeApiKey: "test-api-key",
})
const modelInfo = handlerWithoutModel.getModel()
expect(modelInfo.id).toBe("gpt-5-2025-08-07") // Default model
expect(modelInfo.id).toBe("gpt-5.1") // Default model
expect(modelInfo.info).toBeDefined()
})
})
@ -247,7 +247,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = handler.createMessage(systemPrompt, messages)
@ -271,7 +271,7 @@ describe("OpenAiNativeHandler", () => {
)
const body1 = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(body1)
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
expect(parsedBody.model).toBe("gpt-5.1")
expect(parsedBody.instructions).toBe("You are a helpful assistant.")
// Now using structured format with content arrays (no system prompt in input; it's provided via `instructions`)
expect(parsedBody.input).toEqual([
@ -399,7 +399,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
verbosity: "low", // Set verbosity through options
})
@ -442,7 +442,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
reasoningEffort: "minimal" as any, // GPT-5 supports minimal
})
@ -461,6 +461,44 @@ describe("OpenAiNativeHandler", () => {
)
})
it("should omit reasoning when selection is 'disable'", async () => {
// Mock fetch for Responses API
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.output_item.added","item":{"type":"text","text":"No reasoning"}}\n\n',
),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
global.fetch = mockFetch as any
// Mock SDK to fail
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5.1",
reasoningEffort: "disable" as any,
})
const stream = handler.createMessage(systemPrompt, messages)
for await (const _ of stream) {
// drain
}
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
const parsed = JSON.parse(bodyStr)
expect(parsed.reasoning).toBeUndefined()
expect(parsed.include).toBeUndefined()
})
it("should support low reasoning effort for GPT-5", async () => {
// Mock fetch for Responses API
const mockFetch = vitest.fn().mockResolvedValue({
@ -484,7 +522,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
reasoningEffort: "low",
})
@ -503,7 +541,7 @@ describe("OpenAiNativeHandler", () => {
)
const body2 = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(body2)
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
expect(parsedBody.model).toBe("gpt-5.1")
expect(parsedBody.reasoning?.effort).toBe("low")
expect(parsedBody.reasoning?.summary).toBe("auto")
expect(parsedBody.text?.verbosity).toBe("medium")
@ -535,7 +573,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
verbosity: "high",
reasoningEffort: "minimal" as any,
})
@ -555,7 +593,7 @@ describe("OpenAiNativeHandler", () => {
)
const body3 = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(body3)
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
expect(parsedBody.model).toBe("gpt-5.1")
expect(parsedBody.reasoning?.effort).toBe("minimal")
expect(parsedBody.reasoning?.summary).toBe("auto")
expect(parsedBody.text?.verbosity).toBe("high")
@ -613,7 +651,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = handler.createMessage(systemPrompt, messages)
@ -669,7 +707,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = handler.createMessage(systemPrompt, messages)
@ -714,7 +752,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = handler.createMessage(systemPrompt, messages)
@ -755,7 +793,7 @@ describe("OpenAiNativeHandler", () => {
const gpt5Handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = gpt5Handler.createMessage(systemPrompt, messages, {
@ -800,7 +838,7 @@ describe("OpenAiNativeHandler", () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
})
const stream = handler.createMessage(systemPrompt, messages)
@ -850,7 +888,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
openAiNativeApiKey: "test-api-key",
})
@ -893,7 +931,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
openAiNativeApiKey: "test-api-key",
})
@ -942,7 +980,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
apiModelId: "gpt-5.1",
openAiNativeApiKey: "test-api-key",
})

View file

@ -511,16 +511,17 @@ describe("RooHandler", () => {
// Consume stream
}
expect(mockCreate).toHaveBeenCalledWith(
const firstCallBody = mockCreate.mock.calls[0][0]
expect(firstCallBody).toEqual(
expect.objectContaining({
model: mockOptions.apiModelId,
messages: expect.any(Array),
stream: true,
stream_options: { include_usage: true },
reasoning: { enabled: false },
}),
undefined,
)
expect(firstCallBody.reasoning).toBeUndefined()
expect(mockCreate.mock.calls[0][1]).toBeUndefined()
})
it("should include reasoning with enabled: false when explicitly disabled", async () => {
@ -595,7 +596,7 @@ describe("RooHandler", () => {
)
})
it("should not include reasoning for minimal (treated as none)", async () => {
it("should include reasoning for minimal", async () => {
handler = new RooHandler({
...mockOptions,
reasoningEffort: "minimal",
@ -605,9 +606,8 @@ describe("RooHandler", () => {
// Consume stream
}
// minimal should result in no reasoning parameter
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.reasoning).toBeUndefined()
expect(callArgs.reasoning).toEqual({ enabled: true, effort: "minimal" })
})
it("should handle enableReasoningEffort: false overriding reasoningEffort setting", async () => {

View file

@ -7,10 +7,9 @@ import {
OpenAiNativeModelId,
openAiNativeModels,
OPENAI_NATIVE_DEFAULT_TEMPERATURE,
GPT5_DEFAULT_TEMPERATURE,
type ReasoningEffort,
type VerbosityLevel,
type ReasoningEffortWithMinimal,
type ReasoningEffortExtended,
type ServiceTier,
} from "@roo-code/types"
@ -26,11 +25,6 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ".
export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
// GPT-5 specific types
// Constants for model identification
const GPT5_MODEL_PREFIX = "gpt-5"
export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: OpenAI
@ -170,9 +164,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
metadata,
)
// Temporary debug logging
// console.log("[OpenAI Native] Request body:", requestBody)
// Make the request (pass systemPrompt and messages for potential retry)
yield* this.executeRequest(requestBody, model, metadata, systemPrompt, messages)
}
@ -182,7 +173,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
formattedInput: any,
systemPrompt: string,
verbosity: any,
reasoningEffort: ReasoningEffortWithMinimal | undefined,
reasoningEffort: ReasoningEffortExtended | undefined,
metadata?: ApiHandlerCreateMessageMetadata,
): any {
// Build a request body
@ -192,7 +183,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
model: string
input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }>
stream: boolean
reasoning?: { effort?: ReasoningEffortWithMinimal; summary?: "auto" }
reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" }
text?: { verbosity: VerbosityLevel }
temperature?: number
max_output_tokens?: number
@ -228,11 +219,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
: {}),
// Only include temperature if the model supports it
...(model.info.supportsTemperature !== false && {
temperature:
this.options.modelTemperature ??
(model.id.startsWith(GPT5_MODEL_PREFIX)
? GPT5_DEFAULT_TEMPERATURE
: OPENAI_NATIVE_DEFAULT_TEMPERATURE),
temperature: this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE,
}),
// Explicitly include the calculated max output tokens.
// Use the per-request reserved output computed by Roo (params.maxTokens from getModelParams).
@ -984,20 +971,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
}
}
private getReasoningEffort(model: OpenAiNativeModel): ReasoningEffortWithMinimal | undefined {
const { reasoning, info } = model
// Check if reasoning effort is configured
if (reasoning && "reasoning_effort" in reasoning) {
const effort = reasoning.reasoning_effort as string
// Support all effort levels
if (effort === "minimal" || effort === "low" || effort === "medium" || effort === "high") {
return effort as ReasoningEffortWithMinimal
}
}
// Use the model's default from types if available
return info.reasoningEffort as ReasoningEffortWithMinimal | undefined
private getReasoningEffort(model: OpenAiNativeModel): ReasoningEffortExtended | undefined {
// Single source of truth: user setting overrides, else model default (from types).
const selected = (this.options.reasoningEffort as any) ?? (model.info.reasoningEffort as any)
return selected && selected !== "disable" ? (selected as any) : undefined
}
/**
@ -1035,19 +1012,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
modelId: id,
model: info,
settings: this.options,
defaultTemperature: id.startsWith(GPT5_MODEL_PREFIX)
? GPT5_DEFAULT_TEMPERATURE
: OPENAI_NATIVE_DEFAULT_TEMPERATURE,
defaultTemperature: OPENAI_NATIVE_DEFAULT_TEMPERATURE,
})
// For models using the Responses API, ensure we support reasoning effort
const effort =
(this.options.reasoningEffort as ReasoningEffortWithMinimal | undefined) ??
(info.reasoningEffort as ReasoningEffortWithMinimal | undefined)
if (effort) {
;(params.reasoning as any) = { reasoning_effort: effort }
}
// Reasoning effort inclusion is handled by getModelParams/getOpenAiReasoning.
// Do not re-compute or filter efforts here.
// The o3 models are named like "o3-mini-[reasoning-effort]", which are
// not valid model ids, so we need to strip the suffix.
@ -1120,11 +1089,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
// Only include temperature if the model supports it
if (model.info.supportsTemperature !== false) {
requestBody.temperature =
this.options.modelTemperature ??
(model.id.startsWith(GPT5_MODEL_PREFIX)
? GPT5_DEFAULT_TEMPERATURE
: OPENAI_NATIVE_DEFAULT_TEMPERATURE)
requestBody.temperature = this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE
}
// Include max_output_tokens if available

View file

@ -28,6 +28,16 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
total_cost?: number
}
type RequestyChatCompletionParamsStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
requesty?: {
trace_id?: string
extra?: {
mode?: string
}
}
thinking?: AnthropicReasoningParams
}
type RequestyChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
requesty?: {
trace_id?: string
@ -118,12 +128,17 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
...convertToOpenAiMessages(messages),
]
const completionParams: RequestyChatCompletionParams = {
// Map extended efforts to OpenAI Chat Completions-accepted values (omit unsupported)
const allowedEffort = (["low", "medium", "high"] as const).includes(reasoning_effort as any)
? (reasoning_effort as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming["reasoning_effort"])
: undefined
const completionParams: RequestyChatCompletionParamsStreaming = {
messages: openAiMessages,
model,
max_tokens,
temperature,
...(reasoning_effort && reasoning_effort !== "minimal" && { reasoning_effort }),
...(allowedEffort && { reasoning_effort: allowedEffort }),
...(thinking && { thinking }),
stream: true,
stream_options: { include_usage: true },
@ -132,6 +147,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
let stream
try {
// With streaming params type, SDK returns an async iterable stream
stream = await this.client.chat.completions.create(completionParams)
} catch (error) {
throw handleOpenAIError(error, this.providerName)

View file

@ -545,6 +545,79 @@ describe("getModelParams", () => {
expect(result.reasoning).toEqual({ effort: "medium" })
})
it("should include 'minimal' effort for openai format", () => {
const model: ModelInfo = {
...baseModel,
// Array capability explicitly includes minimal
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as any,
}
const result = getModelParams({
...openaiParams,
settings: { reasoningEffort: "minimal" as any },
model,
})
expect(result.reasoningEffort).toBe("minimal")
expect(result.reasoning).toEqual({ reasoning_effort: "minimal" })
})
it("should include 'none' effort for openai format", () => {
const model: ModelInfo = {
...baseModel,
// Array capability explicitly includes none
supportsReasoningEffort: ["none", "low", "medium", "high"] as any,
}
const result = getModelParams({
...openaiParams,
settings: { reasoningEffort: "none" as any },
model,
})
expect(result.reasoningEffort).toBe("none")
expect(result.reasoning).toEqual({ reasoning_effort: "none" })
})
it("should omit reasoning for 'disable' selection", () => {
const model: ModelInfo = {
...baseModel,
supportsReasoningEffort: true,
}
const result = getModelParams({
...openaiParams,
settings: { reasoningEffort: "disable" as any },
model,
})
expect(result.reasoningEffort).toBeUndefined()
expect(result.reasoning).toBeUndefined()
})
it("should include 'minimal' and 'none' for openrouter format", () => {
const model: ModelInfo = {
...baseModel,
// Array capability explicitly includes both
supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as any,
}
const minimalRes = getModelParams({
...openrouterParams,
settings: { reasoningEffort: "minimal" as any },
model,
})
expect(minimalRes.reasoningEffort).toBe("minimal")
expect(minimalRes.reasoning).toEqual({ effort: "minimal" })
const noneRes = getModelParams({
...openrouterParams,
settings: { reasoningEffort: "none" as any },
model,
})
expect(noneRes.reasoningEffort).toBe("none")
expect(noneRes.reasoning).toEqual({ effort: "none" })
})
it("should not use reasoning effort for anthropic format", () => {
const model: ModelInfo = {
...baseModel,

View file

@ -529,7 +529,7 @@ describe("reasoning.ts", () => {
const result = getOpenAiReasoning(optionsWithoutEffort)
expect(result).toEqual({ reasoning_effort: undefined })
expect(result).toBeUndefined()
})
it("should handle all reasoning effort values", () => {
@ -826,10 +826,10 @@ describe("reasoning.ts", () => {
}
const result = getRooReasoning(options)
expect(result).toEqual({ enabled: false })
expect(result).toBeUndefined()
})
it("should not return reasoning params for minimal effort", () => {
it("should include reasoning params for minimal effort", () => {
const modelWithSupported: ModelInfo = {
...baseModel,
supportsReasoningEffort: true,
@ -847,7 +847,7 @@ describe("reasoning.ts", () => {
}
const result = getRooReasoning(options)
expect(result).toBeUndefined()
expect(result).toEqual({ enabled: true, effort: "minimal" })
})
it("should handle all valid reasoning effort values", () => {
@ -889,7 +889,7 @@ describe("reasoning.ts", () => {
}
const result = getRooReasoning(options)
expect(result).toEqual({ enabled: false })
expect(result).toBeUndefined()
})
})
})

View file

@ -2,7 +2,7 @@ import {
type ModelInfo,
type ProviderSettings,
type VerbosityLevel,
type ReasoningEffortWithMinimal,
type ReasoningEffortExtended,
ANTHROPIC_DEFAULT_MAX_TOKENS,
} from "@roo-code/types"
@ -39,7 +39,7 @@ type GetModelParamsOptions<T extends Format> = {
type BaseModelParams = {
maxTokens: number | undefined
temperature: number | undefined
reasoningEffort: ReasoningEffortWithMinimal | undefined
reasoningEffort: ReasoningEffortExtended | undefined
reasoningBudget: number | undefined
verbosity: VerbosityLevel | undefined
}
@ -129,8 +129,17 @@ export function getModelParams({
temperature = 1.0
} else if (shouldUseReasoningEffort({ model, settings })) {
// "Traditional" reasoning models use the `reasoningEffort` parameter.
const effort = customReasoningEffort ?? model.reasoningEffort
reasoningEffort = effort as ReasoningEffortWithMinimal
const effort = (customReasoningEffort ?? model.reasoningEffort) as any
// Do not propagate "disable" into model params; treat as omission
if (effort && effort !== "disable") {
if (model.supportsReasoningEffort === true) {
// Boolean capability: accept extended efforts; UI still exposes low/medium/high by default
reasoningEffort = effort as ReasoningEffortExtended
} else {
// Array capability: honor exactly what's defined by the model
reasoningEffort = effort as ReasoningEffortExtended
}
}
}
const params: BaseModelParams = { maxTokens, temperature, reasoningEffort, reasoningBudget, verbosity }

View file

@ -2,19 +2,19 @@ import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import OpenAI from "openai"
import type { GenerateContentConfig } from "@google/genai"
import type { ModelInfo, ProviderSettings, ReasoningEffortWithMinimal } from "@roo-code/types"
import type { ModelInfo, ProviderSettings, ReasoningEffortExtended } from "@roo-code/types"
import { shouldUseReasoningBudget, shouldUseReasoningEffort } from "../../shared/api"
export type OpenRouterReasoningParams = {
effort?: ReasoningEffortWithMinimal
effort?: ReasoningEffortExtended
max_tokens?: number
exclude?: boolean
}
export type RooReasoningParams = {
enabled?: boolean
effort?: ReasoningEffortWithMinimal
effort?: ReasoningEffortExtended
}
export type AnthropicReasoningParams = BetaThinkingConfigParam
@ -26,7 +26,7 @@ export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"]
export type GetModelReasoningOptions = {
model: ModelInfo
reasoningBudget: number | undefined
reasoningEffort: ReasoningEffortWithMinimal | undefined
reasoningEffort: ReasoningEffortExtended | "disable" | undefined
settings: ProviderSettings
}
@ -39,8 +39,8 @@ export const getOpenRouterReasoning = ({
shouldUseReasoningBudget({ model, settings })
? { max_tokens: reasoningBudget }
: shouldUseReasoningEffort({ model, settings })
? reasoningEffort
? { effort: reasoningEffort }
? reasoningEffort && reasoningEffort !== "disable"
? { effort: reasoningEffort as ReasoningEffortExtended }
: undefined
: undefined
@ -50,27 +50,24 @@ export const getRooReasoning = ({
settings,
}: GetModelReasoningOptions): RooReasoningParams | undefined => {
// Check if model supports reasoning effort
if (!model.supportsReasoningEffort) {
return undefined
}
if (!model.supportsReasoningEffort) return undefined
// If enableReasoningEffort is explicitly false, return enabled: false
// If disabled via toggle, send explicit disabled flag for back-compat
if (settings.enableReasoningEffort === false) {
return { enabled: false }
}
// If reasoning effort is provided, return it with enabled: true
if (reasoningEffort && reasoningEffort !== "minimal") {
return { enabled: true, effort: reasoningEffort }
// If the selection is "disable", omit the field entirely (no reasoning param)
if (reasoningEffort === "disable") {
return undefined
}
// If reasoningEffort is explicitly undefined (None selected), disable reasoning
// This ensures we explicitly tell the backend not to use reasoning
if (reasoningEffort === undefined) {
return { enabled: false }
// When an effort is provided (including "none" and "minimal"), enable with effort
if (reasoningEffort) {
return { enabled: true, effort: reasoningEffort as ReasoningEffortExtended }
}
// Default: no reasoning parameter (reasoning not enabled)
// No explicit selection -> omit field
return undefined
}
@ -86,17 +83,11 @@ export const getOpenAiReasoning = ({
reasoningEffort,
settings,
}: GetModelReasoningOptions): OpenAiReasoningParams | undefined => {
if (!shouldUseReasoningEffort({ model, settings })) {
return undefined
}
if (!shouldUseReasoningEffort({ model, settings })) return undefined
if (reasoningEffort === "disable" || !reasoningEffort) return undefined
// If model has reasoning effort capability, return object even if effort is undefined
// This preserves the reasoning_effort field in the API call
if (reasoningEffort === "minimal") {
return undefined
}
return { reasoning_effort: reasoningEffort }
// Include "none" | "minimal" | "low" | "medium" | "high" literally
return { reasoning_effort: reasoningEffort as any }
}
export const getGeminiReasoning = ({

View file

@ -473,4 +473,45 @@ describe("shouldUseReasoningEffort", () => {
expect(shouldUseReasoningEffort({ model, settings: settingsMedium })).toBe(true)
expect(shouldUseReasoningEffort({ model, settings: settingsHigh })).toBe(true)
})
// New cases for extended capability surface
test("array capability includes 'disable' with selection 'disable' -> false", () => {
const model: ModelInfo = {
contextWindow: 100_000,
supportsPromptCache: true,
supportsReasoningEffort: ["disable", "low", "medium", "high"] as unknown as any,
}
const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "disable" as any }
expect(shouldUseReasoningEffort({ model, settings })).toBe(false)
})
test("array capability includes 'none' with settings.reasoningEffort='none' -> true", () => {
const model: ModelInfo = {
contextWindow: 100_000,
supportsPromptCache: true,
supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as unknown as any,
}
const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "none" as any }
expect(shouldUseReasoningEffort({ model, settings })).toBe(true)
})
test("array capability includes 'minimal' with settings.reasoningEffort='minimal' -> true", () => {
const model: ModelInfo = {
contextWindow: 100_000,
supportsPromptCache: true,
supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as unknown as any,
}
const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "minimal" as any }
expect(shouldUseReasoningEffort({ model, settings })).toBe(true)
})
test("boolean true with 'none' and 'minimal' -> true", () => {
const model: ModelInfo = {
contextWindow: 100_000,
supportsPromptCache: true,
supportsReasoningEffort: true,
}
expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "none" as any } })).toBe(true)
expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "minimal" as any } })).toBe(true)
})
})

View file

@ -63,15 +63,44 @@ export const shouldUseReasoningEffort = ({
model: ModelInfo
settings?: ProviderSettings
}): boolean => {
// If enableReasoningEffort is explicitly set to false, reasoning should be disabled
if (settings?.enableReasoningEffort === false) {
return false
// Explicit off switch
if (settings?.enableReasoningEffort === false) return false
// Selected effort from settings or model default
const selectedEffort = (settings?.reasoningEffort ?? (model as any).reasoningEffort) as
| "disable"
| "none"
| "minimal"
| "low"
| "medium"
| "high"
| undefined
// "disable" explicitly omits reasoning
if (selectedEffort === "disable") return false
const cap = model.supportsReasoningEffort as unknown
// Capability array: use only if selected is included (treat "none"/"minimal" as valid)
if (Array.isArray(cap)) {
return !!selectedEffort && (cap as ReadonlyArray<string>).includes(selectedEffort as string)
}
// Otherwise, use reasoning if:
// 1. Model supports reasoning effort AND settings provide reasoning effort, OR
// 2. Model itself has a reasoningEffort property
return (!!model.supportsReasoningEffort && !!settings?.reasoningEffort) || !!model.reasoningEffort
// Boolean capability: true → require a selected effort
if (model.supportsReasoningEffort === true) {
return !!selectedEffort
}
// Not explicitly supported: only allow when the model itself defines a default effort
// Ignore settings-only selections when capability is absent/false
const modelDefaultEffort = (model as any).reasoningEffort as
| "none"
| "minimal"
| "low"
| "medium"
| "high"
| undefined
return !!modelDefaultEffort
}
export const DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS = 16_384

View file

@ -1,3 +1,36 @@
/*
Semantics for Reasoning Effort (SimpleThinkingBudget)
Capability surface:
- modelInfo.supportsReasoningEffort: boolean | Array&lt;"disable" | "none" | "minimal" | "low" | "medium" | "high"&gt;
- true UI shows ["low","medium","high"]
- array UI shows exactly the provided values
Selection behavior:
- "disable":
- Label: t("settings:providers.reasoningEffort.none")
- set enableReasoningEffort = false
- persist reasoningEffort = "disable"
- request builders omit any reasoning parameter/body sections
- "none":
- Label: t("settings:providers.reasoningEffort.none")
- set enableReasoningEffort = true
- persist reasoningEffort = "none"
- request builders include reasoning with value "none"
- "minimal" | "low" | "medium" | "high":
- set enableReasoningEffort = true
- persist the selected value
- request builders include reasoning with the selected effort
Required:
- If modelInfo.requiredReasoningEffort is true, do not synthesize a "None" choice. Only show values from the capability.
- On mount, if unset and a default exists, set enableReasoningEffort = true and use modelInfo.reasoningEffort.
Notes:
- Current selection is normalized to the capability: unsupported persisted values are not shown.
- Both "disable" and "none" display as the "None" label per UX, but are wired differently as above.
- "minimal" uses t("settings:providers.reasoningEffort.minimal").
*/
import { useEffect } from "react"
import { type ProviderSettings, type ModelInfo, type ReasoningEffort, reasoningEfforts } from "@roo-code/types"
@ -15,8 +48,8 @@ interface SimpleThinkingBudgetProps {
modelInfo?: ModelInfo
}
// Extended type to include "none" option
type ReasoningEffortWithNone = ReasoningEffort | "none"
// Reasoning selection values including control values
type ReasoningSelectValue = "disable" | "none" | "minimal" | ReasoningEffort
export const SimpleThinkingBudget = ({
apiConfiguration,
@ -25,57 +58,46 @@ export const SimpleThinkingBudget = ({
}: SimpleThinkingBudgetProps) => {
const { t } = useAppTranslation()
// Check model capabilities
const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort
const isReasoningEffortRequired = !!modelInfo && modelInfo.requiredReasoningEffort
const isSupported = !!modelInfo?.supportsReasoningEffort
// Build available reasoning efforts list
// Include "none" option unless reasoning effort is required
const baseEfforts = [...reasoningEfforts] as ReasoningEffort[]
const availableReasoningEfforts: ReadonlyArray<ReasoningEffortWithNone> = isReasoningEffortRequired
? baseEfforts
: (["none", ...baseEfforts] as ReasoningEffortWithNone[])
const isReasoningEffortRequired = !!modelInfo?.requiredReasoningEffort
// Default reasoning effort - use model's default if available, otherwise "medium"
const modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffort | undefined
const defaultReasoningEffort: ReasoningEffortWithNone = isReasoningEffortRequired
? modelDefaultReasoningEffort || "medium"
: "none"
// Compute available options from capability
const supports = modelInfo?.supportsReasoningEffort
const availableOptions: readonly ReasoningSelectValue[] =
supports === true ? ([...reasoningEfforts] as const) : (supports as any)
// Current reasoning effort - treat undefined/null as "none"
const currentReasoningEffort: ReasoningEffortWithNone =
(apiConfiguration.reasoningEffort as ReasoningEffort | undefined) || defaultReasoningEffort
// Helper for labels
const labelFor = (v: ReasoningSelectValue) => {
if (v === "disable" || v === "none") return t("settings:providers.reasoningEffort.none")
if (v === "minimal") return t("settings:providers.reasoningEffort.minimal")
return t(`settings:providers.reasoningEffort.${v}`)
}
// Set default reasoning effort when model supports it and no value is set
// Determine current selection (normalize to capability)
let current: ReasoningSelectValue | undefined = apiConfiguration.reasoningEffort as ReasoningSelectValue | undefined
if (!current && isReasoningEffortRequired && modelInfo.reasoningEffort) {
current = modelInfo.reasoningEffort as ReasoningSelectValue
}
// If persisted value isn't supported by capability (e.g., "minimal" while supports=true), don't show it
const normalizedCurrent: ReasoningSelectValue | undefined =
current && (availableOptions as readonly any[]).includes(current) ? current : undefined
// Default when required: set to model default on mount (no synthetic "None")
useEffect(() => {
if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) {
// Only set a default if reasoning is required, otherwise leave as undefined (which maps to "none")
if (isReasoningEffortRequired && defaultReasoningEffort !== "none") {
setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffort, false)
}
if (!isReasoningEffortRequired) return
if (!apiConfiguration.reasoningEffort && modelInfo?.reasoningEffort) {
setApiConfigurationField("enableReasoningEffort", true, false)
setApiConfigurationField("reasoningEffort", modelInfo?.reasoningEffort as any, false)
}
}, [
isReasoningEffortSupported,
isReasoningEffortRequired,
apiConfiguration.reasoningEffort,
defaultReasoningEffort,
modelInfo?.reasoningEffort,
setApiConfigurationField,
])
useEffect(() => {
if (!isReasoningEffortSupported) return
const shouldEnable = isReasoningEffortRequired || currentReasoningEffort !== "none"
if (shouldEnable && apiConfiguration.enableReasoningEffort !== true) {
setApiConfigurationField("enableReasoningEffort", true, false)
}
}, [
isReasoningEffortSupported,
isReasoningEffortRequired,
currentReasoningEffort,
apiConfiguration.enableReasoningEffort,
setApiConfigurationField,
])
if (!modelInfo || !isReasoningEffortSupported) {
if (!isSupported) {
return null
}
@ -85,32 +107,25 @@ export const SimpleThinkingBudget = ({
<label className="block font-medium mb-1">{t("settings:providers.reasoningEffort.label")}</label>
</div>
<Select
value={currentReasoningEffort}
onValueChange={(value: ReasoningEffortWithNone) => {
// If "none" is selected, clear the reasoningEffort field
if (value === "none") {
setApiConfigurationField("reasoningEffort", undefined)
value={normalizedCurrent}
onValueChange={(value: ReasoningSelectValue) => {
if (value === "disable") {
setApiConfigurationField("enableReasoningEffort", false, true)
setApiConfigurationField("reasoningEffort", "disable" as any, true)
} else {
setApiConfigurationField("reasoningEffort", value as ReasoningEffort)
setApiConfigurationField("enableReasoningEffort", true, true)
setApiConfigurationField("reasoningEffort", value as any, true)
}
}}>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
currentReasoningEffort
? currentReasoningEffort === "none"
? t("settings:providers.reasoningEffort.none")
: t(`settings:providers.reasoningEffort.${currentReasoningEffort}`)
: t("settings:common.select")
}
placeholder={normalizedCurrent ? labelFor(normalizedCurrent) : t("settings:common.select")}
/>
</SelectTrigger>
<SelectContent>
{availableReasoningEfforts.map((value) => (
{availableOptions.map((value) => (
<SelectItem key={value} value={value}>
{value === "none"
? t("settings:providers.reasoningEffort.none")
: t(`settings:providers.reasoningEffort.${value}`)}
{labelFor(value)}
</SelectItem>
))}
</SelectContent>

View file

@ -28,17 +28,6 @@ interface ThinkingBudgetProps {
modelInfo?: ModelInfo
}
// Helper function to determine if minimal option should be shown
const shouldShowMinimalOption = (
provider: string | undefined,
modelId: string | undefined,
supportsEffort: boolean | undefined,
): boolean => {
const isGpt5Model = provider === "openai-native" && modelId?.startsWith("gpt-5")
const isOpenRouterWithEffort = provider === "openrouter" && supportsEffort === true
return !!(isGpt5Model || isOpenRouterWithEffort)
}
export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
const { t } = useAppTranslation()
const { id: selectedModelId } = useSelectedModel(apiConfiguration)
@ -53,18 +42,14 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget
const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort
// Determine if minimal option should be shown
const showMinimalOption = shouldShowMinimalOption(
apiConfiguration.apiProvider,
selectedModelId,
isReasoningEffortSupported,
)
// Build available reasoning efforts list
const baseEfforts = [...reasoningEfforts] as ReasoningEffortWithMinimal[]
const availableReasoningEfforts: ReadonlyArray<ReasoningEffortWithMinimal> = showMinimalOption
? (["minimal", ...baseEfforts] as ReasoningEffortWithMinimal[])
: baseEfforts
// Build available reasoning efforts list from capability
const supports = modelInfo?.supportsReasoningEffort
const availableOptions: ReadonlyArray<ReasoningEffortWithMinimal> =
supports === true
? (reasoningEfforts as readonly ReasoningEffortWithMinimal[])
: Array.isArray(supports)
? (supports as ReadonlyArray<ReasoningEffortWithMinimal>)
: (reasoningEfforts as readonly ReasoningEffortWithMinimal[])
// Default reasoning effort - use model's default if available
// GPT-5 models have "medium" as their default in the model configuration
@ -187,7 +172,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
/>
</SelectTrigger>
<SelectContent>
{availableReasoningEfforts.map((value) => (
{availableOptions.map((value) => (
<SelectItem key={value} value={value}>
{t(`settings:providers.reasoningEffort.${value}`)}
</SelectItem>

View file

@ -10,6 +10,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({
const translations: Record<string, string> = {
"settings:providers.reasoningEffort.label": "Model Reasoning Effort",
"settings:providers.reasoningEffort.none": "None",
"settings:providers.reasoningEffort.minimal": "Minimal",
"settings:providers.reasoningEffort.low": "Low",
"settings:providers.reasoningEffort.medium": "Medium",
"settings:providers.reasoningEffort.high": "High",
@ -189,7 +190,7 @@ describe("SimpleThinkingBudget", () => {
/>,
)
expect(screen.getByText("None")).toBeInTheDocument()
expect(screen.getByText("Select")).toBeInTheDocument()
})
it("should use model default reasoning effort when required and available", () => {