mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
c49f9ab846
commit
01cb12f167
16 changed files with 522 additions and 246 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,36 @@
|
|||
/*
|
||||
Semantics for Reasoning Effort (SimpleThinkingBudget)
|
||||
|
||||
Capability surface:
|
||||
- modelInfo.supportsReasoningEffort: boolean | Array<"disable" | "none" | "minimal" | "low" | "medium" | "high">
|
||||
- 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue