Skip to content

Commit 0434b5c

Browse files
brownrw8ellipsis-dev[bot]saoudrizwan
authored
Advanced configuration for OpenAI Compatible Providers (RooCodeInc#1737)
* feat: advanced configuration for OpenAI Compatible Providers * Update .changeset/thirty-eyes-appear.md Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update webview-ui/src/components/settings/ApiOptions.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * dropdown menu * Show pricing if user entered model info --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Saoud Rizwan <[email protected]>
1 parent e534c3d commit 0434b5c

File tree

9 files changed

+207
-8
lines changed

9 files changed

+207
-8
lines changed

.changeset/thirty-eyes-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Advanced Configuration for OpenAI Compatible Providers

src/api/providers/openai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class OpenAiHandler implements ApiHandler {
7878
getModel(): { id: string; info: ModelInfo } {
7979
return {
8080
id: this.options.openAiModelId ?? "",
81-
info: openAiModelInfoSaneDefaults,
81+
info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults,
8282
}
8383
}
8484
}

src/core/webview/ClineProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type GlobalStateKey =
6666
| "taskHistory"
6767
| "openAiBaseUrl"
6868
| "openAiModelId"
69+
| "openAiModelInfo"
6970
| "ollamaModelId"
7071
| "ollamaBaseUrl"
7172
| "lmStudioModelId"
@@ -443,6 +444,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
443444
openAiBaseUrl,
444445
openAiApiKey,
445446
openAiModelId,
447+
openAiModelInfo,
446448
ollamaModelId,
447449
ollamaBaseUrl,
448450
lmStudioModelId,
@@ -482,6 +484,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
482484
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
483485
await this.storeSecret("openAiApiKey", openAiApiKey)
484486
await this.updateGlobalState("openAiModelId", openAiModelId)
487+
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
485488
await this.updateGlobalState("ollamaModelId", ollamaModelId)
486489
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
487490
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
@@ -561,6 +564,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
561564
break
562565
case "openai":
563566
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
567+
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
564568
break
565569
case "ollama":
566570
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
@@ -592,6 +596,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
592596
break
593597
case "openai":
594598
await this.updateGlobalState("openAiModelId", newModelId)
599+
await this.updateGlobalState("openAiModelInfo", newModelInfo)
595600
break
596601
case "ollama":
597602
await this.updateGlobalState("ollamaModelId", newModelId)
@@ -1387,6 +1392,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13871392
openAiBaseUrl,
13881393
openAiApiKey,
13891394
openAiModelId,
1395+
openAiModelInfo,
13901396
ollamaModelId,
13911397
ollamaBaseUrl,
13921398
lmStudioModelId,
@@ -1437,6 +1443,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14371443
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
14381444
this.getSecret("openAiApiKey") as Promise<string | undefined>,
14391445
this.getGlobalState("openAiModelId") as Promise<string | undefined>,
1446+
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
14401447
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
14411448
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
14421449
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
@@ -1508,6 +1515,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15081515
openAiBaseUrl,
15091516
openAiApiKey,
15101517
openAiModelId,
1518+
openAiModelInfo,
15111519
ollamaModelId,
15121520
ollamaBaseUrl,
15131521
lmStudioModelId,

src/shared/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface ApiHandlerOptions {
3838
openAiBaseUrl?: string
3939
openAiApiKey?: string
4040
openAiModelId?: string
41+
openAiModelInfo?: ModelInfo
4142
ollamaModelId?: string
4243
ollamaBaseUrl?: string
4344
lmStudioModelId?: string

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,20 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
100100
}, [task.text, windowWidth])
101101

102102
const isCostAvailable = useMemo(() => {
103+
const openAiCompatHasPricing =
104+
apiConfiguration?.apiProvider === "openai" &&
105+
apiConfiguration?.openAiModelInfo?.inputPrice &&
106+
apiConfiguration?.openAiModelInfo?.outputPrice
107+
if (openAiCompatHasPricing) {
108+
return true
109+
}
103110
return (
104-
apiConfiguration?.apiProvider !== "openai" &&
105111
apiConfiguration?.apiProvider !== "vscode-lm" &&
106112
apiConfiguration?.apiProvider !== "ollama" &&
107113
apiConfiguration?.apiProvider !== "lmstudio" &&
108114
apiConfiguration?.apiProvider !== "gemini"
109115
)
110-
}, [apiConfiguration?.apiProvider])
116+
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
111117

