Files
Roo-Code/webview-ui/src/components/ApiOptions.tsx
Saoud Rizwan cc9637e0fe Minor fixes
2024-08-26 01:00:00 -04:00

380 lines
13 KiB
TypeScript

import {
VSCodeButton,
VSCodeDropdown,
VSCodeLink,
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import React, { useMemo } from "react"
import {
ApiConfiguration,
ApiModelId,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
maestroDefaultModelId,
maestroModels,
openRouterDefaultModelId,
openRouterModels,
} from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { MaestroUser } from "../../../src/shared/maestro"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maestroUser?: MaestroUser
}
const ApiOptions: React.FC<ApiOptionsProps> = ({
showModelOptions,
apiConfiguration,
setApiConfiguration,
maestroUser,
}) => {
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [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="maestro">Maestro</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 === "maestro" && (
<>
{maestroUser ? (
<div>
<span
style={{
fontWeight: 500,
color: "var(--vscode-testing-iconPassed)",
}}>
<i
className={`codicon codicon-check`}
style={{
marginRight: 4,
marginBottom: 1,
fontSize: 11,
fontWeight: 700,
display: "inline-block",
verticalAlign: "bottom",
}}></i>
Signed in as {maestroUser.email}
</span>
<div style={{ margin: "4px 0px 2px 0px" }}>
<VSCodeButton
appearance="secondary"
onClick={() => vscode.postMessage({ type: "didClickMaestroSignOut" })}>
Sign out
</VSCodeButton>
</div>
</div>
) : (
<div>
<div style={{ margin: "4px 0px" }}>
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "didClickMaestroSignIn" })}>
Sign in to Maestro
</VSCodeButton>
</div>
<p
style={{
fontSize: 12,
marginTop: 5,
color: "var(--vscode-descriptionForeground)",
}}>
This will open your browser to sign in to Maestro. You will be redirected back to the
extension after signing in.
</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>
)}
{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 === "maestro" && createDropdown(maestroModels)}
</div>
<ModelInfoView modelInfo={selectedModelInfo} />
</>
)}
</div>
)
}
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
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 "maestro":
return getProviderData(maestroModels, maestroDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
}
export default ApiOptions