Add support for OpenRouter and AWS Bedrock

This commit is contained in:
Saoud Rizwan
2024-08-03 14:24:56 -04:00
parent d441950b7f
commit c09a8462d7
19 changed files with 4458 additions and 194 deletions

View File

@@ -6,6 +6,7 @@ import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
import WelcomeView from "./components/WelcomeView"
import { vscode } from "./utilities/vscode"
import { useEvent } from "react-use"
import { ApiConfiguration } from "@shared/api"
/*
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
@@ -19,7 +20,7 @@ const App: React.FC = () => {
const [didHydrateState, setDidHydrateState] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const [showWelcome, setShowWelcome] = useState<boolean>(false)
const [apiKey, setApiKey] = useState<string>("")
const [apiConfiguration, setApiConfiguration] = useState<ApiConfiguration | undefined>(undefined)
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
const [vscodeThemeName, setVscodeThemeName] = useState<string | undefined>(undefined)
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
@@ -33,8 +34,12 @@ const App: React.FC = () => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "state":
setShowWelcome(!message.state!.apiKey)
setApiKey(message.state!.apiKey || "")
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
message.state!.apiConfiguration?.awsAccessKey !== undefined
setShowWelcome(!hasKey)
setApiConfiguration(message.state!.apiConfiguration)
setMaxRequestsPerTask(
message.state!.maxRequestsPerTask !== undefined ? message.state!.maxRequestsPerTask.toString() : ""
)
@@ -70,13 +75,13 @@ const App: React.FC = () => {
return (
<>
{showWelcome ? (
<WelcomeView apiKey={apiKey} setApiKey={setApiKey} />
<WelcomeView apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
) : (
<>
{showSettings && (
<SettingsView
apiKey={apiKey}
setApiKey={setApiKey}
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
onDone={() => setShowSettings(false)}

View File

@@ -0,0 +1,144 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React from "react"
interface ApiOptionsProps {
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfiguration }) => {
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
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={apiConfiguration?.apiProvider || "anthropic"}
onChange={handleInputChange("apiProvider")}>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
</VSCodeDropdown>
</div>
{apiConfiguration?.apiProvider === "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 the extension.
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
You can get an Anthropic API key by signing up here.
</VSCodeLink>
</p>
</div>
)}
{apiConfiguration?.apiProvider === "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 the extension.
<VSCodeLink href="https://openrouter.ai/" style={{ display: "inline" }}>
You can get an OpenRouter API key by signing up here.
</VSCodeLink>
</p>
</div>
)}
{apiConfiguration?.apiProvider === "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>
<VSCodeOption value="us-east-1">US East (N. Virginia)</VSCodeOption>
<VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
<VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption>
<VSCodeOption value="us-west-2">US West (Oregon)</VSCodeOption>
<VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption>
<VSCodeOption value="ap-east-1">Asia Pacific (Hong Kong)</VSCodeOption>
<VSCodeOption value="ap-south-1">Asia Pacific (Mumbai)</VSCodeOption>
<VSCodeOption value="ap-northeast-1">Asia Pacific (Tokyo)</VSCodeOption>
<VSCodeOption value="ap-northeast-2">Asia Pacific (Seoul)</VSCodeOption>
<VSCodeOption value="ap-northeast-3">Asia Pacific (Osaka)</VSCodeOption>
<VSCodeOption value="ap-southeast-1">Asia Pacific (Singapore)</VSCodeOption>
<VSCodeOption value="ap-southeast-2">Asia Pacific (Sydney)</VSCodeOption>
<VSCodeOption value="ca-central-1">Canada (Central)</VSCodeOption>
<VSCodeOption value="eu-central-1">Europe (Frankfurt)</VSCodeOption>
<VSCodeOption value="eu-west-1">Europe (Ireland)</VSCodeOption>
<VSCodeOption value="eu-west-2">Europe (London)</VSCodeOption>
<VSCodeOption value="eu-west-3">Europe (Paris)</VSCodeOption>
<VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption>
<VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption>
<VSCodeOption value="sa-east-1">South America (São Paulo)</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 the 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>
)}
</div>
)
}
export default ApiOptions

View File

@@ -1,64 +1,50 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React, { useState } from "react"
import { useEffectOnce } from "react-use"
import React, { useEffect, useState } from "react"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utilities/validate"
import { vscode } from "../utilities/vscode"
import ApiOptions from "./ApiOptions"
type SettingsViewProps = {
apiKey: string
setApiKey: React.Dispatch<React.SetStateAction<string>>
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
onDone: () => void // Define the type of the onDone prop
onDone: () => void
}
const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPerTask, onDone }: SettingsViewProps) => {
const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState<string | undefined>(undefined)
const SettingsView = ({
apiConfiguration,
setApiConfiguration,
maxRequestsPerTask,
setMaxRequestsPerTask,
onDone,
}: SettingsViewProps) => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [maxRequestsErrorMessage, setMaxRequestsErrorMessage] = useState<string | undefined>(undefined)
const disableDoneButton = apiKeyErrorMessage != null || maxRequestsErrorMessage != null
const handleApiKeyChange = (event: any) => {
const input = event.target.value
setApiKey(input)
validateApiKey(input)
}
const validateApiKey = (value: string) => {
if (value.trim() === "") {
setApiKeyErrorMessage("API Key cannot be empty")
} else {
setApiKeyErrorMessage(undefined)
}
}
const handleMaxRequestsChange = (event: any) => {
const input = event.target.value
setMaxRequestsPerTask(input)
validateMaxRequests(input)
}
const validateMaxRequests = (value: string | undefined) => {
if (value?.trim()) {
const num = Number(value)
if (isNaN(num)) {
setMaxRequestsErrorMessage("Maximum requests must be a number")
} else if (num < 3 || num > 100) {
setMaxRequestsErrorMessage("Maximum requests must be between 3 and 100")
} else {
setMaxRequestsErrorMessage(undefined)
}
} else {
setMaxRequestsErrorMessage(undefined)
}
}
const handleSubmit = () => {
vscode.postMessage({ type: "apiKey", text: apiKey })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const maxRequestsValidationResult = validateMaxRequestsPerTask(maxRequestsPerTask)
onDone()
setApiErrorMessage(apiValidationResult)
setMaxRequestsErrorMessage(maxRequestsValidationResult)
if (!apiValidationResult && !maxRequestsValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
onDone()
}
}
useEffect(() => {
setApiErrorMessage(undefined)
}, [apiConfiguration])
useEffect(() => {
setMaxRequestsErrorMessage(undefined)
}, [maxRequestsPerTask])
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks.
@@ -70,14 +56,6 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
useEffectOnce(() => {
const timeoutId = setTimeout(() => {
validateApiKey(apiKey)
validateMaxRequests(maxRequestsPerTask)
}, 1000)
return () => clearTimeout(timeoutId)
})
return (
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
@@ -89,40 +67,21 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
marginBottom: "17px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit} disabled={disableDoneButton}>
Done
</VSCodeButton>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: "20px" }}>
<VSCodeTextField
value={apiKey}
style={{ width: "100%" }}
placeholder="Enter your Anthropic API Key"
onInput={handleApiKeyChange}>
<span style={{ fontWeight: "500" }}>Anthropic API Key</span>
</VSCodeTextField>
{apiKeyErrorMessage && (
<div style={{ marginBottom: 5 }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
}}>
{apiKeyErrorMessage}
{apiErrorMessage}
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is not shared with anyone and only used to make API requests from the extension.
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
You can get an API key by signing up here.
</VSCodeLink>
</p>
</div>
<div style={{ marginBottom: "20px" }}>
@@ -130,9 +89,18 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={handleMaxRequestsChange}>
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
requests.
</p>
{maxRequestsErrorMessage && (
<p
style={{
@@ -143,15 +111,6 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
{maxRequestsErrorMessage}
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
requests.
</p>
</div>
<VSCodeDivider />
@@ -163,13 +122,14 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
lineHeight: "1.2",
fontStyle: "italic",
}}>
<p>
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev">
<p style={{ wordWrap: "break-word" }}>
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev" style={{ display: "inline" }}>
https://github.com/saoudrizwan/claude-dev
</VSCodeLink>
</p>
<p style={{ fontStyle: "italic" }}>v1.0.86</p>
</div>
</div>
)

View File

@@ -1,32 +1,27 @@
import React, { useState, useEffect } from "react"
import { VSCodeButton, VSCodeTextField, VSCodeLink, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { validateApiConfiguration } from "../utilities/validate"
import { vscode } from "../utilities/vscode"
import ApiOptions from "./ApiOptions"
interface WelcomeViewProps {
apiKey: string
setApiKey: React.Dispatch<React.SetStateAction<string>>
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState<string | undefined>(undefined)
const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfiguration }) => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiKeyErrorMessage != null
const validateApiKey = (value: string) => {
if (value.trim() === "") {
setApiKeyErrorMessage("API Key cannot be empty")
} else {
setApiKeyErrorMessage(undefined)
}
}
const disableLetsGoButton = apiErrorMessage != null
const handleSubmit = () => {
vscode.postMessage({ type: "apiKey", text: apiKey })
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
}
useEffect(() => {
validateApiKey(apiKey)
}, [apiKey])
setApiErrorMessage(validateApiConfiguration(apiConfiguration))
}, [apiConfiguration])
return (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
@@ -42,35 +37,14 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
files, analyze project source code, and execute terminal commands (with your permission, of course).
</p>
<b>To get started, this extension needs an Anthropic API key:</b>
<ol style={{ paddingLeft: "15px" }}>
<li>
Go to{" "}
<VSCodeLink href="https://console.anthropic.com" style={{ display: "inline" }}>
https://console.anthropic.com
</VSCodeLink>
</li>
<li>You may need to buy some credits (although Anthropic is offering $5 free credit for new users)</li>
<li>Click 'Get API Keys' and create a new key (you can delete it any time)</li>
</ol>
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
<VSCodeDivider />
<div style={{ marginTop: "20px", display: "flex", alignItems: "center" }}>
<VSCodeTextField
style={{ flexGrow: 1, marginRight: "10px" }}
placeholder="Enter API Key..."
value={apiKey}
onInput={(e: any) => setApiKey(e.target.value)}
/>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton}>
Submit
<div style={{ marginTop: "15px" }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
</div>
<p style={{ fontSize: "12px", marginTop: "10px", color: "var(--vscode-descriptionForeground)" }}>
Your API key is stored securely on your computer and used only for interacting with the Anthropic API.
</p>
</div>
)
}

View File

@@ -109,3 +109,23 @@ The above scrollbar styling uses some transparent background color magic to acco
.code-block-scrollable::-webkit-scrollbar-corner {
background-color: transparent;
}
/*
Dropdown label
https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#with-label
*/
.dropdown-container {
box-sizing: border-box;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
justify-content: flex-start;
}
.dropdown-container label {
display: block;
color: var(--vscode-foreground);
cursor: pointer;
font-size: var(--vscode-font-size);
line-height: normal;
margin-bottom: 2px;
}

View File

@@ -0,0 +1,34 @@
import { ApiConfiguration } from "@shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "anthropic":
if (!apiConfiguration.apiKey) {
return "API Key cannot be empty. You must provide an API key to use Claude Dev."
}
break
case "bedrock":
if (!apiConfiguration.awsAccessKey || !apiConfiguration.awsSecretKey || !apiConfiguration.awsRegion) {
return "AWS credentials are incomplete. You must provide an AWS access key, secret key, and region."
}
break
case "openrouter":
if (!apiConfiguration.openRouterApiKey) {
return "API Key cannot be empty. You must provide an API key to use Claude Dev."
}
break
}
}
return undefined
}
export function validateMaxRequestsPerTask(maxRequestsPerTask?: string): string | undefined {
if (maxRequestsPerTask && maxRequestsPerTask.trim()) {
const num = Number(maxRequestsPerTask)
if (isNaN(num) || num < 3 || num > 100) {
return "Maximum requests must be between 3 and 100"
}
}
return undefined
}