diff --git a/.changeset/thirty-eyes-appear.md b/.changeset/thirty-eyes-appear.md new file mode 100644 index 0000000000..2cfb8405d6 --- /dev/null +++ b/.changeset/thirty-eyes-appear.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Advanced Configuration for OpenAI Compatible Providers diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index c03b1d13ec..7309d5bb98 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -78,7 +78,7 @@ export class OpenAiHandler implements ApiHandler { getModel(): { id: string; info: ModelInfo } { return { id: this.options.openAiModelId ?? "", - info: openAiModelInfoSaneDefaults, + info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults, } } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1977dfaf99..87803eceed 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -66,6 +66,7 @@ type GlobalStateKey = | "taskHistory" | "openAiBaseUrl" | "openAiModelId" + | "openAiModelInfo" | "ollamaModelId" | "ollamaBaseUrl" | "lmStudioModelId" @@ -443,6 +444,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -482,6 +484,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl) await this.storeSecret("openAiApiKey", openAiApiKey) await this.updateGlobalState("openAiModelId", openAiModelId) + await this.updateGlobalState("openAiModelInfo", openAiModelInfo) await this.updateGlobalState("ollamaModelId", ollamaModelId) await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl) await this.updateGlobalState("lmStudioModelId", lmStudioModelId) @@ -561,6 +564,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "openai": await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId) + await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo) break case "ollama": await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId) @@ -592,6 +596,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "openai": await this.updateGlobalState("openAiModelId", newModelId) + await this.updateGlobalState("openAiModelInfo", newModelInfo) break case "ollama": await this.updateGlobalState("ollamaModelId", newModelId) @@ -1387,6 +1392,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1437,6 +1443,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("openAiBaseUrl") as Promise, this.getSecret("openAiApiKey") as Promise, this.getGlobalState("openAiModelId") as Promise, + this.getGlobalState("openAiModelInfo") as Promise, this.getGlobalState("ollamaModelId") as Promise, this.getGlobalState("ollamaBaseUrl") as Promise, this.getGlobalState("lmStudioModelId") as Promise, @@ -1508,6 +1515,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, diff --git a/src/shared/api.ts b/src/shared/api.ts index 0eee68b19e..c7f6d218d6 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -38,6 +38,7 @@ export interface ApiHandlerOptions { openAiBaseUrl?: string openAiApiKey?: string openAiModelId?: string + openAiModelInfo?: ModelInfo ollamaModelId?: string ollamaBaseUrl?: string lmStudioModelId?: string diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 92e44e2e4d..51780ea895 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -100,14 +100,20 @@ const TaskHeader: React.FC = ({ }, [task.text, windowWidth]) const isCostAvailable = useMemo(() => { + const openAiCompatHasPricing = + apiConfiguration?.apiProvider === "openai" && + apiConfiguration?.openAiModelInfo?.inputPrice && + apiConfiguration?.openAiModelInfo?.outputPrice + if (openAiCompatHasPricing) { + return true + } return ( - apiConfiguration?.apiProvider !== "openai" && apiConfiguration?.apiProvider !== "vscode-lm" && apiConfiguration?.apiProvider !== "ollama" && apiConfiguration?.apiProvider !== "lmstudio" && apiConfiguration?.apiProvider !== "gemini" ) - }, [apiConfiguration?.apiProvider]) + }, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo]) const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 1633e4abea..4f54cc0978 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -41,6 +41,7 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink" import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker" import styled from "styled-components" import * as vscodemodels from "vscode" +import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles" interface ApiOptionsProps { showModelOptions: boolean @@ -80,6 +81,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is const [vsCodeLmModels, setVsCodeLmModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) + const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { @@ -694,6 +696,127 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is placeholder={`Default: ${azureOpenAiDefaultApiVersion}`} /> )} +
setModelConfigurationSelected((val) => !val)}> + + + Model Configuration + +
+ {modelConfigurationSelected && ( + <> + { + const isChecked = e.target.checked === true + let modelInfo = apiConfiguration?.openAiModelInfo + ? apiConfiguration.openAiModelInfo + : { ...openAiModelInfoSaneDefaults } + modelInfo.supportsImages = isChecked + setApiConfiguration({ + ...apiConfiguration, + openAiModelInfo: modelInfo, + }) + }}> + Supports Images + +
+ { + let modelInfo = apiConfiguration?.openAiModelInfo + ? apiConfiguration.openAiModelInfo + : { ...openAiModelInfoSaneDefaults } + modelInfo.contextWindow = Number(input.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiModelInfo: modelInfo, + }) + }}> + Context Window Size + + { + let modelInfo = apiConfiguration?.openAiModelInfo + ? apiConfiguration.openAiModelInfo + : { ...openAiModelInfoSaneDefaults } + modelInfo.maxTokens = input.target.value + setApiConfiguration({ + ...apiConfiguration, + openAiModelInfo: modelInfo, + }) + }}> + Max Output Tokens + +
+
+ { + let modelInfo = apiConfiguration?.openAiModelInfo + ? apiConfiguration.openAiModelInfo + : { ...openAiModelInfoSaneDefaults } + modelInfo.inputPrice = input.target.value + setApiConfiguration({ + ...apiConfiguration, + openAiModelInfo: modelInfo, + }) + }}> + Input Price / 1M tokens + + { + let modelInfo = apiConfiguration?.openAiModelInfo + ? apiConfiguration.openAiModelInfo + : { ...openAiModelInfoSaneDefaults } + modelInfo.outputPrice = input.target.value + setApiConfiguration({ + ...apiConfiguration, + openAiModelInfo: modelInfo, + }) + }}> + Output Price / 1M tokens + +
+ + )}

