Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const openAiSchema = baseProviderSettingsSchema.extend({
openAiBaseUrl: z.string().optional(),
openAiApiKey: z.string().optional(),
openAiR1FormatEnabled: z.boolean().optional(),
openAiThinkingModeEnabled: z.boolean().optional(),
openAiModelId: z.string().optional(),
openAiCustomModelInfo: modelInfoSchema.nullish(),
openAiUseAzure: z.boolean().optional(),
Expand Down
84 changes: 84 additions & 0 deletions src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,90 @@ describe("OpenAiHandler", () => {
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.max_completion_tokens).toBe(4096)
})

describe("thinking mode", () => {
it("should include thinking parameter when openAiThinkingModeEnabled is true (streaming)", async () => {
const thinkingOptions: ApiHandlerOptions = {
...mockOptions,
openAiThinkingModeEnabled: true,
}
const thinkingHandler = new OpenAiHandler(thinkingOptions)
const stream = thinkingHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with thinking parameter
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toEqual({ type: "enabled" })
})

it("should not include thinking parameter when openAiThinkingModeEnabled is false (streaming)", async () => {
const noThinkingOptions: ApiHandlerOptions = {
...mockOptions,
openAiThinkingModeEnabled: false,
}
const noThinkingHandler = new OpenAiHandler(noThinkingOptions)
const stream = noThinkingHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called without thinking parameter
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toBeUndefined()
})

it("should not include thinking parameter when openAiThinkingModeEnabled is undefined (streaming)", async () => {
const defaultOptions: ApiHandlerOptions = {
...mockOptions,
// openAiThinkingModeEnabled is not set
}
const defaultHandler = new OpenAiHandler(defaultOptions)
const stream = defaultHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called without thinking parameter
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toBeUndefined()
})

it("should include thinking parameter when openAiThinkingModeEnabled is true (non-streaming)", async () => {
const thinkingOptions: ApiHandlerOptions = {
...mockOptions,
openAiThinkingModeEnabled: true,
openAiStreamingEnabled: false,
}
const thinkingHandler = new OpenAiHandler(thinkingOptions)
const stream = thinkingHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with thinking parameter
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toEqual({ type: "enabled" })
})

it("should not include thinking parameter when openAiThinkingModeEnabled is false (non-streaming)", async () => {
const noThinkingOptions: ApiHandlerOptions = {
...mockOptions,
openAiThinkingModeEnabled: false,
openAiStreamingEnabled: false,
}
const noThinkingHandler = new OpenAiHandler(noThinkingOptions)
const stream = noThinkingHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called without thinking parameter
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toBeUndefined()
})
})
})

describe("error handling", () => {
Expand Down
11 changes: 9 additions & 2 deletions src/api/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
const modelUrl = this.options.openAiBaseUrl ?? ""
const modelId = this.options.openAiModelId ?? ""
const enabledR1Format = this.options.openAiR1FormatEnabled ?? false
const enabledThinkingMode = this.options.openAiThinkingModeEnabled ?? false
const isAzureAiInference = this._isAzureAiInference(modelUrl)
const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format

Expand Down Expand Up @@ -153,13 +154,16 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl

const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)

const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
thinking?: { type: "enabled"; budget_tokens?: number }
} = {
model: modelId,
temperature: this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
messages: convertedMessages,
stream: true as const,
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
...(reasoning && reasoning),
...(enabledThinkingMode && { thinking: { type: "enabled" } }),
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
...(metadata?.toolProtocol === "native" && {
Expand Down Expand Up @@ -224,11 +228,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
yield this.processUsageMetrics(lastUsage, modelInfo)
}
} else {
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming & {
thinking?: { type: "enabled"; budget_tokens?: number }
} = {
model: modelId,
messages: deepseekReasoner
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [systemMessage, ...convertToOpenAiMessages(messages)],
...(enabledThinkingMode && { thinking: { type: "enabled" } }),
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
...(metadata?.toolProtocol === "native" && {
Expand Down
25 changes: 25 additions & 0 deletions webview-ui/src/components/settings/ThinkingModeSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Checkbox } from "vscrui"

import { useAppTranslation } from "@/i18n/TranslationContext"

interface ThinkingModeSettingProps {
onChange: (value: boolean) => void
openAiThinkingModeEnabled?: boolean
}

export const ThinkingModeSetting = ({ onChange, openAiThinkingModeEnabled }: ThinkingModeSettingProps) => {
const { t } = useAppTranslation()

return (
<div>
<div className="flex items-center gap-2">
<Checkbox checked={openAiThinkingModeEnabled} onChange={onChange}>
<span className="font-medium">{t("settings:modelInfo.enableThinkingMode")}</span>
</Checkbox>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:modelInfo.enableThinkingModeTips")}
</p>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { convertHeadersToObject } from "../utils/headers"
import { inputEventTransform, noTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
import { R1FormatSetting } from "../R1FormatSetting"
import { ThinkingModeSetting } from "../ThinkingModeSetting"
import { ThinkingBudget } from "../ThinkingBudget"

type OpenAICompatibleProps = {
Expand Down Expand Up @@ -153,6 +154,10 @@ export const OpenAICompatible = ({
onChange={handleInputChange("openAiR1FormatEnabled", noTransform)}
openAiR1FormatEnabled={apiConfiguration?.openAiR1FormatEnabled ?? false}
/>
<ThinkingModeSetting
onChange={handleInputChange("openAiThinkingModeEnabled", noTransform)}
openAiThinkingModeEnabled={apiConfiguration?.openAiThinkingModeEnabled ?? false}
/>
<Checkbox
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ca/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/de/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,8 @@
"enableStreaming": "Enable streaming",
"enableR1Format": "Enable R1 model parameters",
"enableR1FormatTips": "Must be enabled when using R1 models such as QWQ to prevent 400 errors",
"enableThinkingMode": "Enable thinking mode",
"enableThinkingModeTips": "Enable for thinking models like Kimi K2, DeepSeek Reasoner, GLM-4 to return reasoning content",
"useAzure": "Use Azure",
"azureApiVersion": "Set Azure API version",
"gemini": {
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/es/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/fr/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/hi/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/id/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/it/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ja/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ko/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/nl/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/pl/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/pt-BR/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ru/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/tr/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/vi/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/zh-CN/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/zh-TW/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading