Add openai compatible provider

This commit is contained in:
Saoud Rizwan
2024-09-03 17:08:29 -04:00
parent 0badfa2706
commit c209198b23
14 changed files with 383 additions and 187 deletions

View File

@@ -2,12 +2,12 @@ import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vsco
import React, { useMemo } from "react"
import {
ApiConfiguration,
ApiModelId,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
openAiModelInfoSaneDefaults,
openRouterDefaultModelId,
openRouterModels,
vertexDefaultModelId,
@@ -69,11 +69,16 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
<VSCodeDropdown
id="api-provider"
value={selectedProvider}
onChange={handleInputChange("apiProvider")}
style={{ minWidth: 125 }}>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
</VSCodeDropdown>
</div>
@@ -256,6 +261,47 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
</div>
)}
{selectedProvider === "openai" && (
<div>
<VSCodeTextField
value={apiConfiguration?.openAiBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("openAiBaseUrl")}
placeholder={"e.g. http://localhost:11434"}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.openAiApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openAiApiKey")}
placeholder="e.g. ollama">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.openAiModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("openAiModelId")}
placeholder={"e.g. llama3.1"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
You can use any OpenAI compatible API with models that support tool use.{" "}
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Claude Dev uses complex prompts, so results
may vary depending on the quality of the model you choose. Less capable models may not work
as expected.)
</span>
</p>
</div>
)}
{apiErrorMessage && (
<p
style={{
@@ -267,7 +313,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
</p>
)}
{showModelOptions && (
{selectedProvider !== "openai" && showModelOptions && (
<>
<div className="dropdown-container">
<label htmlFor="model-id">
@@ -365,8 +411,8 @@ 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
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
if (modelId && modelId in models) {
selectedModelId = modelId
@@ -386,6 +432,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId)
case "openai":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openAiModelId ?? "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}

View File

@@ -497,9 +497,6 @@ const ChatView = ({
cacheReads={apiMetrics.totalCacheReads}
totalCost={apiMetrics.totalCost}
onClose={handleTaskCloseButtonClick}
isHidden={isHidden}
vscodeUriScheme={uriScheme}
apiProvider={apiConfiguration?.apiProvider}
/>
) : (
<>

View File

@@ -108,17 +108,21 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
<span>
Tokens: {item.tokensIn?.toLocaleString()} {item.tokensOut?.toLocaleString()}
</span>
{" • "}
{item.cacheWrites && item.cacheReads && (
<>
{" • "}
<span>
Cache: +{item.cacheWrites?.toLocaleString()} {" "}
{item.cacheReads?.toLocaleString()}
</span>
{" • "}
</>
)}
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
{!!item.totalCost && (
<>
{" • "}
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
</>
)}
</div>
</div>
</div>

View File

@@ -63,6 +63,17 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)
}
const ExportButton = ({ itemId }: { itemId: string }) => (
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
handleExportMd(itemId)
}}>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT .MD</div>
</VSCodeButton>
)
return (
<>
<style>
@@ -216,52 +227,61 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
Tokens:
</span>
<span
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
gap: "4px",
flexWrap: "wrap",
}}>
<i
className="codicon codicon-arrow-up"
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{item.tokensIn?.toLocaleString()}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-down"
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
Tokens:
</span>
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{item.tokensOut?.toLocaleString()}
</span>
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-up"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{item.tokensIn?.toLocaleString()}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-down"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{item.tokensOut?.toLocaleString()}
</span>
</div>
{!item.totalCost && <ExportButton itemId={item.id} />}
</div>
{item.cacheWrites && item.cacheReads && (
<div
style={{
@@ -313,36 +333,29 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
</span>
</div>
)}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: -2,
}}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
API Cost:
</span>
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
${item.totalCost?.toFixed(4)}
</span>
</div>
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
handleExportMd(item.id)
{!!item.totalCost && (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: -2,
}}>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>
EXPORT .MD
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
API Cost:
</span>
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
${item.totalCost?.toFixed(4)}
</span>
</div>
</VSCodeButton>
</div>
<ExportButton itemId={item.id} />
</div>
)}
</div>
</div>
</div>

View File

@@ -1,9 +1,4 @@
import {
VSCodeButton,
VSCodeCheckbox,
VSCodeLink,
VSCodeTextArea
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import { useExtensionState } from "../context/ExtensionStateContext"
import { validateApiConfiguration } from "../utils/validate"

View File

@@ -1,8 +1,8 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useRef, useState } from "react"
import { useWindowSize } from "react-use"
import { ApiProvider } from "../../../src/shared/api"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../context/ExtensionStateContext"
import { vscode } from "../utils/vscode"
import Thumbnails from "./Thumbnails"
@@ -15,9 +15,6 @@ interface TaskHeaderProps {
cacheReads?: number
totalCost: number
onClose: () => void
isHidden: boolean
vscodeUriScheme?: string
apiProvider?: ApiProvider
}
const TaskHeader: React.FC<TaskHeaderProps> = ({
@@ -29,10 +26,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
cacheReads,
totalCost,
onClose,
isHidden,
vscodeUriScheme,
apiProvider,
}) => {
const { apiConfiguration } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -100,6 +95,18 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
vscode.postMessage({ type: "exportCurrentTask" })
}
const ExportButton = () => (
<VSCodeButton
appearance="icon"
onClick={handleDownload}
style={{
marginBottom: "-2px",
marginRight: "-2.5px",
}}>
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
</VSCodeButton>
)
return (
<div style={{ padding: "10px 13px 10px 13px" }}>
<div
@@ -196,23 +203,32 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)}
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-up"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensIn?.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-down"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensOut?.toLocaleString()}
</span>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-up"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensIn?.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-down"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensOut?.toLocaleString()}
</span>
</div>
{apiConfiguration?.apiProvider === "openai" && <ExportButton />}
</div>
{(doesModelSupportPromptCache || cacheReads !== undefined || cacheWrites !== undefined) && (
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
@@ -232,26 +248,20 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
</span>
</div>
)}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost?.toFixed(4)}</span>
</div>
<VSCodeButton
appearance="icon"
onClick={handleDownload}
{apiConfiguration?.apiProvider !== "openai" && (
<div
style={{
marginBottom: "-2px",
marginRight: "-2.5px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
</VSCodeButton>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost?.toFixed(4)}</span>
</div>
<ExportButton />
</div>
)}
</div>
</div>
{/* {apiProvider === "kodu" && (

View File

@@ -31,9 +31,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState(message.state)
const config = message.state?.apiConfiguration
const hasKey = config
? [config.apiKey, config.openRouterApiKey, config.awsRegion, config.vertexProjectId].some(
(key) => key !== undefined
)
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)

View File

@@ -23,6 +23,15 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid Google Cloud Project ID and Region."
}
break
case "openai":
if (
!apiConfiguration.openAiBaseUrl ||
!apiConfiguration.openAiApiKey ||
!apiConfiguration.openAiModelId
) {
return "You must provide a valid base URL, API key, and model ID."
}
break
}
}
return undefined