{ expect(modelIdInput).toBeInTheDocument() }) }) + +vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + useExtensionState: vi.fn(() => ({ + apiConfiguration: { + apiProvider: "openai", + requestyApiKey: "", + requestyModelId: "", + }, + setApiConfiguration: vi.fn(), + uriScheme: "vscode", + })), + } +}) + +describe("OpenApiInfoOptions", () => { + const mockPostMessage = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + global.vscode = { postMessage: mockPostMessage } + }) + + it("renders OpenAI Supports Images input", () => { + render( + + + , + ) + const apiKeyInput = screen.getByText("Supports Images") + expect(apiKeyInput).toBeInTheDocument() + }) + + it("renders OpenAI Context Window Size input", () => { + render( + + + , + ) + const orgIdInput = screen.getByText("Context Window Size") + expect(orgIdInput).toBeInTheDocument() + }) + + it("renders OpenAI Max Output Tokens input", () => { + render( + + + , + ) + const modelInput = screen.getByText("Max Output Tokens") + expect(modelInput).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/utils/__tests__/hooks.spec.ts b/webview-ui/src/utils/__tests__/hooks.spec.ts index c613db72b3..2a1466f132 100644 --- a/webview-ui/src/utils/__tests__/hooks.spec.ts +++ b/webview-ui/src/utils/__tests__/hooks.spec.ts @@ -45,14 +45,14 @@ describe("useMetaKeyDetection", () => { // mock the detect functions const { result } = renderHook(() => useMetaKeyDetection("win32")) expect(result.current[0]).toBe("windows") - expect(result.current[1]).toBe("⊞ Win") + expect(result.current[1]).toBe("Win") }) it("should detect Mac OS and metaKey from platform", () => { // mock the detect functions const { result } = renderHook(() => useMetaKeyDetection("darwin")) expect(result.current[0]).toBe("mac") - expect(result.current[1]).toBe("⌘ Command") + expect(result.current[1]).toBe("CMD") }) it("should detect Linux OS and metaKey from platform", () => { diff --git a/webview-ui/src/utils/__tests__/platformUtils.spec.ts b/webview-ui/src/utils/__tests__/platformUtils.spec.ts index 9ec19ba3d0..2938280ab4 100644 --- a/webview-ui/src/utils/__tests__/platformUtils.spec.ts +++ b/webview-ui/src/utils/__tests__/platformUtils.spec.ts @@ -4,12 +4,12 @@ import { detectMetaKeyChar } from "../platformUtils" describe("detectMetaKeyChar", () => { it("should return ⌘ Command for darwin platform", () => { const result = detectMetaKeyChar("darwin") - expect(result).toBe("⌘ Command") + expect(result).toBe("CMD") }) it("should return ⊞ Win for win32 platform", () => { const result = detectMetaKeyChar("win32") - expect(result).toBe("⊞ Win") + expect(result).toBe("Win") }) it("should return Alt for linux platform", () => {