Skip to content

Commit 22e9b98

Browse files
committed
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
1 parent f30caf2 commit 22e9b98

File tree

4 files changed

+193
-8
lines changed

4 files changed

+193
-8
lines changed

src/api/providers/fetchers/__tests__/openrouter.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,5 +386,76 @@ describe("OpenRouter API", () => {
386386
expect(textResult.maxTokens).toBe(64000)
387387
expect(imageResult.maxTokens).toBe(64000)
388388
})
389+
390+
it("handles grok-4-fast reasoning correctly", () => {
391+
const mockModel = {
392+
name: "Grok 4 Fast",
393+
context_length: 32768,
394+
max_completion_tokens: 8192,
395+
}
396+
397+
// Test with reasoning in supported parameters
398+
const resultWithReasoning = parseOpenRouterModel({
399+
id: "x-ai/grok-4-fast",
400+
model: mockModel,
401+
inputModality: ["text"],
402+
outputModality: ["text"],
403+
maxTokens: 8192,
404+
supportedParameters: ["temperature", "max_tokens", "reasoning"],
405+
})
406+
407+
expect(resultWithReasoning.supportsReasoningEffort).toBe(false)
408+
expect(resultWithReasoning.supportedParameters).toContain("reasoning")
409+
expect(resultWithReasoning.supportedParameters).toContain("temperature")
410+
expect(resultWithReasoning.supportedParameters).toContain("max_tokens")
411+
412+
// Test without reasoning in supported parameters - should add it
413+
const resultWithoutReasoning = parseOpenRouterModel({
414+
id: "x-ai/grok-4-fast",
415+
model: mockModel,
416+
inputModality: ["text"],
417+
outputModality: ["text"],
418+
maxTokens: 8192,
419+
supportedParameters: ["temperature", "max_tokens"],
420+
})
421+
422+
expect(resultWithoutReasoning.supportsReasoningEffort).toBe(false)
423+
expect(resultWithoutReasoning.supportedParameters).toContain("reasoning")
424+
expect(resultWithoutReasoning.supportedParameters).toContain("temperature")
425+
expect(resultWithoutReasoning.supportedParameters).toContain("max_tokens")
426+
427+
// Test with undefined supported parameters
428+
const resultNoParams = parseOpenRouterModel({
429+
id: "x-ai/grok-4-fast",
430+
model: mockModel,
431+
inputModality: ["text"],
432+
outputModality: ["text"],
433+
maxTokens: 8192,
434+
})
435+
436+
expect(resultNoParams.supportsReasoningEffort).toBe(false)
437+
expect(resultNoParams.supportedParameters).toContain("reasoning")
438+
})
439+
440+
it("does not affect other models reasoning configuration", () => {
441+
const mockModel = {
442+
name: "Other Model",
443+
context_length: 32768,
444+
max_completion_tokens: 8192,
445+
}
446+
447+
const result = parseOpenRouterModel({
448+
id: "other/model",
449+
model: mockModel,
450+
inputModality: ["text"],
451+
outputModality: ["text"],
452+
maxTokens: 8192,
453+
supportedParameters: ["temperature", "max_tokens", "reasoning"],
454+
})
455+
456+
// Should not modify supportsReasoningEffort for other models
457+
expect(result.supportsReasoningEffort).toBe(true)
458+
expect(result.supportedParameters).toContain("reasoning")
459+
})
389460
})
390461
})

src/api/providers/fetchers/openrouter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,5 +270,16 @@ export const parseOpenRouterModel = ({
270270
modelInfo.maxTokens = 32768
271271
}
272272

273+
// Special handling for grok-4-fast on OpenRouter - it should use simple on/off reasoning toggle
274+
// instead of reasoning effort levels (low/medium/high)
275+
if (id === "x-ai/grok-4-fast") {
276+
// Disable reasoning effort but keep reasoning in supportedParameters for on/off toggle
277+
modelInfo.supportsReasoningEffort = false
278+
// Ensure reasoning is in supportedParameters
279+
if (!supportedParameters || !supportedParameters.includes("reasoning")) {
280+
modelInfo.supportedParameters = [...(modelInfo.supportedParameters || []), "reasoning"]
281+
}
282+
}
283+
273284
return modelInfo
274285
}