112118
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
113119

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink"
4141
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
4242
import styled from "styled-components"
4343
import * as vscodemodels from "vscode"
44+
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
4445

4546
interface ApiOptionsProps {
4647
showModelOptions: boolean
@@ -80,6 +81,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
8081
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
8182
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
8283
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
84+
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
8385
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
8486

8587
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
@@ -694,6 +696,127 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
694696
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
695697
/>
696698
)}
699+
<div
700+
style={{
701+
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
702+
display: "flex",
703+
margin: "10px 0",
704+
cursor: "pointer",
705+
alignItems: "center",
706+
}}
707+
onClick={() => setModelConfigurationSelected((val) => !val)}>
708+
<span
709+
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
710+
style={{
711+
marginRight: "4px",
712+
}}></span>
713+
<span
714+
style={{
715+
fontWeight: 700,
716+
textTransform: "uppercase",
717+
}}>
718+
Model Configuration
719+
</span>
720+
</div>
721+
{modelConfigurationSelected && (
722+
<>
723+
<VSCodeCheckbox
724+
checked={apiConfiguration?.openAiModelInfo?.supportsImages}
725+
onChange={(e: any) => {
726+
const isChecked = e.target.checked === true
727+
let modelInfo = apiConfiguration?.openAiModelInfo
728+
? apiConfiguration.openAiModelInfo
729+
: { ...openAiModelInfoSaneDefaults }
730+
modelInfo.supportsImages = isChecked
731+
setApiConfiguration({
732+
...apiConfiguration,
733+
openAiModelInfo: modelInfo,
734+
})
735+
}}>
736+
Supports Images
737+
</VSCodeCheckbox>
738+
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
739+
<VSCodeTextField
740+
value={
741+
apiConfiguration?.openAiModelInfo?.contextWindow
742+
? apiConfiguration.openAiModelInfo.contextWindow.toString()
743+
: openAiModelInfoSaneDefaults.contextWindow?.toString()
744+
}
745+
style={{ flex: 1 }}
746+
onInput={(input: any) => {
747+
let modelInfo = apiConfiguration?.openAiModelInfo
748+
? apiConfiguration.openAiModelInfo
749+
: { ...openAiModelInfoSaneDefaults }
750+
modelInfo.contextWindow = Number(input.target.value)
751+
setApiConfiguration({
752+
...apiConfiguration,
753+
openAiModelInfo: modelInfo,
754+
})
755+
}}>
756+
<span style={{ fontWeight: 500 }}>Context Window Size</span>
757+
</VSCodeTextField>
758+
<VSCodeTextField
759+
value={
760+
apiConfiguration?.openAiModelInfo?.maxTokens
761+
? apiConfiguration.openAiModelInfo.maxTokens.toString()
762+
: openAiModelInfoSaneDefaults.maxTokens?.toString()
763+
}
764+
style={{ flex: 1 }}
765+
onInput={(input: any) => {
766+
let modelInfo = apiConfiguration?.openAiModelInfo
767+
? apiConfiguration.openAiModelInfo
768+
: { ...openAiModelInfoSaneDefaults }
769+
modelInfo.maxTokens = input.target.value
770+
setApiConfiguration({
771+
...apiConfiguration,
772+
openAiModelInfo: modelInfo,
773+
})
774+
}}>
775+
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
776+
</VSCodeTextField>
777+
</div>
778+
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
779+
<VSCodeTextField
780+
value={
781+
apiConfiguration?.openAiModelInfo?.inputPrice
782+
? apiConfiguration.openAiModelInfo.inputPrice.toString()
783+
: openAiModelInfoSaneDefaults.inputPrice?.toString()
784+
}
785+
style={{ flex: 1 }}
786+
onInput={(input: any) => {
787+
let modelInfo = apiConfiguration?.openAiModelInfo
788+
? apiConfiguration.openAiModelInfo
789+
: { ...openAiModelInfoSaneDefaults }
790+
modelInfo.inputPrice = input.target.value
791+
setApiConfiguration({
792+
...apiConfiguration,
793+
openAiModelInfo: modelInfo,
794+
})
795+
}}>
796+
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
797+
</VSCodeTextField>
798+
<VSCodeTextField
799+
value={
800+
apiConfiguration?.openAiModelInfo?.outputPrice
801+
? apiConfiguration.openAiModelInfo.outputPrice.toString()
802+
: openAiModelInfoSaneDefaults.outputPrice?.toString()
803+
}
804+
style={{ flex: 1 }}
805+
onInput={(input: any) => {
806+
let modelInfo = apiConfiguration?.openAiModelInfo
807+
? apiConfiguration.openAiModelInfo
808+
: { ...openAiModelInfoSaneDefaults }
809+
modelInfo.outputPrice = input.target.value
810+
setApiConfiguration({
811+
...apiConfiguration,
812+
openAiModelInfo: modelInfo,
813+
})
814+
}}>
815+
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
816+
</VSCodeTextField>
817+
</div>
818+
</>
819+
)}
697820
<p
698821
style={{
699822
fontSize: "12px",

webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from "@testing-library/react"
1+
import { render, screen, fireEvent } from "@testing-library/react"
22
import { describe, it, expect, vi } from "vitest"
33
import ApiOptions from "../ApiOptions"
44
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
@@ -94,3 +94,59 @@ describe("ApiOptions Component", () => {
9494
expect(modelIdInput).toBeInTheDocument()
9595
})
9696
})
97+
98+
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
99+
const actual = await importOriginal()
100+
return {
101+
...actual,
102+
// your mocked methods
103+
useExtensionState: vi.fn(() => ({
104+
apiConfiguration: {
105+
apiProvider: "openai",
106+
requestyApiKey: "",
107+
requestyModelId: "",
108+
},
109+
setApiConfiguration: vi.fn(),
110+
uriScheme: "vscode",
111+
})),
112+
}
113+
})
114+
115+
describe("OpenApiInfoOptions", () => {
116+
const mockPostMessage = vi.fn()
117+
118+
beforeEach(() => {
119+
vi.clearAllMocks()
120+
global.vscode = { postMessage: mockPostMessage }
121+
})
122+
123+
it("renders OpenAI Supports Images input", () => {
124+
render(
125+
<ExtensionStateContextProvider>
126+
<ApiOptions showModelOptions={true} />
127+
</ExtensionStateContextProvider>,
128+
)
129+
const apiKeyInput = screen.getByText("Supports Images")
130+
expect(apiKeyInput).toBeInTheDocument()
131+
})
132+
133+
it("renders OpenAI Context Window Size input", () => {
134+
render(
135+
<ExtensionStateContextProvider>
136+
<ApiOptions showModelOptions={true} />
137+
</ExtensionStateContextProvider>,
138+
)
139+
const orgIdInput = screen.getByText("Context Window Size")
140+
expect(orgIdInput).toBeInTheDocument()
141+
})
142+
143+
it("renders OpenAI Max Output Tokens input", () => {
144+
render(
145+
<ExtensionStateContextProvider>
146+
<ApiOptions showModelOptions={true} />
147+
</ExtensionStateContextProvider>,
148+
)
149+
const modelInput = screen.getByText("Max Output Tokens")
150+
expect(modelInput).toBeInTheDocument()
151+
})
152+
})

