diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index 993cd4c23de5..98b98797bd49 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -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") + }) }) }) diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index fa0f0954cd92..30958eea9023 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -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 } diff --git a/src/api/transform/__tests__/reasoning.spec.ts b/src/api/transform/__tests__/reasoning.spec.ts index fc0983d74169..59cc02836614 100644 --- a/src/api/transform/__tests__/reasoning.spec.ts +++ b/src/api/transform/__tests__/reasoning.spec.ts @@ -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", () => { diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index 100b1c268464..7a44d2a149d6 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -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,