Merge pull request #1202 from RooVetGit/cte/fix-model-picker

Fix model picker
This commit is contained in:
Chris Estreich 2025-02-25 23:05:37 -08:00 committed by GitHub
commit bdc96a0681
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 664 additions and 621 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Fix model picker

View file

@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler {
}
}
export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
export async function getRequestyModels() {
const models: Record<string, ModelInfo> = {}
if (!apiKey) {
return models
}
try {
const config: Record<string, any> = {}
config["headers"] = { Authorization: `Bearer ${apiKey}` }
const response = await axios.get("https://router.requesty.ai/v1/models", config)
const response = await axios.get("https://router.requesty.ai/v1/models")
const rawModels = response.data.data
for (const rawModel of rawModels) {
// {
// id: "anthropic/claude-3-5-sonnet-20240620",
// object: "model",
// created: 1740552655,
// owned_by: "system",
// input_price: 0.0000028,
// caching_price: 0.00000375,
// cached_price: 3e-7,
// output_price: 0.000015,
// max_output_tokens: 8192,
// context_window: 200000,
// supports_caching: true,
// description:
// "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.",
// }
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_output_tokens,
contextWindow: rawModel.context_window,
supportsImages: rawModel.support_image,
supportsComputerUse: rawModel.support_computer_use,
supportsPromptCache: rawModel.supports_caching,
inputPrice: parseApiPrice(rawModel.input_price),
outputPrice: parseApiPrice(rawModel.output_price),
@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.supportsComputerUse = true
modelInfo.supportsImages = true
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"):
modelInfo.supportsComputerUse = true
modelInfo.supportsImages = true
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break

View file

@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
const requestyApiKey = await this.getSecret("requestyApiKey")
getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => {
getRequestyModels().then(async (requestyModels) => {
if (Object.keys(requestyModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "refreshRequestyModels":
if (message?.values?.apiKey) {
const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey })
const requestyModels = await getRequestyModels()
if (Object.keys(requestyModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
JSON.stringify(requestyModels),
)
await this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
if (Object.keys(requestyModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
JSON.stringify(requestyModels),
)
await this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
break

View file

@ -27,10 +27,11 @@ export interface ExtensionMessage {
| "workspaceUpdated"
| "invoke"
| "partialMessage"
| "glamaModels"
| "openRouterModels"
| "openAiModels"
| "glamaModels"
| "unboundModels"
| "requestyModels"
| "openAiModels"
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
@ -43,8 +44,6 @@ export interface ExtensionMessage {
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
| "unboundModels"
| "refreshUnboundModels"
| "currentCheckpointUpdated"
text?: string
action?:
@ -67,11 +66,11 @@ export interface ExtensionMessage {
path?: string
}>
partialMessage?: ClineMessage
glamaModels?: Record<string, ModelInfo>
requestyModels?: Record<string, ModelInfo>
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
glamaModels?: Record<string, ModelInfo>
unboundModels?: Record<string, ModelInfo>
requestyModels?: Record<string, ModelInfo>
openAiModels?: string[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]

View file

@ -40,11 +40,11 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
| "refreshGlamaModels"
| "refreshOpenRouterModels"
| "refreshOpenAiModels"
| "refreshGlamaModels"
| "refreshUnboundModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
@ -71,7 +71,6 @@ export interface WebviewMessage {
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
| "refreshGlamaModels"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"

View file

@ -3674,6 +3674,7 @@
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
@ -4719,6 +4720,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},

View file

@ -4,13 +4,13 @@ interface ApiErrorMessageProps {
errorMessage: string | undefined
children?: React.ReactNode
}
const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
return (
<div className="text-vscode-errorForeground text-sm">
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
{errorMessage}
{children}
export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => (
<div className="flex flex-col gap-2 text-vscode-errorForeground text-sm">
<div className="flex flex-row items-center gap-1">
<div className="codicon codicon-close" />
<div>{errorMessage}</div>
</div>
)
}
export default ApiErrorMessage
{children}
</div>
)

View file

@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import * as vscodemodels from "vscode"
import { Slider } from "@/components/ui"
import {
ApiConfiguration,
ModelInfo,
@ -33,7 +31,6 @@ import {
unboundDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
THINKING_BUDGET,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import ApiErrorMessage from "./ApiErrorMessage"
import { ApiErrorMessage } from "./ApiErrorMessage"
import { ThinkingBudget } from "./ThinkingBudget"
const modelsByProvider: Record<string, Record<string, ModelInfo>> = {
anthropic: anthropicModels,
bedrock: bedrockModels,
vertex: vertexModels,
gemini: geminiModels,
"openai-native": openAiNativeModels,
deepseek: deepSeekModels,
mistral: mistralModels,
}
interface ApiOptionsProps {
uriScheme: string | undefined
@ -66,18 +74,23 @@ const ApiOptions = ({
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
[glamaDefaultModelId]: glamaDefaultModelInfo,
})
const [unboundModels, setUnboundModels] = useState<Record<string, ModelInfo>>({
[unboundDefaultModelId]: unboundDefaultModelInfo,
})
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
@ -85,8 +98,6 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
const noTransform = <T,>(value: T) => value
const inputEventTransform = <E,>(event: E) => (event as { target: HTMLInputElement })?.target?.value as any
const dropdownEventTransform = <T,>(event: DropdownOption | string | undefined) =>
@ -103,62 +114,87 @@ const ApiOptions = ({
[setApiConfigurationField],
)
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
// Pull ollama/lmstudio models
// Debounced model updates, only executed 250ms after the user stops typing
// Debounced refresh model updates, only executed 250ms after the user
// stops typing.
useDebounce(
() => {
if (selectedProvider === "ollama") {
if (selectedProvider === "openrouter") {
vscode.postMessage({ type: "refreshOpenRouterModels" })
} else if (selectedProvider === "glama") {
vscode.postMessage({ type: "refreshGlamaModels" })
} else if (selectedProvider === "unbound") {
vscode.postMessage({ type: "refreshUnboundModels" })
} else if (selectedProvider === "requesty") {
vscode.postMessage({
type: "refreshRequestyModels",
values: { apiKey: apiConfiguration?.requestyApiKey },
})
} else if (selectedProvider === "openai") {
vscode.postMessage({
type: "refreshOpenAiModels",
values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey },
})
} else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
} else if (selectedProvider === "openai") {
vscode.postMessage({
type: "refreshOpenAiModels",
values: {
baseUrl: apiConfiguration?.openAiBaseUrl,
apiKey: apiConfiguration?.openAiApiKey,
},
})
} else if (selectedProvider === "openrouter") {
vscode.postMessage({ type: "refreshOpenRouterModels", values: {} })
} else if (selectedProvider === "glama") {
vscode.postMessage({ type: "refreshGlamaModels", values: {} })
} else if (selectedProvider === "requesty") {
vscode.postMessage({
type: "refreshRequestyModels",
values: {
apiKey: apiConfiguration?.requestyApiKey,
},
})
}
},
250,
[
selectedProvider,
apiConfiguration?.ollamaBaseUrl,
apiConfiguration?.lmStudioBaseUrl,
apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
apiConfiguration?.requestyApiKey,
apiConfiguration?.ollamaBaseUrl,
apiConfiguration?.lmStudioBaseUrl,
],
)
useEffect(() => {
const apiValidationResult =
validateApiConfiguration(apiConfiguration) ||
validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
setErrorMessage(apiValidationResult)
}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels)
const handleMessage = useCallback((event: MessageEvent) => {
setErrorMessage(apiValidationResult)
}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels })
break
}
case "glamaModels": {
const updatedModels = message.glamaModels ?? {}
setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels })
break
}
case "unboundModels": {
const updatedModels = message.unboundModels ?? {}
setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels })
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels })
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
break
}
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
@ -177,72 +213,30 @@ const ApiOptions = ({
setVsCodeLmModels(newModels)
}
break
case "glamaModels": {
const updatedModels = message.glamaModels ?? {}
setGlamaModels({
[glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
break
}
case "unboundModels": {
const updatedModels = message.unboundModels ?? {}
setUnboundModels(updatedModels)
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
setRequestyModels({
[requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
}
}, [])
useEvent("message", handleMessage)
useEvent("message", onMessage)
const createDropdown = (models: Record<string, ModelInfo>) => {
const options: DropdownOption[] = [
{ value: "", label: "Select a model..." },
...Object.keys(models).map((modelId) => ({
value: modelId,
label: modelId,
})),
]
return (
<Dropdown
id="model-id"
value={selectedModelId}
onChange={(value) => {
setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
}}
style={{ width: "100%" }}
options={options}
/>
)
}
const selectedProviderModelOptions: DropdownOption[] = useMemo(
() =>
modelsByProvider[selectedProvider]
? [
{ value: "", label: "Select a model..." },
...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({
value: modelId,
label: modelId,
})),
]
: [],
[selectedProvider],
)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
<label htmlFor="api-provider" className="font-medium">
API Provider
</label>
<Dropdown
id="api-provider"
@ -269,6 +263,8 @@ const ApiOptions = ({
/>
</div>
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
@ -277,7 +273,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("apiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
<span className="font-medium">Anthropic API Key</span>
</VSCodeTextField>
<Checkbox
@ -328,7 +324,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("glamaApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Glama API Key</span>
<span className="font-medium">Glama API Key</span>
</VSCodeTextField>
{!apiConfiguration?.glamaApiKey && (
<VSCodeButtonLink
@ -357,7 +353,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("requestyApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Requesty API Key</span>
<span className="font-medium">Requesty API Key</span>
</VSCodeTextField>
<p
style={{
@ -378,7 +374,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("openAiNativeApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>OpenAI API Key</span>
<span className="font-medium">OpenAI API Key</span>
</VSCodeTextField>
<p
style={{
@ -406,7 +402,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("mistralApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Mistral API Key</span>
<span className="font-medium">Mistral API Key</span>
</VSCodeTextField>
<p
style={{
@ -435,7 +431,7 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("mistralCodestralUrl")}
placeholder="Default: https://codestral.mistral.ai">
<span style={{ fontWeight: 500 }}>Codestral Base URL (Optional)</span>
<span className="font-medium">Codestral Base URL (Optional)</span>
</VSCodeTextField>
<p
style={{
@ -458,7 +454,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
<span className="font-medium">OpenRouter API Key</span>
</VSCodeTextField>
{!apiConfiguration?.openRouterApiKey && (
<p>
@ -530,7 +526,7 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
<span className="font-medium">AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
@ -541,7 +537,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
<span className="font-medium">AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
@ -549,7 +545,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
<span className="font-medium">AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
@ -557,13 +553,13 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
<span className="font-medium">AWS Session Token</span>
</VSCodeTextField>
</>
)}
<div className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
<span className="font-medium">AWS Region</span>
</label>
<Dropdown
id="aws-region-dropdown"
@ -615,11 +611,11 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("vertexProjectId")}
placeholder="Enter Project ID...">
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
<span className="font-medium">Google Cloud Project ID</span>
</VSCodeTextField>
<div className="dropdown-container">
<label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
<span className="font-medium">Google Cloud Region</span>
</label>
<Dropdown
id="vertex-region-dropdown"
@ -636,7 +632,6 @@ const ApiOptions = ({
]}
/>
</div>
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
<p
style={{
fontSize: "12px",
@ -668,7 +663,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("geminiApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Gemini API Key</span>
<span className="font-medium">Gemini API Key</span>
</VSCodeTextField>
<p
style={{
@ -696,7 +691,7 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("openAiBaseUrl")}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
<span className="font-medium">Base URL</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.openAiApiKey || ""}
@ -704,19 +699,18 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("openAiApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
<span className="font-medium">API Key</span>
</VSCodeTextField>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId="gpt-4o"
defaultModelInfo={openAiModelInfoSaneDefaults}
models={openAiModels}
modelIdKey="openAiModelId"
modelInfoKey="openAiCustomModelInfo"
serviceName="OpenAI"
serviceUrl="https://platform.openai.com"
recommendedModel="gpt-4-turbo-preview"
models={openAiModels}
setApiConfigurationField={setApiConfigurationField}
defaultModelInfo={openAiModelInfoSaneDefaults}
errorMessage={errorMessage}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<Checkbox
@ -749,12 +743,7 @@ const ApiOptions = ({
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
/>
)}
<div
style={{
marginTop: 15,
}}
/>
<div className="mt-4" />
<Pane
title="Model Configuration"
open={false}
@ -814,7 +803,7 @@ const ApiOptions = ({
}
})}
placeholder="e.g. 4096">
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
<span className="font-medium">Max Output Tokens</span>
</VSCodeTextField>
<div
style={{
@ -864,7 +853,7 @@ const ApiOptions = ({
}
})}
placeholder="e.g. 128000">
<span style={{ fontWeight: 500 }}>Context Window Size</span>
<span className="font-medium">Context Window Size</span>
</VSCodeTextField>
<div
style={{
@ -902,7 +891,7 @@ const ApiOptions = ({
supportsImages: checked,
}
})}>
<span style={{ fontWeight: 500 }}>Image Support</span>
<span className="font-medium">Image Support</span>
</Checkbox>
<i
className="codicon codicon-info"
@ -946,7 +935,7 @@ const ApiOptions = ({
supportsComputerUse: checked,
}
})}>
<span style={{ fontWeight: 500 }}>Computer Use</span>
<span className="font-medium">Computer Use</span>
</Checkbox>
<i
className="codicon codicon-info"
@ -1011,7 +1000,7 @@ const ApiOptions = ({
})}
placeholder="e.g. 0.0001">
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: 500 }}>Input Price</span>
<span className="font-medium">Input Price</span>
<i
className="codicon codicon-info"
title="Cost per million tokens in the input/prompt. This affects the cost of sending context and instructions to the model."
@ -1056,7 +1045,7 @@ const ApiOptions = ({
})}
placeholder="e.g. 0.0002">
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: 500 }}>Output Price</span>
<span className="font-medium">Output Price</span>
<i
className="codicon codicon-info"
title="Cost per million tokens in the model's response. This affects the cost of generated content and completions."
@ -1091,17 +1080,15 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("lmStudioBaseUrl")}
placeholder={"Default: http://localhost:1234"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
<span className="font-medium">Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
<span className="font-medium">Model ID</span>
</VSCodeTextField>
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
@ -1139,7 +1126,7 @@ const ApiOptions = ({
</VSCodeLink>{" "}
feature to use it with this extension.{" "}
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
(<span className="font-medium">Note:</span> Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
</span>
</p>
@ -1154,7 +1141,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>DeepSeek API Key</span>
<span className="font-medium">DeepSeek API Key</span>
</VSCodeTextField>
<p
style={{
@ -1178,7 +1165,7 @@ const ApiOptions = ({
<div>
<div className="dropdown-container">
<label htmlFor="vscode-lm-model">
<span style={{ fontWeight: 500 }}>Language Model</span>
<span className="font-medium">Language Model</span>
</label>
{vsCodeLmModels.length > 0 ? (
<Dropdown
@ -1237,14 +1224,14 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("ollamaBaseUrl")}
placeholder={"Default: http://localhost:11434"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
<span className="font-medium">Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("ollamaModelId")}
placeholder={"e.g. llama3.1"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
<span className="font-medium">Model ID</span>
</VSCodeTextField>
{errorMessage && (
<div className="text-vscode-errorForeground text-sm">
@ -1284,7 +1271,7 @@ const ApiOptions = ({
quickstart guide.
</VSCodeLink>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
(<span className="font-medium">Note:</span> Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
</span>
</p>
@ -1299,7 +1286,7 @@ const ApiOptions = ({
type="password"
onChange={handleInputChange("unboundApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Unbound API Key</span>
<span className="font-medium">Unbound API Key</span>
</VSCodeTextField>
{!apiConfiguration?.unboundApiKey && (
<VSCodeButtonLink
@ -1317,38 +1304,9 @@ const ApiOptions = ({
}}>
This key is stored locally and only used to make API requests from this extension.
</p>
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={unboundDefaultModelId}
defaultModelInfo={unboundDefaultModelInfo}
models={unboundModels}
modelInfoKey="unboundModelInfo"
modelIdKey="unboundModelId"
serviceName="Unbound"
serviceUrl="https://api.getunbound.ai/models"
recommendedModel={unboundDefaultModelId}
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
/>
</div>
)}
{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration ?? {}}
defaultModelId={glamaDefaultModelId}
defaultModelInfo={glamaDefaultModelInfo}
models={glamaModels}
modelInfoKey="glamaModelInfo"
modelIdKey="glamaModelId"
serviceName="Glama"
serviceUrl="https://glama.ai/models"
recommendedModel="anthropic/claude-3-7-sonnet"
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
/>
)}
{selectedProvider === "openrouter" && (
<ModelPicker
apiConfiguration={apiConfiguration}
@ -1360,10 +1318,37 @@ const ApiOptions = ({
modelInfoKey="openRouterModelInfo"
serviceName="OpenRouter"
serviceUrl="https://openrouter.ai/models"
recommendedModel="anthropic/claude-3.7-sonnet"
errorMessage={errorMessage}
/>
)}
{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={glamaDefaultModelId}
defaultModelInfo={glamaDefaultModelInfo}
models={glamaModels}
modelInfoKey="glamaModelInfo"
modelIdKey="glamaModelId"
serviceName="Glama"
serviceUrl="https://glama.ai/models"
/>
)}
{selectedProvider === "unbound" && (
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={unboundDefaultModelId}
defaultModelInfo={unboundDefaultModelInfo}
models={unboundModels}
modelInfoKey="unboundModelInfo"
modelIdKey="unboundModelId"
serviceName="Unbound"
serviceUrl="https://api.getunbound.ai/models"
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "requesty" && (
<ModelPicker
apiConfiguration={apiConfiguration}
@ -1375,58 +1360,37 @@ const ApiOptions = ({
modelInfoKey="requestyModelInfo"
serviceName="Requesty"
serviceUrl="https://requesty.ai"
recommendedModel="anthropic/claude-3-7-sonnet-latest"
errorMessage={errorMessage}
/>
)}
{selectedProvider !== "glama" &&
selectedProvider !== "openrouter" &&
selectedProvider !== "requesty" &&
selectedProvider !== "openai" &&
selectedProvider !== "ollama" &&
selectedProvider !== "lmstudio" &&
selectedProvider !== "unbound" && (
<>
<div className="dropdown-container">
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "vertex" && createDropdown(vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
</div>
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
{selectedProviderModelOptions.length > 0 && (
<>
<div className="dropdown-container">
<label htmlFor="model-id" className="font-medium">
Model
</label>
<Dropdown
id="model-id"
value={selectedModelId}
onChange={(value) => {
setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
}}
options={selectedProviderModelOptions}
className="w-full"
/>
</>
)}
{selectedModelInfo && selectedModelInfo.thinking && (
<div className="flex flex-col gap-1 mt-2">
<div className="font-medium">Thinking Budget</div>
<div className="flex items-center gap-1">
<Slider
min={THINKING_BUDGET.min}
max={(selectedModelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
step={THINKING_BUDGET.step}
value={[anthropicThinkingBudget]}
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
/>
<div className="w-12 text-sm text-center">{anthropicThinkingBudget}</div>
</div>
<div className="text-muted-foreground text-sm">
Number of tokens Claude is allowed to use for its internal reasoning process.
</div>
</div>
<ThinkingBudget
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
/>
</>
)}
{!fromWelcomeView && (
@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
@ -1481,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "glama":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
}
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@ -1495,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "glama":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
}
case "unbound":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
}
case "requesty":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
}
case "openai":
return {
selectedProvider: provider,
@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
supportsImages: false, // VSCode LM API currently doesn't support images
supportsImages: false, // VSCode LM API currently doesn't support images.
},
}
case "unbound":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
}
case "requesty":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}

View file

@ -1,11 +1,13 @@
import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useMemo, useState, useCallback, useEffect } from "react"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox"
import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { normalizeApiConfiguration } from "./ApiOptions"
import { ThinkingBudget } from "./ThinkingBudget"
import { ModelInfoView } from "./ModelInfoView"
import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
import ApiErrorMessage from "./ApiErrorMessage"
type ExtractType<T> = NonNullable<
{ [K in keyof ApiConfiguration]: Required<ApiConfiguration>[K] extends T ? K : never }[keyof ApiConfiguration]
@ -14,24 +16,17 @@ type ExtractType<T> = NonNullable<
type ModelIdKeys = NonNullable<
{ [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
>
declare module "react" {
interface CSSProperties {
// Allow CSS variables
[key: `--${string}`]: string | number
}
}
interface ModelPickerProps {
defaultModelId?: string
defaultModelId: string
defaultModelInfo?: ModelInfo
models: Record<string, ModelInfo> | null
modelIdKey: ModelIdKeys
modelInfoKey: ExtractType<ModelInfo>
serviceName: string
serviceUrl: string
recommendedModel: string
apiConfiguration: ApiConfiguration
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
defaultModelInfo?: ModelInfo
errorMessage?: string
}
export const ModelPicker = ({
@ -41,13 +36,12 @@ export const ModelPicker = ({
modelInfoKey,
serviceName,
serviceUrl,
recommendedModel,
apiConfiguration,
setApiConfigurationField,
defaultModelInfo,
errorMessage,
}: ModelPickerProps) => {
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const isInitialized = useRef(false)
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
@ -55,6 +49,7 @@ export const ModelPicker = ({
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
const onSelect = useCallback(
(modelId: string) => {
const modelInfo = models?.[modelId]
@ -63,26 +58,23 @@ export const ModelPicker = ({
},
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)
const inputValue = apiConfiguration[modelIdKey]
useEffect(() => {
if (apiConfiguration[modelIdKey] == null && defaultModelId) {
onSelect(defaultModelId)
if (!inputValue && !isInitialized.current) {
const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId
setApiConfigurationField(modelIdKey, initialValue)
}
}, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
isInitialized.current = true
}, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId])
return (
<>
<div className="font-semibold">Model</div>
<Combobox
style={errorMessage ? { "--color-vscode-dropdown-border": "var(--color-vscode-errorForeground)" } : {}}
type="single"
inputValue={apiConfiguration[modelIdKey]}
onInputValueChange={onSelect}>
<ComboboxInput
className="border-vscode-errorForeground tefat"
placeholder="Search model..."
data-testid="model-input"
aria-errormessage={errorMessage}
/>
<Combobox type="single" inputValue={inputValue} onInputValueChange={onSelect}>
<ComboboxInput placeholder="Search model..." data-testid="model-input" />
<ComboboxContent>
<ComboboxEmpty>No model found.</ComboboxEmpty>
{modelIds.map((model) => (
@ -92,31 +84,18 @@ export const ModelPicker = ({
))}
</ComboboxContent>
</Combobox>
{errorMessage ? (
<ApiErrorMessage errorMessage={errorMessage}>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.
</span>
</p>
</ApiErrorMessage>
) : (
selectedModelId &&
selectedModelInfo && (
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
/>
)
<ThinkingBudget
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
{selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
/>
)}
<p>
The extension automatically fetches the latest list of models available on{" "}
@ -124,7 +103,7 @@ export const ModelPicker = ({
{serviceName}.
</VSCodeLink>
If you're unsure which model to choose, Roo Code works best with{" "}
<VSCodeLink onClick={() => onSelect(recommendedModel)}>{recommendedModel}.</VSCodeLink>
<VSCodeLink onClick={() => onSelect(defaultModelId)}>{defaultModelId}.</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</p>
</>

View file

@ -66,21 +66,20 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
terminalOutputLineLimit,
writeDelayMs,
} = cachedState
//Make sure apiConfiguration is initialized and managed by SettingsView
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
// Update only when currentApiConfigName is changed
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration
// Update only when currentApiConfigName is changed.
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
if (prevApiConfigName.current === currentApiConfigName) {
return
}
setCachedState((prevCachedState) => ({
...prevCachedState,
...extensionState,
}))
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
// console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
setChangeDetected(false)
}, [currentApiConfigName, extensionState, isChangeDetected])
@ -90,11 +89,10 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
if (prevState[field] === value) {
return prevState
}
// console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
return {
...prevState,
[field]: value,
}
return { ...prevState, [field]: value }
})
},
[],
@ -107,15 +105,10 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
return prevState
}
// console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
return {
...prevState,
apiConfiguration: {
...prevState.apiConfiguration,
[field]: value,
},
}
return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
})
},
[],
@ -126,14 +119,19 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
if (prevState.experiments?.[id] === enabled) {
return prevState
}
// console.log("setExperimentEnabled: setChangeDetected -> true")
setChangeDetected(true)
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
})
}, [])
const isSettingValid = !errorMessage
const handleSubmit = () => {
if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
@ -160,6 +158,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
vscode.postMessage({ type: "updateExperimental", values: experiments })
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
// console.log("handleSubmit: setChangeDetected -> false")
setChangeDetected(false)
}
}
@ -176,13 +175,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
[isChangeDetected],
)
useImperativeHandle(
ref,
() => ({
checkUnsaveChanges,
}),
[checkUnsaveChanges],
)
useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
@ -200,10 +193,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
const newCommands = [...currentCommands, commandInput]
setCachedStateField("allowedCommands", newCommands)
setCommandInput("")
vscode.postMessage({
type: "allowedCommands",
commands: newCommands,
})
vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}
}

View file

@ -0,0 +1,29 @@
import { Slider } from "@/components/ui"
import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
interface ThinkingBudgetProps {
apiConfiguration: ApiConfiguration
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
modelInfo?: ModelInfo
}
export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
return modelInfo && modelInfo.thinking ? (
<div className="flex flex-col gap-1 mt-2">
<div className="font-medium">Thinking Budget</div>
<div className="flex items-center gap-1">
<Slider
min={THINKING_BUDGET.min}
max={(modelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
step={THINKING_BUDGET.step}
value={[budget]}
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
/>
<div className="w-12 text-sm text-center">{budget}</div>
</div>
</div>
) : null
}

View file

@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
}
const AlertDialogPortal = AlertDialogPrimitive.Portal
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
}
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
)
}
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
)
AlertDialogHeader.displayName = "AlertDialogHeader"
function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
)
}
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-row justify-end space-x-2", className)} {...props} />
)
AlertDialogFooter.displayName = "AlertDialogFooter"
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...props}
/>
)
}
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-base text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "secondary" }), "mt-0", className)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />
}
function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: "outline" }), className)} {...props} />
}
export {
AlertDialog,

View file

@ -1,96 +1,108 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
const DialogTrigger = DialogPrimitive.Trigger
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
const DialogPortal = DialogPrimitive.Portal
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
const DialogClose = DialogPrimitive.Close
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}>
{children}
<DialogPrimitive.Close className="cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
{...props}
/>
)
}
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...props}
/>
)
}
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View file

@ -12,16 +12,14 @@ const WelcomeView = () => {
const handleSubmit = useCallback(() => {
const error = validateApiConfiguration(apiConfiguration)
if (error) {
setErrorMessage(error)
return
}
setErrorMessage(undefined)
vscode.postMessage({
type: "upsertApiConfiguration",
text: currentApiConfigName,
apiConfiguration,
})
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
}, [apiConfiguration, currentApiConfigName])
return (

View file

@ -1,74 +1,83 @@
import { ApiConfiguration } from "../../../src/shared/api"
import { ModelInfo } from "../../../src/shared/api"
import { ApiConfiguration, ModelInfo } from "../../../src/shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "anthropic":
if (!apiConfiguration.apiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "glama":
if (!apiConfiguration.glamaApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "bedrock":
if (!apiConfiguration.awsRegion) {
return "You must choose a region to use with AWS Bedrock."
}
break
case "openrouter":
if (!apiConfiguration.openRouterApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "vertex":
if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
return "You must provide a valid Google Cloud Project ID and Region."
}
break
case "gemini":
if (!apiConfiguration.geminiApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "openai-native":
if (!apiConfiguration.openAiNativeApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "mistral":
if (!apiConfiguration.mistralApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "openai":
if (
!apiConfiguration.openAiBaseUrl ||
!apiConfiguration.openAiApiKey ||
!apiConfiguration.openAiModelId
) {
return "You must provide a valid base URL, API key, and model ID."
}
break
case "ollama":
if (!apiConfiguration.ollamaModelId) {
return "You must provide a valid model ID."
}
break
case "lmstudio":
if (!apiConfiguration.lmStudioModelId) {
return "You must provide a valid model ID."
}
break
case "vscode-lm":
if (!apiConfiguration.vsCodeLmModelSelector) {
return "You must provide a valid model selector."
}
break
}
if (!apiConfiguration) {
return undefined
}
switch (apiConfiguration.apiProvider) {
case "openrouter":
if (!apiConfiguration.openRouterApiKey) {
return "You must provide a valid API key."
}
break
case "glama":
if (!apiConfiguration.glamaApiKey) {
return "You must provide a valid API key."
}
break
case "unbound":
if (!apiConfiguration.unboundApiKey) {
return "You must provide a valid API key."
}
break
case "requesty":
if (!apiConfiguration.requestyApiKey) {
return "You must provide a valid API key."
}
break
case "anthropic":
if (!apiConfiguration.apiKey) {
return "You must provide a valid API key."
}
break
case "bedrock":
if (!apiConfiguration.awsRegion) {
return "You must choose a region to use with AWS Bedrock."
}
break
case "vertex":
if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
return "You must provide a valid Google Cloud Project ID and Region."
}
break
case "gemini":
if (!apiConfiguration.geminiApiKey) {
return "You must provide a valid API key."
}
break
case "openai-native":
if (!apiConfiguration.openAiNativeApiKey) {
return "You must provide a valid API key."
}
break
case "mistral":
if (!apiConfiguration.mistralApiKey) {
return "You must provide a valid API key."
}
break
case "openai":
if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
return "You must provide a valid base URL, API key, and model ID."
}
break
case "ollama":
if (!apiConfiguration.ollamaModelId) {
return "You must provide a valid model ID."
}
break
case "lmstudio":
if (!apiConfiguration.lmStudioModelId) {
return "You must provide a valid model ID."
}
break
case "vscode-lm":
if (!apiConfiguration.vsCodeLmModelSelector) {
return "You must provide a valid model selector."
}
break
}
return undefined
}
@ -77,40 +86,81 @@ export function validateModelId(
glamaModels?: Record<string, ModelInfo>,
openRouterModels?: Record<string, ModelInfo>,
unboundModels?: Record<string, ModelInfo>,
requestyModels?: Record<string, ModelInfo>,
): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "glama":
const glamaModelId = apiConfiguration.glamaModelId
if (!glamaModelId) {
return "You must provide a model ID."
}
if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) {
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
return "The model ID you provided is not available. Please choose a different model."
}
break
case "openrouter":
const modelId = apiConfiguration.openRouterModelId
if (!modelId) {
return "You must provide a model ID."
}
if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
return "The model ID you provided is not available. Please choose a different model."
}
break
case "unbound":
const unboundModelId = apiConfiguration.unboundModelId
if (!unboundModelId) {
return "You must provide a model ID."
}
if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) {
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
return "The model ID you provided is not available. Please choose a different model."
}
break
}
if (!apiConfiguration) {
return undefined
}
switch (apiConfiguration.apiProvider) {
case "openrouter":
const modelId = apiConfiguration.openRouterModelId
if (!modelId) {
return "You must provide a model ID."
}
if (
openRouterModels &&
Object.keys(openRouterModels).length > 1 &&
!Object.keys(openRouterModels).includes(modelId)
) {
return `The model ID (${modelId}) you provided is not available. Please choose a different model.`
}
break
case "glama":
const glamaModelId = apiConfiguration.glamaModelId
if (!glamaModelId) {
return "You must provide a model ID."
}
if (
glamaModels &&
Object.keys(glamaModels).length > 1 &&
!Object.keys(glamaModels).includes(glamaModelId)
) {
return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.`
}
break
case "unbound":
const unboundModelId = apiConfiguration.unboundModelId
if (!unboundModelId) {
return "You must provide a model ID."
}
if (
unboundModels &&
Object.keys(unboundModels).length > 1 &&
!Object.keys(unboundModels).includes(unboundModelId)
) {
return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.`
}
break
case "requesty":
const requestyModelId = apiConfiguration.requestyModelId
if (!requestyModelId) {
return "You must provide a model ID."
}
if (
requestyModels &&
Object.keys(requestyModels).length > 1 &&
!Object.keys(requestyModels).includes(requestyModelId)
) {
return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.`
}
break
}
return undefined
}