From d8406acfe27993f089f496f5500777356834535f Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 28 Apr 2025 13:25:49 -0700 Subject: [PATCH] OpenAI compatible reasoning effort --- package-lock.json | 11 + package.json | 1 + src/api/providers/__tests__/openai.test.ts | 35 ++++ src/exports/roo-code.d.ts | 1 + src/exports/types.ts | 1 + src/schemas/index.ts | 2 + .../src/components/settings/ApiOptions.tsx | 46 ++++- .../settings/__tests__/ApiOptions.test.tsx | 189 +++++++++++++++++- .../src/components/settings/constants.ts | 2 + webview-ui/src/i18n/locales/ca/settings.json | 3 +- webview-ui/src/i18n/locales/de/settings.json | 3 +- webview-ui/src/i18n/locales/en/settings.json | 3 +- webview-ui/src/i18n/locales/es/settings.json | 3 +- webview-ui/src/i18n/locales/fr/settings.json | 3 +- webview-ui/src/i18n/locales/hi/settings.json | 3 +- webview-ui/src/i18n/locales/it/settings.json | 3 +- webview-ui/src/i18n/locales/ja/settings.json | 3 +- webview-ui/src/i18n/locales/ko/settings.json | 3 +- webview-ui/src/i18n/locales/pl/settings.json | 3 +- .../src/i18n/locales/pt-BR/settings.json | 3 +- webview-ui/src/i18n/locales/ru/settings.json | 3 +- webview-ui/src/i18n/locales/tr/settings.json | 3 +- webview-ui/src/i18n/locales/vi/settings.json | 3 +- .../src/i18n/locales/zh-CN/settings.json | 3 +- .../src/i18n/locales/zh-TW/settings.json | 3 +- 25 files changed, 308 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3aedb880bf5..c66b610cf7c 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 4862683683a..a78fec7cb7b 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 17da968ae61..493c1e549f2 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 f2f7da56098..2c552b17021 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 9582a212796..10724695098 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 6874783f48d..a3ec3381313 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/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 86e9defc5f5..737a65931c0 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,38 @@ 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") { + setApiConfigurationField("openAiCustomModelInfo", { + ...(apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults), + 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 d93b39f2298..115c623a27d 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) => ( -