src/api/transform/__tests__/reasoning.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,89 @@ describe("reasoning.ts", () => {
284284

285285
expect(result).toBeUndefined()
286286
})
287+
288+
it("should handle models with simple on/off reasoning toggle (like grok-4-fast)", () => {
289+
// Model with reasoning support but no effort or budget capabilities
290+
const modelWithSimpleToggle: ModelInfo = {
291+
maxTokens: 8192,
292+
contextWindow: 32768,
293+
supportsPromptCache: false,
294+
supportedParameters: ["reasoning"],
295+
supportsReasoningEffort: false,
296+
supportsReasoningBudget: false,
297+
}
298+
299+
// Test with reasoning enabled (not minimal)
300+
const optionsWithReasoningEnabled = {
301+
...baseOptions,
302+
model: modelWithSimpleToggle,
303+
reasoningEffort: "high" as ReasoningEffortWithMinimal,
304+
}
305+
306+
const resultEnabled = getOpenRouterReasoning(optionsWithReasoningEnabled)
307+
expect(resultEnabled).toEqual({ exclude: false })
308+
309+
// Test with reasoning disabled (minimal)
310+
const optionsWithReasoningDisabled = {
311+
...baseOptions,
312+
model: modelWithSimpleToggle,
313+
reasoningEffort: "minimal" as ReasoningEffortWithMinimal,
314+
}
315+
316+
const resultDisabled = getOpenRouterReasoning(optionsWithReasoningDisabled)
317+
expect(resultDisabled).toEqual({ exclude: true })
318+
319+
// Test without reasoning effort setting (should enable by default)
320+
const optionsWithoutEffort = {
321+
...baseOptions,
322+
model: modelWithSimpleToggle,
323+
}
324+
325+
const resultDefault = getOpenRouterReasoning(optionsWithoutEffort)
326+
expect(resultDefault).toEqual({ exclude: false })
327+
})
328+
329+
it("should not apply simple toggle logic to models with effort or budget capabilities", () => {
330+
// Model with reasoning effort capability
331+
const modelWithEffort: ModelInfo = {
332+
maxTokens: 8192,
333+
contextWindow: 32768,
334+
supportsPromptCache: false,
335+
supportedParameters: ["reasoning"],
336+
supportsReasoningEffort: true,
337+
}
338+
339+
const optionsWithEffort = {
340+
...baseOptions,
341+
model: modelWithEffort,
342+
settings: { reasoningEffort: "high" as ReasoningEffortWithMinimal },
343+
reasoningEffort: "high" as ReasoningEffortWithMinimal,
344+
}
345+
346+
const resultWithEffort = getOpenRouterReasoning(optionsWithEffort)
347+
// Should use effort logic, not simple toggle
348+
expect(resultWithEffort).toEqual({ effort: "high" })
349+
350+
// Model with reasoning budget capability
351+
const modelWithBudget: ModelInfo = {
352+
maxTokens: 8192,
353+
contextWindow: 32768,
354+
supportsPromptCache: false,
355+
supportedParameters: ["reasoning"],
356+
supportsReasoningBudget: true,
357+
}
358+
359+
const optionsWithBudget = {
360+
...baseOptions,
361+
model: modelWithBudget,
362+
settings: { enableReasoningEffort: true },
363+
reasoningBudget: 1000,
364+
}
365+
366+
const resultWithBudget = getOpenRouterReasoning(optionsWithBudget)
367+
// Should use budget logic, not simple toggle
368+
expect(resultWithBudget).toEqual({ max_tokens: 1000 })
369+
})
287370
})
288371

289372
describe("getAnthropicReasoning", () => {

src/api/transform/reasoning.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,34 @@ export const getOpenRouterReasoning = ({
3030
reasoningBudget,
3131
reasoningEffort,
3232
settings,
33-
}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined =>
34-
shouldUseReasoningBudget({ model, settings })
35-
? { max_tokens: reasoningBudget }
36-
: shouldUseReasoningEffort({ model, settings })
37-
? reasoningEffort
38-
? { effort: reasoningEffort }
39-
: undefined
40-
: undefined
33+
}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined => {
34+
// Check if model should use reasoning budget
35+
if (shouldUseReasoningBudget({ model, settings })) {
36+
return { max_tokens: reasoningBudget }
37+
}
38+
39+
// Check if model should use reasoning effort
40+
if (shouldUseReasoningEffort({ model, settings })) {
41+
return reasoningEffort ? { effort: reasoningEffort } : undefined
42+
}
43+
44+
// Check if model supports reasoning but not effort or budget (e.g., grok-4-fast on OpenRouter)
45+
// These models use a simple on/off toggle via the exclude parameter
46+
if (
47+
model.supportedParameters?.includes("reasoning") &&
48+
!model.supportsReasoningEffort &&
49+
!model.supportsReasoningBudget
50+
) {
51+
// If reasoning is not explicitly disabled, enable it
52+
// We use exclude: false to enable reasoning, and exclude: true to disable
53+
// Check both settings.reasoningEffort and the passed reasoningEffort parameter
54+
const effectiveEffort = reasoningEffort || settings.reasoningEffort
55+
const reasoningEnabled = effectiveEffort !== "minimal"
56+
return reasoningEnabled ? { exclude: false } : { exclude: true }
57+
}
58+
59+
return undefined
60+
}
4161

4262
export const getAnthropicReasoning = ({
4363
model,

0 commit comments

Comments
 (0)