Skip to content

Commit 5d78b4c

Browse files
committed
feat(ui): Add custom provider support to API settings UI
- Import and integrate CustomProvidersSection component - Add 'Custom' option to API provider dropdown - Implement custom provider rendering and state management - Add custom provider configuration handling
1 parent c14de4f commit 5d78b4c

File tree

1 file changed

+133
-2
lines changed

1 file changed

+133
-2
lines changed

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

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Checkbox, Dropdown, Pane } from "vscrui"
22
import type { DropdownOption } from "vscrui"
33
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
4+
import CustomProvidersSection from "./CustomProvidersSection"
45
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
56
import { useEvent, useInterval } from "react-use"
67
import {
78
ApiConfiguration,
89
ModelInfo,
10+
CustomProviderConfig,
911
anthropicDefaultModelId,
1012
anthropicModels,
1113
azureOpenAiDefaultApiVersion,
@@ -153,6 +155,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
153155
{ value: "lmstudio", label: "LM Studio" },
154156
{ value: "ollama", label: "Ollama" },
155157
{ value: "unbound", label: "Unbound" },
158+
{ value: "custom", label: "Custom" },
156159
]}
157160
/>
158161
</div>
@@ -1288,14 +1291,98 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
12881291
</p>
12891292
</div>
12901293
)}
1294+
{selectedProvider === "custom" && (
1295+
<div>
1296+
<CustomProvidersSection
1297+
apiConfiguration={apiConfiguration || {}}
1298+
providers={apiConfiguration?.customProviders || {}}
1299+
activeProvider={apiConfiguration?.activeCustomProvider}
1300+
onAddProvider={(provider: CustomProviderConfig) => {
1301+
if (!apiConfiguration) return
1302+
// Update customProviders
1303+
handleInputChange("customProviders")({
1304+
target: {
1305+
value: {
1306+
...(apiConfiguration.customProviders || {}),
1307+
[provider.name]: provider,
1308+
},
1309+
},
1310+
})
1311+
// Set active provider
1312+
handleInputChange("activeCustomProvider")({
1313+
target: { value: provider.name },
1314+
})
1315+
// Set current provider
1316+
handleInputChange("customProvider")({
1317+
target: { value: provider },
1318+
})
1319+
}}
1320+
onDeleteProvider={(name) => {
1321+
if (!apiConfiguration?.customProviders) return
1322+
const newProviders = { ...apiConfiguration.customProviders }
1323+
delete newProviders[name]
12911324

1325+
// Update customProviders
1326+
handleInputChange("customProviders")({
1327+
target: { value: newProviders },
1328+
})
1329+
1330+
// Clear active provider if it was the deleted one
1331+
if (apiConfiguration.activeCustomProvider === name) {
1332+
handleInputChange("activeCustomProvider")({
1333+
target: { value: "" },
1334+
})
1335+
handleInputChange("customProvider")({
1336+
target: { value: undefined },
1337+
})
1338+
}
1339+
}}
1340+
onSelectProvider={(name) => {
1341+
if (!apiConfiguration?.customProviders?.[name]) return
1342+
1343+
// Set active provider
1344+
handleInputChange("activeCustomProvider")({
1345+
target: { value: name },
1346+
})
1347+
// Set current provider
1348+
handleInputChange("customProvider")({
1349+
target: { value: apiConfiguration.customProviders[name] },
1350+
})
1351+
}}
1352+
handleInputChange={(field) => (event) => {
1353+
const value = event.target.value
1354+
1355+
// Handle setting the active custom provider
1356+
if (field === "activeCustomProvider") {
1357+
handleInputChange("activeCustomProvider")({
1358+
target: { value },
1359+
})
1360+
// Also set the customProvider if it exists
1361+
if (value && apiConfiguration?.customProviders?.[value]) {
1362+
handleInputChange("customProvider")({
1363+
target: { value: apiConfiguration.customProviders[value] },
1364+
})
1365+
}
1366+
} else if (field === "customProvider") {
1367+
handleInputChange("customProvider")({
1368+
target: { value },
1369+
})
1370+
}
1371+
}}
1372+
isDescriptionExpanded={false}
1373+
setIsDescriptionExpanded={() => {}}
1374+
modelIdErrorMessage={modelIdErrorMessage}
1375+
apiErrorMessage={apiErrorMessage}
1376+
/>
1377+
</div>
1378+
)}
12921379
{selectedProvider === "unbound" && (
12931380
<div>
12941381
<VSCodeTextField
12951382
value={apiConfiguration?.unboundApiKey || ""}
12961383
style={{ width: "100%" }}
12971384
type="password"
1298-
onChange={handleInputChange("unboundApiKey")}
1385+
onInput={handleInputChange("unboundApiKey")}
12991386
placeholder="Enter API Key...">
13001387
<span style={{ fontWeight: 500 }}>Unbound API Key</span>
13011388
</VSCodeTextField>
@@ -1332,7 +1419,6 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
13321419
{selectedProvider === "glama" && <GlamaModelPicker />}
13331420

13341421
{selectedProvider === "openrouter" && <OpenRouterModelPicker />}
1335-
13361422
{selectedProvider !== "glama" &&
13371423
selectedProvider !== "openrouter" &&
13381424
selectedProvider !== "openai" &&
@@ -1520,6 +1606,17 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
15201606
const provider = apiConfiguration?.apiProvider || "anthropic"
15211607
const modelId = apiConfiguration?.apiModelId
15221608

1609+
const baseModelInfo: ModelInfo = {
1610+
maxTokens: -1,
1611+
contextWindow: 128000,
1612+
supportsImages: false,
1613+
supportsPromptCache: false,
1614+
supportsComputerUse: false,
1615+
inputPrice: 0,
1616+
outputPrice: 0,
1617+
description: "",
1618+
}
1619+
15231620
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
15241621
let selectedModelId: string
15251622
let selectedModelInfo: ModelInfo
@@ -1532,6 +1629,40 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
15321629
}
15331630
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
15341631
}
1632+
1633+
// Handle custom provider first to keep it separate from built-in providers
1634+
if (provider === "custom") {
1635+
const customProvider = apiConfiguration?.customProvider
1636+
const activeCustomProvider = apiConfiguration?.activeCustomProvider
1637+
1638+
if (!customProvider || !activeCustomProvider) {
1639+
return {
1640+
selectedProvider: provider,
1641+
selectedModelId: "",
1642+
selectedModelInfo: {
1643+
...baseModelInfo,
1644+
description: "No custom provider selected",
1645+
},
1646+
}
1647+
}
1648+
1649+
return {
1650+
selectedProvider: provider,
1651+
selectedModelId: activeCustomProvider,
1652+
selectedModelInfo: {
1653+
...baseModelInfo,
1654+
maxTokens: customProvider.maxTokens ?? -1,
1655+
contextWindow: customProvider.contextWindow ?? 128000,
1656+
supportsImages: customProvider.supportsImages ?? false,
1657+
supportsComputerUse: customProvider.supportsComputerUse ?? false,
1658+
description: customProvider.name
1659+
? `Custom provider: ${customProvider.name}`
1660+
: "Unnamed custom provider",
1661+
},
1662+
}
1663+
}
1664+
1665+
// Handle other providers
15351666
switch (provider) {
15361667
case "anthropic":
15371668
return getProviderData(anthropicModels, anthropicDefaultModelId)

0 commit comments

Comments
 (0)