mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
|
import React, { useMemo } from "react"
|
|
import {
|
|
ApiConfiguration,
|
|
ApiModelId,
|
|
ModelInfo,
|
|
anthropicDefaultModelId,
|
|
anthropicModels,
|
|
bedrockDefaultModelId,
|
|
bedrockModels,
|
|
openRouterDefaultModelId,
|
|
openRouterModels,
|
|
vertexDefaultModelId,
|
|
vertexModels,
|
|
} from "../../../src/shared/api"
|
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
|
|
|
interface ApiOptionsProps {
|
|
showModelOptions: boolean
|
|
apiErrorMessage?: string
|
|
}
|
|
|
|
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessage }) => {
|
|
const { apiConfiguration, setApiConfiguration } = useExtensionState()
|
|
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
|
setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
|
|
}
|
|
|
|
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
|
|
return normalizeApiConfiguration(apiConfiguration)
|
|
}, [apiConfiguration])
|
|
|
|
/*
|
|
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>) => {
|
|
return (
|
|
<VSCodeDropdown
|
|
id="model-id"
|
|
value={selectedModelId}
|
|
onChange={handleInputChange("apiModelId")}
|
|
style={{ width: "100%" }}>
|
|
<VSCodeOption value="">Select a model...</VSCodeOption>
|
|
{Object.keys(models).map((modelId) => (
|
|
<VSCodeOption
|
|
key={modelId}
|
|
value={modelId}
|
|
style={{
|
|
whiteSpace: "normal",
|
|
wordWrap: "break-word",
|
|
maxWidth: "100%",
|
|
}}>
|
|
{modelId}
|
|
</VSCodeOption>
|
|
))}
|
|
</VSCodeDropdown>
|
|
)
|
|
}
|
|
|
|
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>
|
|
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
|
|
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
|
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
|
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
|
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
|
</VSCodeDropdown>
|
|
</div>
|
|
|
|
{selectedProvider === "anthropic" && (
|
|
<div>
|
|
<VSCodeTextField
|
|
value={apiConfiguration?.apiKey || ""}
|
|
style={{ width: "100%" }}
|
|
onInput={handleInputChange("apiKey")}
|
|
placeholder="Enter API Key...">
|
|
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
|
|
</VSCodeTextField>
|
|
<p
|
|
style={{
|
|
fontSize: "12px",
|
|
marginTop: "5px",
|
|
color: "var(--vscode-descriptionForeground)",
|
|
}}>
|
|
This key is stored locally and only used to make API requests from this extension.
|
|
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
|
|
You can get an Anthropic API key by signing up here.
|
|
</VSCodeLink>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedProvider === "openrouter" && (
|
|
<div>
|
|
<VSCodeTextField
|
|
value={apiConfiguration?.openRouterApiKey || ""}
|
|
style={{ width: "100%" }}
|
|
onInput={handleInputChange("openRouterApiKey")}
|
|
placeholder="Enter API Key...">
|
|
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
|
|
</VSCodeTextField>
|
|
<p
|
|
style={{
|
|
fontSize: "12px",
|
|
marginTop: "5px",
|
|
color: "var(--vscode-descriptionForeground)",
|
|
}}>
|
|
This key is stored locally and only used to make API requests from this extension.
|
|
<VSCodeLink href="https://openrouter.ai/" style={{ display: "inline" }}>
|
|
You can get an OpenRouter API key by signing up here.
|
|
</VSCodeLink>{" "}
|
|
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
|
(<span style={{ fontWeight: 500 }}>Note:</span> OpenRouter support is experimental and may
|
|
not work well with large files.)
|
|
</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedProvider === "bedrock" && (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
|
<VSCodeTextField
|
|
value={apiConfiguration?.awsAccessKey || ""}
|
|
style={{ width: "100%" }}
|
|
onInput={handleInputChange("awsAccessKey")}
|
|
placeholder="Enter Access Key...">
|
|
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
|
|
</VSCodeTextField>
|
|
<VSCodeTextField
|
|
value={apiConfiguration?.awsSecretKey || ""}
|
|
style={{ width: "100%" }}
|
|
onInput={handleInputChange("awsSecretKey")}
|
|
placeholder="Enter Secret Key...">
|
|
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
|
|
</VSCodeTextField>
|
|
<div className="dropdown-container">
|
|
<label htmlFor="aws-region-dropdown">
|
|
<span style={{ fontWeight: 500 }}>AWS Region</span>
|
|
</label>
|
|
<VSCodeDropdown
|
|
id="aws-region-dropdown"
|
|
value={apiConfiguration?.awsRegion || ""}
|
|
style={{ width: "100%" }}
|
|
onChange={handleInputChange("awsRegion")}>
|
|
<VSCodeOption value="">Select a region...</VSCodeOption>
|
|
{/* 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. */}
|
|
<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> */}
|
|
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
|
|
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
|
|
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
|
|
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
|
|
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
|
|
{/* <VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption> */}
|
|
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */}
|
|
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
|
|
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
|
|
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
|
|
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
|
|
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
|
|
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
|
|
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
|
|
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */}
|
|
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
|
|
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
|
|
</VSCodeDropdown>
|
|
</div>
|
|
<p
|
|
style={{
|
|
fontSize: "12px",
|
|
marginTop: "5px",
|
|
color: "var(--vscode-descriptionForeground)",
|
|
}}>
|
|
These credentials are stored locally and only used to make API requests from this extension.
|
|
<VSCodeLink
|
|
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"
|
|
style={{ display: "inline" }}>
|
|
You can find your AWS access key and secret key here.
|
|
</VSCodeLink>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{apiConfiguration?.apiProvider === "vertex" && (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
|
<VSCodeTextField
|
|
value={apiConfiguration?.vertexProjectId || ""}
|
|
style={{ width: "100%" }}
|
|
onInput={handleInputChange("vertexProjectId")}
|
|
placeholder="Enter Project ID...">
|
|
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
|
|
</VSCodeTextField>
|
|
<div className="dropdown-container">
|
|
<label htmlFor="vertex-region-dropdown">
|
|
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
|
|
</label>
|
|
<VSCodeDropdown
|
|
id="vertex-region-dropdown"
|
|
value={apiConfiguration?.vertexRegion || ""}
|
|
style={{ width: "100%" }}
|
|
onChange={handleInputChange("vertexRegion")}>
|
|
<VSCodeOption value="">Select a region...</VSCodeOption>
|
|
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
|
|
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
|
|
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
|
|
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
|
|
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
|
|
</VSCodeDropdown>
|
|
</div>
|
|
<p
|
|
style={{
|
|
fontSize: "12px",
|
|
marginTop: "5px",
|
|
color: "var(--vscode-descriptionForeground)",
|
|
}}>
|
|
To use Google Cloud Vertex AI, you need to
|
|
<VSCodeLink
|
|
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
|
|
style={{ display: "inline" }}>
|
|
{
|
|
"1) create a Google Cloud account > enable the Vertex AI API > enable the desired Claude models,"
|
|
}
|
|
</VSCodeLink>{" "}
|
|
<VSCodeLink
|
|
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
|
|
style={{ display: "inline" }}>
|
|
{"2) install the Google Cloud CLI > configure Application Default Credentials."}
|
|
</VSCodeLink>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{apiErrorMessage && (
|
|
<p
|
|
style={{
|
|
margin: "-10px 0 4px 0",
|
|
fontSize: 12,
|
|
color: "var(--vscode-errorForeground)",
|
|
}}>
|
|
{apiErrorMessage}
|
|
</p>
|
|
)}
|
|
|
|
{showModelOptions && (
|
|
<>
|
|
<div className="dropdown-container">
|
|
<label htmlFor="model-id">
|
|
<span style={{ fontWeight: 500 }}>Model</span>
|
|
</label>
|
|
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
|
|
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
|
|
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
|
|
{selectedProvider === "vertex" && createDropdown(vertexModels)}
|
|
</div>
|
|
|
|
<ModelInfoView modelInfo={selectedModelInfo} />
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const formatPrice = (price: number) => {
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency: "USD",
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
}).format(price)
|
|
}
|
|
|
|
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
|
|
return (
|
|
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
|
|
<ModelInfoSupportsItem
|
|
isSupported={modelInfo.supportsImages}
|
|
supportsLabel="Supports images"
|
|
doesNotSupportLabel="Does not support images"
|
|
/>
|
|
<br />
|
|
<ModelInfoSupportsItem
|
|
isSupported={modelInfo.supportsPromptCache}
|
|
supportsLabel="Supports prompt caching"
|
|
doesNotSupportLabel="Does not support prompt caching"
|
|
/>
|
|
<br />
|
|
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo?.maxTokens?.toLocaleString()} tokens
|
|
<br />
|
|
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
|
|
{modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && modelInfo.cacheReadsPrice && (
|
|
<>
|
|
<br />
|
|
<span style={{ fontWeight: 500 }}>Cache writes price:</span>{" "}
|
|
{formatPrice(modelInfo.cacheWritesPrice || 0)}/million tokens
|
|
<br />
|
|
<span style={{ fontWeight: 500 }}>Cache reads price:</span>{" "}
|
|
{formatPrice(modelInfo.cacheReadsPrice || 0)}/million tokens
|
|
</>
|
|
)}
|
|
<br />
|
|
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
|
|
</p>
|
|
)
|
|
}
|
|
|
|
const ModelInfoSupportsItem = ({
|
|
isSupported,
|
|
supportsLabel,
|
|
doesNotSupportLabel,
|
|
}: {
|
|
isSupported: boolean
|
|
supportsLabel: string
|
|
doesNotSupportLabel: string
|
|
}) => (
|
|
<span
|
|
style={{
|
|
fontWeight: 500,
|
|
color: isSupported ? "var(--vscode-testing-iconPassed)" : "var(--vscode-errorForeground)",
|
|
}}>
|
|
<i
|
|
className={`codicon codicon-${isSupported ? "check" : "x"}`}
|
|
style={{
|
|
marginRight: 4,
|
|
marginBottom: isSupported ? 1 : -1,
|
|
fontSize: isSupported ? 11 : 13,
|
|
fontWeight: 700,
|
|
display: "inline-block",
|
|
verticalAlign: "bottom",
|
|
}}></i>
|
|
{isSupported ? supportsLabel : doesNotSupportLabel}
|
|
</span>
|
|
)
|
|
|
|
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
|
const provider = apiConfiguration?.apiProvider || "anthropic"
|
|
const modelId = apiConfiguration?.apiModelId
|
|
|
|
const getProviderData = (models: Record<string, ModelInfo>, defaultId: ApiModelId) => {
|
|
let selectedModelId: ApiModelId
|
|
let selectedModelInfo: ModelInfo
|
|
if (modelId && modelId in models) {
|
|
selectedModelId = modelId
|
|
selectedModelInfo = models[modelId]
|
|
} else {
|
|
selectedModelId = defaultId
|
|
selectedModelInfo = models[defaultId]
|
|
}
|
|
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
|
|
}
|
|
switch (provider) {
|
|
case "anthropic":
|
|
return getProviderData(anthropicModels, anthropicDefaultModelId)
|
|
case "openrouter":
|
|
return getProviderData(openRouterModels, openRouterDefaultModelId)
|
|
case "bedrock":
|
|
return getProviderData(bedrockModels, bedrockDefaultModelId)
|
|
case "vertex":
|
|
return getProviderData(vertexModels, vertexDefaultModelId)
|
|
default:
|
|
return getProviderData(anthropicModels, anthropicDefaultModelId)
|
|
}
|
|
}
|
|
|
|
export default ApiOptions
|