Skip to content

Commit fdc8085

Browse files
committed
Add reasoning_effort to openai compatible provider for grok-mini
1 parent 255a158 commit fdc8085

File tree

7 files changed

+201
-1
lines changed

7 files changed

+201
-1
lines changed

src/api/providers/__tests__/openai.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,136 @@ describe("OpenAiHandler", () => {
392392
expect(lastCall[0]).not.toHaveProperty("stream_options")
393393
})
394394
})
395+
396+
describe("Grok 3 Mini models with reasoning", () => {
397+
const grokMiniOptions = {
398+
...mockOptions,
399+
openAiBaseUrl: "https://api.x.ai/v1",
400+
openAiModelId: "grok-3-mini-beta",
401+
openAiCustomModelInfo: {
402+
reasoningEffort: "low" as const,
403+
thinking: true,
404+
contextWindow: 128_000,
405+
supportsPromptCache: false,
406+
maxTokens: -1,
407+
supportsImages: true,
408+
inputPrice: 0,
409+
outputPrice: 0,
410+
},
411+
}
412+
it("should include reasoning_effort parameter for Grok mini models", async () => {
413+
const grokHandler = new OpenAiHandler(grokMiniOptions)
414+
const systemPrompt = "You are a helpful assistant."
415+
const messages: Anthropic.Messages.MessageParam[] = [
416+
{
417+
role: "user",
418+
content: "Hello!",
419+
},
420+
]
421+
422+
const stream = grokHandler.createMessage(systemPrompt, messages)
423+
await stream.next()
424+
425+
expect(mockCreate).toHaveBeenCalledWith(
426+
expect.objectContaining({
427+
model: grokMiniOptions.openAiModelId,
428+
stream: true,
429+
reasoning_effort: "low",
430+
}),
431+
{},
432+
)
433+
})
434+
435+
it("should use the specified reasoningEffort value", async () => {
436+
const grokHandler = new OpenAiHandler({
437+
...grokMiniOptions,
438+
openAiCustomModelInfo: {
439+
...grokMiniOptions.openAiCustomModelInfo,
440+
reasoningEffort: "high",
441+
},
442+
})
443+
const systemPrompt = "You are a helpful assistant."
444+
const messages: Anthropic.Messages.MessageParam[] = [
445+
{
446+
role: "user",
447+
content: "Hello!",
448+
},
449+
]
450+
451+
const stream = grokHandler.createMessage(systemPrompt, messages)
452+
await stream.next()
453+
454+
expect(mockCreate).toHaveBeenCalledWith(
455+
expect.objectContaining({
456+
model: grokMiniOptions.openAiModelId,
457+
stream: true,
458+
reasoning_effort: "high",
459+
}),
460+
{},
461+
)
462+
})
463+
464+
it("should process reasoning_content from response", async () => {
465+
// Update the mock to include reasoning_content in the response
466+
mockCreate.mockImplementationOnce(() => ({
467+
[Symbol.asyncIterator]: async function* () {
468+
yield {
469+
choices: [
470+
{
471+
delta: { content: "Test response" },
472+
index: 0,
473+
},
474+
],
475+
usage: null,
476+
}
477+
yield {
478+
choices: [
479+
{
480+
delta: { reasoning_content: "This is reasoning content" },
481+
index: 0,
482+
},
483+
],
484+
usage: null,
485+
}
486+
yield {
487+
choices: [
488+
{
489+
delta: {},
490+
index: 0,
491+
},
492+
],
493+
usage: {
494+
prompt_tokens: 10,
495+
completion_tokens: 5,
496+
total_tokens: 15,
497+
},
498+
}
499+
},
500+
}))
501+
502+
const grokHandler = new OpenAiHandler(grokMiniOptions)
503+
const systemPrompt = "You are a helpful assistant."
504+
const messages: Anthropic.Messages.MessageParam[] = [
505+
{
506+
role: "user",
507+
content: "Hello!",
508+
},
509+
]
510+
511+
const stream = grokHandler.createMessage(systemPrompt, messages)
512+
const chunks: any[] = []
513+
for await (const chunk of stream) {
514+
chunks.push(chunk)
515+
}
516+
517+
const textChunks = chunks.filter((chunk) => chunk.type === "text")
518+
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
519+
520+
expect(textChunks).toHaveLength(1)
521+
expect(textChunks[0].text).toBe("Test response")
522+
523+
expect(reasoningChunks).toHaveLength(1)
524+
expect(reasoningChunks[0].text).toBe("This is reasoning content")
525+
})
526+
})
395527
})

src/api/providers/openai.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,16 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
138138
}
139139

140140
const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)
141+
const isGrokMiniModel = modelId.includes("grok-3-mini")
142+
const useGrokReasoning = modelInfo.thinking || (isGrokXAI && isGrokMiniModel)
141143