webview-ui/src/utils/__tests__/hooks.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ describe("useMetaKeyDetection", () => {
4545
// mock the detect functions
4646
const { result } = renderHook(() => useMetaKeyDetection("win32"))
4747
expect(result.current[0]).toBe("windows")
48-
expect(result.current[1]).toBe("Win")
48+
expect(result.current[1]).toBe("Win")
4949
})
5050

5151
it("should detect Mac OS and metaKey from platform", () => {
5252
// mock the detect functions
5353
const { result } = renderHook(() => useMetaKeyDetection("darwin"))
5454
expect(result.current[0]).toBe("mac")
55-
expect(result.current[1]).toBe("⌘ Command")
55+
expect(result.current[1]).toBe("CMD")
5656
})
5757

5858
it("should detect Linux OS and metaKey from platform", () => {

webview-ui/src/utils/__tests__/platformUtils.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { detectMetaKeyChar } from "../platformUtils"
44
describe("detectMetaKeyChar", () => {
55
it("should return ⌘ Command for darwin platform", () => {
66
const result = detectMetaKeyChar("darwin")
7-
expect(result).toBe("⌘ Command")
7+
expect(result).toBe("CMD")
88
})
99

1010
it("should return ⊞ Win for win32 platform", () => {
1111
const result = detectMetaKeyChar("win32")
12-
expect(result).toBe("Win")
12+
expect(result).toBe("Win")
1313
})
1414

1515
it("should return Alt for linux platform", () => {

0 commit comments

Comments
 (0)