Roo-Code/src/api/transform/model-params.ts
Roo Code 46ea320821 fix: respect supportsTemperature flag in OpenAI-compatible providers
When models like Claude Opus 4.7 are proxied through OpenAI-compatible
gateways, sending the temperature parameter causes a 400 error because
the model has deprecated it.

This change:
- Uses the existing supportsTemperature ModelInfo field in model-params.ts
  to omit temperature when supportsTemperature === false
- Adds the same guard in OpenAiHandler, BaseOpenAiCompatibleProvider,
  and OpenAICompatibleHandler
- Adds a "Supports Temperature" checkbox in the OpenAI Compatible
  settings UI so users can toggle it off for models that need it
- Adds test coverage for the new behavior

Closes #12162
2026-04-21 15:25:56 +00:00

186 lines
6 KiB
TypeScript

import {
type ModelInfo,
type ProviderSettings,
type VerbosityLevel,
type ReasoningEffortExtended,
ANTHROPIC_DEFAULT_MAX_TOKENS,
} from "@roo-code/types"
import {
DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS,
DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,
GEMINI_25_PRO_MIN_THINKING_TOKENS,
shouldUseReasoningBudget,
shouldUseReasoningEffort,
getModelMaxOutputTokens,
} from "../../shared/api"
import {
type AnthropicReasoningParams,
type OpenAiReasoningParams,
type GeminiReasoningParams,
type OpenRouterReasoningParams,
getAnthropicReasoning,
getOpenAiReasoning,
getGeminiReasoning,
getOpenRouterReasoning,
} from "./reasoning"
type Format = "anthropic" | "openai" | "gemini" | "openrouter"
type GetModelParamsOptions<T extends Format> = {
format: T
modelId: string
model: ModelInfo
settings: ProviderSettings
defaultTemperature: number
}
type BaseModelParams = {
maxTokens: number | undefined
temperature: number | undefined
reasoningEffort: ReasoningEffortExtended | undefined
reasoningBudget: number | undefined
verbosity: VerbosityLevel | undefined
tools?: boolean
}
type AnthropicModelParams = {
format: "anthropic"
reasoning: AnthropicReasoningParams | undefined
} & BaseModelParams
type OpenAiModelParams = {
format: "openai"
reasoning: OpenAiReasoningParams | undefined
} & BaseModelParams
type GeminiModelParams = {
format: "gemini"
reasoning: GeminiReasoningParams | undefined
} & BaseModelParams
type OpenRouterModelParams = {
format: "openrouter"
reasoning: OpenRouterReasoningParams | undefined
} & BaseModelParams
export type ModelParams = AnthropicModelParams | OpenAiModelParams | GeminiModelParams | OpenRouterModelParams
// Function overloads for specific return types
export function getModelParams(options: GetModelParamsOptions<"anthropic">): AnthropicModelParams
export function getModelParams(options: GetModelParamsOptions<"openai">): OpenAiModelParams
export function getModelParams(options: GetModelParamsOptions<"gemini">): GeminiModelParams
export function getModelParams(options: GetModelParamsOptions<"openrouter">): OpenRouterModelParams
export function getModelParams({
format,
modelId,
model,
settings,
defaultTemperature,
}: GetModelParamsOptions<Format>): ModelParams {
const {
modelMaxTokens: customMaxTokens,
modelMaxThinkingTokens: customMaxThinkingTokens,
modelTemperature: customTemperature,
reasoningEffort: customReasoningEffort,
verbosity: customVerbosity,
} = settings
// Use the centralized logic for computing maxTokens
const maxTokens = getModelMaxOutputTokens({
modelId,
model,
settings,
format,
})
let temperature = customTemperature ?? model.defaultTemperature ?? defaultTemperature
let reasoningBudget: ModelParams["reasoningBudget"] = undefined
let reasoningEffort: ModelParams["reasoningEffort"] = undefined
let verbosity: VerbosityLevel | undefined = customVerbosity
if (shouldUseReasoningBudget({ model, settings })) {
// Check if this is a Gemini 2.5 Pro model
const isGemini25Pro = modelId.includes("gemini-2.5-pro")
// If `customMaxThinkingTokens` is not specified use the default.
// For Gemini 2.5 Pro, default to 128 instead of 8192
const defaultThinkingTokens = isGemini25Pro
? GEMINI_25_PRO_MIN_THINKING_TOKENS
: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS
reasoningBudget = customMaxThinkingTokens ?? defaultThinkingTokens
// Reasoning cannot exceed 80% of the `maxTokens` value.
// maxTokens should always be defined for reasoning budget models, but add a guard just in case
if (maxTokens && reasoningBudget > Math.floor(maxTokens * 0.8)) {
reasoningBudget = Math.floor(maxTokens * 0.8)
}
// Reasoning cannot be less than minimum tokens.
// For Gemini 2.5 Pro models, the minimum is 128 tokens
// For other models, the minimum is 1024 tokens
const minThinkingTokens = isGemini25Pro ? GEMINI_25_PRO_MIN_THINKING_TOKENS : 1024
if (reasoningBudget < minThinkingTokens) {
reasoningBudget = minThinkingTokens
}
// Let's assume that "Hybrid" reasoning models require a temperature of
// 1.0 since Anthropic does.
temperature = 1.0
} else if (shouldUseReasoningEffort({ model, settings })) {
// "Traditional" reasoning models use the `reasoningEffort` parameter.
// Only fallback to model default if user hasn't explicitly set a value.
// If customReasoningEffort is "disable", don't fallback to model default.
const effort =
customReasoningEffort !== undefined
? customReasoningEffort
: (model.reasoningEffort as ReasoningEffortExtended | "disable" | undefined)
// Capability and settings checks are handled by shouldUseReasoningEffort.
// Here we simply propagate the resolved effort into the params, while
// still treating "disable" as an omission.
if (effort && effort !== "disable") {
reasoningEffort = effort as ReasoningEffortExtended
}
}
const params: BaseModelParams = { maxTokens, temperature, reasoningEffort, reasoningBudget, verbosity }
if (format === "anthropic") {
return {
format,
...params,
reasoning: getAnthropicReasoning({ model, reasoningBudget, reasoningEffort, settings }),
}
} else if (format === "openai") {
// Omit temperature for models that don't support it (e.g. o1, o3-mini,
// claude-opus-4-7 proxied via OpenAI-compatible gateways).
if (model.supportsTemperature === false || modelId.startsWith("o1") || modelId.startsWith("o3-mini")) {
params.temperature = undefined
}
return {
format,
...params,
reasoning: getOpenAiReasoning({ model, reasoningBudget, reasoningEffort, settings }),
// Whether tools are included is determined by whether the caller provided tool definitions.
}
} else if (format === "gemini") {
return {
format,
...params,
reasoning: getGeminiReasoning({ model, reasoningBudget, reasoningEffort, settings }),
}
} else {
// Omit temperature for models that don't support it.
if (model.supportsTemperature === false || modelId === "openai/o1-pro") {
params.temperature = undefined
}
return {
format,
...params,
reasoning: getOpenRouterReasoning({ model, reasoningBudget, reasoningEffort, settings }),
}
}
}