fix: support simple on/off reasoning toggle for grok-4-fast on OpenRouter

- Added special handling for x-ai/grok-4-fast model in OpenRouter parser
- Disabled reasoning effort for this model while keeping reasoning parameter
- Updated reasoning logic to handle models with simple on/off toggle
- Added comprehensive tests for the new functionality

Fixes #8709
This commit is contained in:
Roo Code 2025-10-17 19:05:28 +00:00
parent f30caf277c
commit 22e9b98ccf
4 changed files with 193 additions and 8 deletions

View file

@ -386,5 +386,76 @@ describe("OpenRouter API", () => {
expect(textResult.maxTokens).toBe(64000)
expect(imageResult.maxTokens).toBe(64000)
})
it("handles grok-4-fast reasoning correctly", () => {
const mockModel = {
name: "Grok 4 Fast",
context_length: 32768,
max_completion_tokens: 8192,
}
// Test with reasoning in supported parameters
const resultWithReasoning = parseOpenRouterModel({
id: "x-ai/grok-4-fast",
model: mockModel,
inputModality: ["text"],
outputModality: ["text"],
maxTokens: 8192,
supportedParameters: ["temperature", "max_tokens", "reasoning"],
})
expect(resultWithReasoning.supportsReasoningEffort).toBe(false)
expect(resultWithReasoning.supportedParameters).toContain("reasoning")
expect(resultWithReasoning.supportedParameters).toContain("temperature")
expect(resultWithReasoning.supportedParameters).toContain("max_tokens")
// Test without reasoning in supported parameters - should add it
const resultWithoutReasoning = parseOpenRouterModel({
id: "x-ai/grok-4-fast",
model: mockModel,
inputModality: ["text"],
outputModality: ["text"],
maxTokens: 8192,
supportedParameters: ["temperature", "max_tokens"],
})
expect(resultWithoutReasoning.supportsReasoningEffort).toBe(false)
expect(resultWithoutReasoning.supportedParameters).toContain("reasoning")
expect(resultWithoutReasoning.supportedParameters).toContain("temperature")
expect(resultWithoutReasoning.supportedParameters).toContain("max_tokens")
// Test with undefined supported parameters
const resultNoParams = parseOpenRouterModel({
id: "x-ai/grok-4-fast",
model: mockModel,
inputModality: ["text"],
outputModality: ["text"],
maxTokens: 8192,
})
expect(resultNoParams.supportsReasoningEffort).toBe(false)
expect(resultNoParams.supportedParameters).toContain("reasoning")
})
it("does not affect other models reasoning configuration", () => {
const mockModel = {
name: "Other Model",
context_length: 32768,
max_completion_tokens: 8192,
}
const result = parseOpenRouterModel({
id: "other/model",
model: mockModel,
inputModality: ["text"],
outputModality: ["text"],
maxTokens: 8192,
supportedParameters: ["temperature", "max_tokens", "reasoning"],
})
// Should not modify supportsReasoningEffort for other models
expect(result.supportsReasoningEffort).toBe(true)
expect(result.supportedParameters).toContain("reasoning")
})
})
})

View file

@ -270,5 +270,16 @@ export const parseOpenRouterModel = ({
modelInfo.maxTokens = 32768
}
// Special handling for grok-4-fast on OpenRouter - it should use simple on/off reasoning toggle
// instead of reasoning effort levels (low/medium/high)
if (id === "x-ai/grok-4-fast") {
// Disable reasoning effort but keep reasoning in supportedParameters for on/off toggle
modelInfo.supportsReasoningEffort = false
// Ensure reasoning is in supportedParameters
if (!supportedParameters || !supportedParameters.includes("reasoning")) {
modelInfo.supportedParameters = [...(modelInfo.supportedParameters || []), "reasoning"]
}
}
return modelInfo
}

View file

