fix api config profile

This commit is contained in:
sam hoang
2025-01-15 21:04:02 +07:00
parent ef8d02dfe5
commit 40fd397407
7 changed files with 193 additions and 125 deletions

View File

@@ -956,10 +956,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.configManager.SaveConfig(message.text, message.apiConfiguration); await this.configManager.SaveConfig(message.text, message.apiConfiguration);
let listApiConfig = await this.configManager.ListConfig(); let listApiConfig = await this.configManager.ListConfig();
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([ await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateApiConfiguration(message.apiConfiguration), this.updateApiConfiguration(message.apiConfiguration),
this.updateGlobalState("currentApiConfigName", message.text), this.updateGlobalState("currentApiConfigName", message.text),
]) ])
@@ -999,14 +997,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "loadApiConfiguration": case "loadApiConfiguration":
if (message.text) { if (message.text) {
try { try {
console.log("loadApiConfiguration", message.text)
const apiConfig = await this.configManager.LoadConfig(message.text); const apiConfig = await this.configManager.LoadConfig(message.text);
const listApiConfig = await this.configManager.ListConfig(); const listApiConfig = await this.configManager.ListConfig();
const config = listApiConfig?.find(c => c.name === message.text);
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([ await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateGlobalState("currentApiConfigName", message.text), this.updateGlobalState("currentApiConfigName", message.text),
this.updateApiConfiguration(apiConfig), this.updateApiConfiguration(apiConfig),
]) ])

View File

@@ -31,6 +31,7 @@
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"styled-components": "^6.1.13", "styled-components": "^6.1.13",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vscrui": "^0.2.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -15155,6 +15156,20 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/vscrui": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/vscrui/-/vscrui-0.2.0.tgz",
"integrity": "sha512-fvxZM/uIYOMN3fUbE2In+R1VrNj8PKcfAdh+Us2bJaPGuG9ySkR6xkV2aJVqXxWDX77U3v/UQGc5e7URrB52Gw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/estruyf"
},
"peerDependencies": {
"@types/react": "*",
"react": "^17 || ^18"
}
},
"node_modules/w3c-hr-time": { "node_modules/w3c-hr-time": {
"version": "1.0.2", "version": "1.0.2",
"license": "MIT", "license": "MIT",

View File

@@ -26,6 +26,7 @@
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"styled-components": "^6.1.13", "styled-components": "^6.1.13",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vscrui": "^0.2.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@@ -1,8 +1,7 @@
import { Checkbox, Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeLink, VSCodeLink,
VSCodeOption,
VSCodeRadio, VSCodeRadio,
VSCodeRadioGroup, VSCodeRadioGroup,
VSCodeTextField, VSCodeTextField,
@@ -90,35 +89,26 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
}, []) }, [])
useEvent("message", handleMessage) useEvent("message", handleMessage)
/*
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
const createDropdown = (models: Record<string, ModelInfo>) => { const createDropdown = (models: Record<string, ModelInfo>) => {
const options: DropdownOption[] = [
{ value: "", label: "Select a model..." },
...Object.keys(models).map((modelId) => ({
value: modelId,
label: modelId,
}))
]
return ( return (
<VSCodeDropdown <Dropdown
id="model-id" id="model-id"
value={selectedModelId} value={selectedModelId}
onChange={handleInputChange("apiModelId")} onChange={(value: unknown) => {handleInputChange("apiModelId")({
style={{ width: "100%" }}> target: {
<VSCodeOption value="">Select a model...</VSCodeOption> value: (value as DropdownOption).value
{Object.keys(models).map((modelId) => ( }
<VSCodeOption })}}
key={modelId} style={{ width: "100%" }}
value={modelId} options={options}
style={{ />
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
) )
} }
@@ -128,23 +118,31 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="api-provider"> <label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span> <span style={{ fontWeight: 500 }}>API Provider</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="api-provider" id="api-provider"
value={selectedProvider} value={selectedProvider}
onChange={handleInputChange("apiProvider")} onChange={(value: unknown) => {
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}> handleInputChange("apiProvider")({
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption> target: {
<VSCodeOption value="anthropic">Anthropic</VSCodeOption> value: (value as DropdownOption).value
<VSCodeOption value="gemini">Google Gemini</VSCodeOption> }
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption> })
<VSCodeOption value="openai-native">OpenAI</VSCodeOption> }}
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption> style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption> options={[
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption> { value: "openrouter", label: "OpenRouter" },
<VSCodeOption value="glama">Glama</VSCodeOption> { value: "anthropic", label: "Anthropic" },
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption> { value: "gemini", label: "Google Gemini" },
<VSCodeOption value="ollama">Ollama</VSCodeOption> { value: "deepseek", label: "DeepSeek" },
</VSCodeDropdown> { value: "openai-native", label: "OpenAI" },
{ value: "openai", label: "OpenAI Compatible" },
{ value: "vertex", label: "GCP Vertex AI" },
{ value: "bedrock", label: "AWS Bedrock" },
{ value: "glama", label: "Glama" },
{ value: "lmstudio", label: "LM Studio" },
{ value: "ollama", label: "Ollama" }
]}
/>
</div> </div>
{selectedProvider === "anthropic" && ( {selectedProvider === "anthropic" && (
@@ -158,17 +156,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<span style={{ fontWeight: 500 }}>Anthropic API Key</span> <span style={{ fontWeight: 500 }}>Anthropic API Key</span>
</VSCodeTextField> </VSCodeTextField>
<VSCodeCheckbox <Checkbox
checked={anthropicBaseUrlSelected} checked={anthropicBaseUrlSelected}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true setAnthropicBaseUrlSelected(checked)
setAnthropicBaseUrlSelected(isChecked) if (!checked) {
if (!isChecked) {
setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" }) setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
} }
}}> }}>
Use custom base URL Use custom base URL
</VSCodeCheckbox> </Checkbox>
{anthropicBaseUrlSelected && ( {anthropicBaseUrlSelected && (
<VSCodeTextField <VSCodeTextField
@@ -286,15 +283,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
</span> </span>
)} */} )} */}
</p> </p>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.openRouterUseMiddleOutTransform || false} checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true handleInputChange("openRouterUseMiddleOutTransform")({
setApiConfiguration({ ...apiConfiguration, openRouterUseMiddleOutTransform: isChecked }) target: { value: checked },
})
}}> }}>
Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>) Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
</VSCodeCheckbox> </Checkbox>
<br/> <br />
</div> </div>
)} )}
@@ -328,45 +326,44 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="aws-region-dropdown"> <label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span> <span style={{ fontWeight: 500 }}>AWS Region</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="aws-region-dropdown" id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""} value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }} style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}> onChange={(value: unknown) => {handleInputChange("awsRegion")({
<VSCodeOption value="">Select a region...</VSCodeOption> target: {
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */} value: (value as DropdownOption).value
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption> }
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption> })}}
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */} options={[
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption> { value: "", label: "Select a region..." },
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */} { value: "us-east-1", label: "us-east-1" },
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */} { value: "us-east-2", label: "us-east-2" },
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption> { value: "us-west-2", label: "us-west-2" },
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption> { value: "ap-south-1", label: "ap-south-1" },
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption> { value: "ap-northeast-1", label: "ap-northeast-1" },
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */} { value: "ap-northeast-2", label: "ap-northeast-2" },
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption> { value: "ap-southeast-1", label: "ap-southeast-1" },
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption> { value: "ap-southeast-2", label: "ap-southeast-2" },
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption> { value: "ca-central-1", label: "ca-central-1" },
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption> { value: "eu-central-1", label: "eu-central-1" },
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption> { value: "eu-west-1", label: "eu-west-1" },
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption> { value: "eu-west-2", label: "eu-west-2" },
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption> { value: "eu-west-3", label: "eu-west-3" },
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */} { value: "sa-east-1", label: "sa-east-1" },
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */} { value: "us-gov-west-1", label: "us-gov-west-1" }
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption> ]}
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption> />
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</div> </div>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.awsUseCrossRegionInference || false} checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true handleInputChange("awsUseCrossRegionInference")({
setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked }) target: { value: checked },
})
}}> }}>
Use cross-region inference Use cross-region inference
</VSCodeCheckbox> </Checkbox>
<p <p
style={{ style={{
fontSize: "12px", fontSize: "12px",
@@ -393,18 +390,24 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="vertex-region-dropdown"> <label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>Google Cloud Region</span> <span style={{ fontWeight: 500 }}>Google Cloud Region</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="vertex-region-dropdown" id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""} value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }} style={{ width: "100%" }}
onChange={handleInputChange("vertexRegion")}> onChange={(value: unknown) => {handleInputChange("vertexRegion")({
<VSCodeOption value="">Select a region...</VSCodeOption> target: {
<VSCodeOption value="us-east5">us-east5</VSCodeOption> value: (value as DropdownOption).value
<VSCodeOption value="us-central1">us-central1</VSCodeOption> }
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption> })}}
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption> options={[
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption> { value: "", label: "Select a region..." },
</VSCodeDropdown> { value: "us-east5", label: "us-east5" },
{ value: "us-central1", label: "us-central1" },
{ value: "europe-west1", label: "europe-west1" },
{ value: "europe-west4", label: "europe-west4" },
{ value: "asia-southeast1", label: "asia-southeast1" }
]}
/>
</div> </div>
<p <p
style={{ style={{
@@ -477,29 +480,27 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
</VSCodeTextField> </VSCodeTextField>
<OpenAiModelPicker /> <OpenAiModelPicker />
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.openAiStreamingEnabled ?? true} checked={apiConfiguration?.openAiStreamingEnabled ?? true}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked console.log("isChecked", checked)
setApiConfiguration({ handleInputChange("openAiStreamingEnabled")({
...apiConfiguration, target: { value: checked },
openAiStreamingEnabled: isChecked
}) })
}}> }}>
Enable streaming Enable streaming
</VSCodeCheckbox> </Checkbox>
</div> </div>
<VSCodeCheckbox <Checkbox
checked={azureApiVersionSelected} checked={azureApiVersionSelected}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true setAzureApiVersionSelected(checked)
setAzureApiVersionSelected(isChecked) if (!checked) {
if (!isChecked) {
setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" }) setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
} }
}}> }}>
Set Azure API version Set Azure API version
</VSCodeCheckbox> </Checkbox>
{azureApiVersionSelected && ( {azureApiVersionSelected && (
<VSCodeTextField <VSCodeTextField
value={apiConfiguration?.azureApiVersion || ""} value={apiConfiguration?.azureApiVersion || ""}

View File

@@ -1,4 +1,5 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
@@ -44,8 +45,24 @@ const GlamaModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshGlamaModels" })
},
50
),
[]
)
useMount(() => { useMount(() => {
vscode.postMessage({ type: "refreshGlamaModels" }) debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
}
}) })
useEffect(() => { useEffect(() => {

View File

@@ -1,6 +1,7 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import debounce from "debounce"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
import styled from "styled-components" import styled from "styled-components"
import { useExtensionState } from "../../context/ExtensionStateContext" import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -34,18 +35,38 @@ const OpenAiModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
const debouncedRefreshModels = useMemo(
() =>
debounce(
(baseUrl: string, apiKey: string) => {
vscode.postMessage({
type: "refreshOpenAiModels",
values: {
baseUrl,
apiKey
}
})
},
50
),
[]
)
useEffect(() => { useEffect(() => {
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) { if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
return return
} }
vscode.postMessage({ debouncedRefreshModels(
type: "refreshOpenAiModels", values: { apiConfiguration.openAiBaseUrl,
baseUrl: apiConfiguration?.openAiBaseUrl, apiConfiguration.openAiApiKey
apiKey: apiConfiguration?.openAiApiKey )
}
}) // Cleanup debounced function
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey]) return () => {
debouncedRefreshModels.clear()
}
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {

View File

@@ -1,4 +1,5 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
@@ -43,8 +44,24 @@ const OpenRouterModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshOpenRouterModels" })
},
50
),
[]
)
useMount(() => { useMount(() => {
vscode.postMessage({ type: "refreshOpenRouterModels" }) debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
}
}) })
useEffect(() => { useEffect(() => {