Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/api/providers/fetchers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
})
11 changes: 11 additions & 0 deletions src/api/providers/fetchers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
83 changes: 83 additions & 0 deletions src/api/transform/__tests__/reasoning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
36 changes: 28 additions & 8 deletions src/api/transform/reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both reasoningEffort and settings.reasoningEffort are undefined, this code enables reasoning by default ({ exclude: false }). This is inconsistent with how other reasoning models work, where undefined effort typically results in no reasoning parameters being sent. For grok-4-fast, when no effort preference is set, it should return undefined instead of defaulting to enabled. This ensures the API's default behavior is respected.

}
Comment on lines +44 to +57
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simple toggle logic doesn't respect the settings.enableReasoningEffort === false setting. When users explicitly disable reasoning effort in settings, the model should not use reasoning, but this code will still enable it based on the effort values. This should check settings?.enableReasoningEffort === false and return undefined if true, similar to how shouldUseReasoningEffort handles this in src/shared/api.ts lines 66-69.


return undefined
}

export const getAnthropicReasoning = ({
model,
Expand Down
Loading