mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
379 lines
14 KiB
TypeScript
379 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%" }}
|
||
type="password"
|
||
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%" }}
|
||
type="password"
|
||
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%" }}
|
||
type="password"
|
||
onInput={handleInputChange("awsAccessKey")}
|
||
placeholder="Enter Access Key...">
|
||
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
|
||
</VSCodeTextField>
|
||
<VSCodeTextField
|
||
value={apiConfiguration?.awsSecretKey || ""}
|
||
style={{ width: "100%" }}
|
||
type="password"
|
||
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
|