diff --git a/.changeset/odd-ligers-press.md b/.changeset/odd-ligers-press.md new file mode 100644 index 0000000000..c9942c972d --- /dev/null +++ b/.changeset/odd-ligers-press.md @@ -0,0 +1,5 @@ +--- +"roo-cline": minor +--- + +Add Reasoning Effort setting for OpenAI Compatible provider diff --git a/package-lock.json b/package-lock.json index 3aedb880bf..c66b610cf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "@types/jest": "^29.5.14", "@types/mocha": "^10.0.10", "@types/node": "20.x", + "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", "@types/string-similarity": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.14.1", @@ -9006,6 +9007,16 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/node-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/node-cache/-/node-cache-4.1.3.tgz", + "integrity": "sha512-3hsqnv3H1zkOhjygJaJUYmgz5+FcPO3vejBX7cE9/cnuINOJYrzkfOnUCvpwGe9kMZANIHJA7J5pOdeyv52OEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", diff --git a/package.json b/package.json index 4862683683..a78fec7cb7 100644 --- a/package.json +++ b/package.json @@ -469,6 +469,7 @@ "@types/jest": "^29.5.14", "@types/mocha": "^10.0.10", "@types/node": "20.x", + "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", "@types/string-similarity": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.14.1", diff --git a/src/api/providers/__tests__/openai.test.ts b/src/api/providers/__tests__/openai.test.ts index 17da968ae6..493c1e549f 100644 --- a/src/api/providers/__tests__/openai.test.ts +++ b/src/api/providers/__tests__/openai.test.ts @@ -1,3 +1,5 @@ +// npx jest src/api/providers/__tests__/openai.test.ts + import { OpenAiHandler } from "../openai" import { ApiHandlerOptions } from "../../../shared/api" import { Anthropic } from "@anthropic-ai/sdk" @@ -155,6 +157,39 @@ describe("OpenAiHandler", () => { expect(textChunks).toHaveLength(1) expect(textChunks[0].text).toBe("Test response") }) + it("should include reasoning_effort when reasoning effort is enabled", async () => { + const reasoningOptions: ApiHandlerOptions = { + ...mockOptions, + enableReasoningEffort: true, + openAiCustomModelInfo: { contextWindow: 128_000, supportsPromptCache: false, reasoningEffort: "high" }, + } + const reasoningHandler = new OpenAiHandler(reasoningOptions) + const stream = reasoningHandler.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with reasoning_effort + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("high") + }) + + it("should not include reasoning_effort when reasoning effort is disabled", async () => { + const noReasoningOptions: ApiHandlerOptions = { + ...mockOptions, + enableReasoningEffort: false, + openAiCustomModelInfo: { contextWindow: 128_000, supportsPromptCache: false }, + } + const noReasoningHandler = new OpenAiHandler(noReasoningOptions) + const stream = noReasoningHandler.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called without reasoning_effort + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBeUndefined() + }) }) describe("error handling", () => { diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index f2f7da5609..2c552b1702 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -87,6 +87,7 @@ type ProviderSettings = { openAiUseAzure?: boolean | undefined azureApiVersion?: string | undefined openAiStreamingEnabled?: boolean | undefined + enableReasoningEffort?: boolean | undefined ollamaModelId?: string | undefined ollamaBaseUrl?: string | undefined vsCodeLmModelSelector?: diff --git a/src/exports/types.ts b/src/exports/types.ts index 9582a21279..1072469509 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -88,6 +88,7 @@ type ProviderSettings = { openAiUseAzure?: boolean | undefined azureApiVersion?: string | undefined openAiStreamingEnabled?: boolean | undefined + enableReasoningEffort?: boolean | undefined ollamaModelId?: string | undefined ollamaBaseUrl?: string | undefined vsCodeLmModelSelector?: diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 6874783f48..a3ec338131 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -355,6 +355,7 @@ export const providerSettingsSchema = z.object({ openAiUseAzure: z.boolean().optional(), azureApiVersion: z.string().optional(), openAiStreamingEnabled: z.boolean().optional(), + enableReasoningEffort: z.boolean().optional(), // Ollama ollamaModelId: z.string().optional(), ollamaBaseUrl: z.string().optional(), @@ -453,6 +454,7 @@ const providerSettingsRecord: ProviderSettingsRecord = { openAiUseAzure: undefined, azureApiVersion: undefined, openAiStreamingEnabled: undefined, + enableReasoningEffort: undefined, // Ollama ollamaModelId: undefined, ollamaBaseUrl: undefined, diff --git a/src/utils/__tests__/enhance-prompt.test.ts b/src/utils/__tests__/enhance-prompt.test.ts index d3cca04c38..adda8860eb 100644 --- a/src/utils/__tests__/enhance-prompt.test.ts +++ b/src/utils/__tests__/enhance-prompt.test.ts @@ -13,6 +13,7 @@ describe("enhancePrompt", () => { apiProvider: "openai", openAiApiKey: "test-key", openAiBaseUrl: "https://api.openai.com/v1", + enableReasoningEffort: false, } beforeEach(() => { @@ -97,6 +98,7 @@ describe("enhancePrompt", () => { apiProvider: "openrouter", openRouterApiKey: "test-key", openRouterModelId: "test-model", + enableReasoningEffort: false, } // Mock successful enhancement diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 86e9defc5f..917947ac05 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,13 +1,12 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" -import { Trans } from "react-i18next" -import { getRequestyAuthUrl, getOpenRouterAuthUrl, getGlamaAuthUrl } from "@src/oauth/urls" import { useDebounce, useEvent } from "react-use" +import { Trans } from "react-i18next" import { LanguageModelChatSelector } from "vscode" import { Checkbox } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { ExternalLinkIcon } from "@radix-ui/react-icons" +import { ReasoningEffort as ReasoningEffortType } from "@roo/schemas" import { ApiConfiguration, ModelInfo, @@ -21,21 +20,22 @@ import { ApiProvider, } from "@roo/shared/api" import { ExtensionMessage } from "@roo/shared/ExtensionMessage" -import { AWS_REGIONS } from "@roo/shared/aws_regions" import { vscode } from "@src/utils/vscode" import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@src/utils/validate" -import { useRouterModels } from "@/components/ui/hooks/useRouterModels" -import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" +import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import { useOpenRouterModelProviders, OPENROUTER_DEFAULT_PROVIDER_NAME, } from "@src/components/ui/hooks/useOpenRouterModelProviders" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button } from "@src/components/ui" +import { getRequestyAuthUrl, getOpenRouterAuthUrl, getGlamaAuthUrl } from "@src/oauth/urls" import { VSCodeButtonLink } from "../common/VSCodeButtonLink" -import { MODELS_BY_PROVIDER, PROVIDERS, VERTEX_REGIONS, REASONING_MODELS } from "./constants" +import { MODELS_BY_PROVIDER, PROVIDERS, VERTEX_REGIONS, REASONING_MODELS, AWS_REGIONS } from "./constants" import { ModelInfoView } from "./ModelInfoView" import { ModelPicker } from "./ModelPicker" import { ApiErrorMessage } from "./ApiErrorMessage" @@ -851,6 +851,41 @@ const ApiOptions = ({ )} +
+ { + setApiConfigurationField("enableReasoningEffort", checked) + + if (!checked) { + const { reasoningEffort: _, ...openAiCustomModelInfo } = + apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults + + setApiConfigurationField("openAiCustomModelInfo", openAiCustomModelInfo) + } + }}> + {t("settings:providers.setReasoningLevel")} + + {!!apiConfiguration.enableReasoningEffort && ( + { + if (field === "reasoningEffort") { + const openAiCustomModelInfo = + apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults + + setApiConfigurationField("openAiCustomModelInfo", { + ...openAiCustomModelInfo, + reasoningEffort: value as ReasoningEffortType, + }) + } + }} + /> + )} +
{t("settings:providers.customModel.capabilities")} diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index d93b39f229..115c623a27 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -1,11 +1,12 @@ // npx jest src/components/settings/__tests__/ApiOptions.test.ts -import { render, screen } from "@testing-library/react" +import { render, screen, fireEvent } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ApiConfiguration } from "@roo/shared/api" import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" +import { openAiModelInfoSaneDefaults } from "@roo/shared/api" import ApiOptions, { ApiOptionsProps } from "../ApiOptions" @@ -26,8 +27,13 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({ // Mock other components jest.mock("vscrui", () => ({ Checkbox: ({ children, checked, onChange }: any) => ( -