@ -284,6 +284,89 @@ describe("reasoning.ts", () => {
expect(result).toBeUndefined()
})
it("should handle models with simple on/off reasoning toggle (like grok-4-fast)", () => {
// Model with reasoning support but no effort or budget capabilities
const modelWithSimpleToggle: ModelInfo = {
maxTokens: 8192,
contextWindow: 32768,
supportsPromptCache: false,
supportedParameters: ["reasoning"],
supportsReasoningEffort: false,
supportsReasoningBudget: false,
}
// Test with reasoning enabled (not minimal)
const optionsWithReasoningEnabled = {
...baseOptions,
model: modelWithSimpleToggle,
reasoningEffort: "high" as ReasoningEffortWithMinimal,
}
const resultEnabled = getOpenRouterReasoning(optionsWithReasoningEnabled)
expect(resultEnabled).toEqual({ exclude: false })
// Test with reasoning disabled (minimal)
const optionsWithReasoningDisabled = {
...baseOptions,
model: modelWithSimpleToggle,
reasoningEffort: "minimal" as ReasoningEffortWithMinimal,
}
const resultDisabled = getOpenRouterReasoning(optionsWithReasoningDisabled)
expect(resultDisabled).toEqual({ exclude: true })
// Test without reasoning effort setting (should enable by default)
const optionsWithoutEffort = {
...baseOptions,
model: modelWithSimpleToggle,
}
const resultDefault = getOpenRouterReasoning(optionsWithoutEffort)
expect(resultDefault).toEqual({ exclude: false })
})
it("should not apply simple toggle logic to models with effort or budget capabilities", () => {
// Model with reasoning effort capability
const modelWithEffort: ModelInfo = {
maxTokens: 8192,
contextWindow: 32768,
supportsPromptCache: false,
supportedParameters: ["reasoning"],
supportsReasoningEffort: true,
}
const optionsWithEffort = {
...baseOptions,
model: modelWithEffort,
settings: { reasoningEffort: "high" as ReasoningEffortWithMinimal },
reasoningEffort: "high" as ReasoningEffortWithMinimal,
}
const resultWithEffort = getOpenRouterReasoning(optionsWithEffort)
// Should use effort logic, not simple toggle
expect(resultWithEffort).toEqual({ effort: "high" })
// Model with reasoning budget capability
const modelWithBudget: ModelInfo = {
maxTokens: 8192,
contextWindow: 32768,
supportsPromptCache: false,
supportedParameters: ["reasoning"],
supportsReasoningBudget: true,
}
const optionsWithBudget = {
...baseOptions,
model: modelWithBudget,
settings: { enableReasoningEffort: true },
reasoningBudget: 1000,
}
const resultWithBudget = getOpenRouterReasoning(optionsWithBudget)
// Should use budget logic, not simple toggle
expect(resultWithBudget).toEqual({ max_tokens: 1000 })
})
})
describe("getAnthropicReasoning", () => {

View file

@ -30,14 +30,34 @@ export const getOpenRouterReasoning = ({
reasoningBudget,
reasoningEffort,
settings,
}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined =>
shouldUseReasoningBudget({ model, settings })
? { max_tokens: reasoningBudget }
: shouldUseReasoningEffort({ model, settings })
? reasoningEffort
? { effort: reasoningEffort }
: undefined
: undefined
}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined => {
// Check if model should use reasoning budget
if (shouldUseReasoningBudget({ model, settings })) {
return { max_tokens: reasoningBudget }
}
// Check if model should use reasoning effort
if (shouldUseReasoningEffort({ model, settings })) {
return reasoningEffort ? { effort: reasoningEffort } : undefined
}
// Check if model supports reasoning but not effort or budget (e.g., grok-4-fast on OpenRouter)
// These models use a simple on/off toggle via the exclude parameter
if (
model.supportedParameters?.includes("reasoning") &&
!model.supportsReasoningEffort &&
!model.supportsReasoningBudget
) {
// If reasoning is not explicitly disabled, enable it
// We use exclude: false to enable reasoning, and exclude: true to disable
// Check both settings.reasoningEffort and the passed reasoningEffort parameter
const effectiveEffort = reasoningEffort || settings.reasoningEffort
const reasoningEnabled = effectiveEffort !== "minimal"
return reasoningEnabled ? { exclude: false } : { exclude: true }
}
return undefined
}
export const getAnthropicReasoning = ({
model,