142144
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
143145
model: modelId,
144146
temperature: this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
145147
messages: convertedMessages,
146148
stream: true as const,
147149
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
150+
...(useGrokReasoning ? { reasoning_effort: modelInfo.reasoningEffort || "low" } : {}),
148151
}
149152
if (this.options.includeMaxTokens) {
150153
requestOptions.max_tokens = modelInfo.maxTokens
@@ -267,7 +270,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
267270
if (this.options.openAiStreamingEnabled ?? true) {
268271
const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl)
269272

273+
const modelInfo = this.getModel().info
270274
const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)
275+
const isGrokMiniModel = modelId.includes("grok-3-mini")
276+
const useGrokReasoning = modelInfo.thinking || (isGrokXAI && isGrokMiniModel)
271277

272278
const stream = await this.client.chat.completions.create(
273279
{
@@ -281,7 +287,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
281287
],
282288
stream: true,
283289
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
284-
reasoning_effort: this.getModel().info.reasoningEffort,
290+
...(useGrokReasoning ? { reasoning_effort: this.getModel().info.reasoningEffort || "low" } : {}),
285291
},
286292
methodIsAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {},
287293
)

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ export type ExtensionState = Pick<
199199
telemetrySetting: TelemetrySetting
200200
telemetryKey?: string
201201
machineId?: string
202+
reasoningEffort?: "low" | "medium" | "high" // The reasoning effort level for models that support reasoning
203+
grokReasoningEffort?: "low" | "high" // The reasoning effort level for Grok 3 Mini models
202204

203205
renderContext: "sidebar" | "editor"
204206
settingsImportedAt?: number

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export interface WebviewMessage {
120120
| "maxReadFileLine"
121121
| "searchFiles"
122122
| "toggleApiConfigPin"
123+
| "reasoningEffort"
123124
text?: string
124125
disabled?: boolean
125126
askResponse?: ClineAskResponse

src/shared/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
622622
supportsPromptCache: false,
623623
inputPrice: 0,
624624
outputPrice: 0,
625+
reasoningEffort: "low",
625626
}
626627

627628
// Gemini

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LanguageModelChatSelector } from "vscode"
77
import { Checkbox } from "vscrui"
88
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
99
import { ExternalLinkIcon } from "@radix-ui/react-icons"
10+
import { GrokReasoningSettings } from "./GrokReasoningSettings"
1011

1112
import {
1213
ApiConfiguration,
@@ -129,6 +130,16 @@ const ApiOptions = ({
129130
[apiConfiguration],
130131
)
131132

133+
// Check if the current model is a Grok 3 Mini model
134+
const isGrokMiniModel = useMemo(() => {
135+
return selectedModelId?.includes("grok-3-mini-beta") || selectedModelId?.includes("grok-3-mini-fast-beta")
136+
}, [selectedModelId])
137+
138+
// Check if the endpoint is x.ai
139+
const isXaiEndpoint = useMemo(() => {
140+
return apiConfiguration?.openAiBaseUrl?.includes("x.ai") || false
141+
}, [apiConfiguration?.openAiBaseUrl])
142+
132143
// Debounced refresh model updates, only executed 250ms after the user
133144
// stops typing.
134145
useDebounce(
@@ -800,6 +811,24 @@ const ApiOptions = ({
800811
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
801812
{t("settings:modelInfo.enableStreaming")}
802813
</Checkbox>
814+
815+
{/* Grok3 Reasoning Settings - Only show for Grok Mini models */}
816+
{isGrokMiniModel && isXaiEndpoint && (
817+
<GrokReasoningSettings
818+
reasoningEffort={
819+
(apiConfiguration?.openAiCustomModelInfo?.reasoningEffort === "medium"
820+
? "low"
821+
: apiConfiguration?.openAiCustomModelInfo?.reasoningEffort) ?? "low"
822+
}
823+
setReasoningEffort={(value) => {
824+
setApiConfigurationField("openAiCustomModelInfo", {
825+
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
826+
reasoningEffort: value,
827+
})
828+
}}
829+
/>
830+
)}
831+
803832
<Checkbox
804833
checked={apiConfiguration?.openAiUseAzure ?? false}
805834
onChange={handleInputChange("openAiUseAzure", noTransform)}>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react"
2+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
3+
4+
interface GrokReasoningSettingsProps {
5+
reasoningEffort: "low" | "high"
6+
setReasoningEffort: (value: "low" | "high") => void
7+
}
8+
9+
export const GrokReasoningSettings: React.FC<GrokReasoningSettingsProps> = ({
10+
reasoningEffort,
11+
setReasoningEffort,
12+
}) => {
13+
return (
14+
<div className="flex flex-col gap-3">
15+
<div>
16+
<label className="block font-medium mb-1">Reasoning Effort</label>
17+
<Select value={reasoningEffort} onValueChange={(value) => setReasoningEffort(value as "low" | "high")}>
18+
<SelectTrigger className="w-full">
19+
<SelectValue placeholder="Select reasoning effort" />
20+
</SelectTrigger>
21+
<SelectContent>
22+
<SelectItem value="low">Low (faster responses, less thinking)</SelectItem>
23+
<SelectItem value="high">High (more thorough thinking, slower responses)</SelectItem>
24+
</SelectContent>
25+
</Select>
26+
</div>
27+
</div>
28+
)
29+
}

0 commit comments

Comments
 (0)