mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8770cee90 | ||
|
|
61a431867d | ||
|
|
616aa75a34 | ||
|
|
7cb8f9a4ee |
@@ -6,7 +6,6 @@ import {
|
||||
ApiConfiguration,
|
||||
askSageDefaultURL,
|
||||
askSageModels,
|
||||
azureOpenAiDefaultApiVersion,
|
||||
bedrockDefaultModelId,
|
||||
bedrockModels,
|
||||
cerebrasModels,
|
||||
@@ -18,9 +17,7 @@ import {
|
||||
mainlandQwenModels,
|
||||
ModelInfo,
|
||||
nebiusModels,
|
||||
openAiModelInfoSaneDefaults,
|
||||
openAiNativeModels,
|
||||
sambanovaModels,
|
||||
vertexGlobalModels,
|
||||
vertexModels,
|
||||
xaiModels,
|
||||
@@ -55,6 +52,8 @@ import { OpenRouterProvider } from "./providers/OpenRouterProvider"
|
||||
import { MistralProvider } from "./providers/MistralProvider"
|
||||
import { DeepSeekProvider } from "./providers/DeepSeekProvider"
|
||||
import { TogetherProvider } from "./providers/TogetherProvider"
|
||||
import { OpenAICompatibleProvider } from "./providers/OpenAICompatible"
|
||||
import { SambanovaProvider } from "./providers/SambanovaProvider"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
@@ -128,7 +127,6 @@ const ApiOptions = ({
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [geminiBaseUrlSelected, setGeminiBaseUrlSelected] = useState(!!apiConfiguration?.geminiBaseUrl)
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
|
||||
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
@@ -253,36 +251,6 @@ const ApiOptions = ({
|
||||
)
|
||||
}
|
||||
|
||||
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const debouncedRefreshOpenAiModels = useCallback((baseUrl?: string, apiKey?: string) => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
|
||||
if (baseUrl && apiKey) {
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
ModelsServiceClient.refreshOpenAiModels(
|
||||
OpenAiModelsRequest.create({
|
||||
baseUrl,
|
||||
apiKey,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error("Failed to refresh OpenAI models:", error)
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
|
||||
<DropdownContainer className="dropdown-container">
|
||||
@@ -594,6 +562,24 @@ const ApiOptions = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "openai" && (
|
||||
<OpenAICompatibleProvider
|
||||
apiConfiguration={apiConfiguration}
|
||||
handleInputChange={handleInputChange}
|
||||
showModelOptions={showModelOptions}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "sambanova" && (
|
||||
<SambanovaProvider
|
||||
apiConfiguration={apiConfiguration}
|
||||
handleInputChange={handleInputChange}
|
||||
showModelOptions={showModelOptions}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "bedrock" && (
|
||||
<div
|
||||
style={{
|
||||
@@ -973,341 +959,6 @@ const ApiOptions = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "openai" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiBaseUrl || ""}
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
type="url"
|
||||
onInput={(e: any) => {
|
||||
const baseUrl = e.target.value
|
||||
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
|
||||
|
||||
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
|
||||
}}
|
||||
placeholder={"Enter base URL..."}>
|
||||
<span style={{ fontWeight: 500 }}>Base URL</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiApiKey || ""}
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
type="password"
|
||||
onInput={(e: any) => {
|
||||
const apiKey = e.target.value
|
||||
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
|
||||
|
||||
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
|
||||
}}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiModelId || ""}
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
onInput={handleInputChange("openAiModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
{/* OpenAI Compatible Custom Headers */}
|
||||
{(() => {
|
||||
const headerEntries = Object.entries(apiConfiguration?.openAiHeaders ?? {})
|
||||
return (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: 500 }}>Custom Headers</span>
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
const currentHeaders = { ...(apiConfiguration?.openAiHeaders || {}) }
|
||||
const headerCount = Object.keys(currentHeaders).length
|
||||
const newKey = `header${headerCount + 1}`
|
||||
currentHeaders[newKey] = ""
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: currentHeaders,
|
||||
},
|
||||
})
|
||||
}}>
|
||||
Add Header
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div>
|
||||
{headerEntries.map(([key, value], index) => (
|
||||
<div key={index} style={{ display: "flex", gap: 5, marginTop: 5 }}>
|
||||
<VSCodeTextField
|
||||
value={key}
|
||||
style={{ width: "40%" }}
|
||||
placeholder="Header name"
|
||||
onInput={(e: any) => {
|
||||
const currentHeaders = apiConfiguration?.openAiHeaders ?? {}
|
||||
const newValue = e.target.value
|
||||
if (newValue && newValue !== key) {
|
||||
const { [key]: _, ...rest } = currentHeaders
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: {
|
||||
...rest,
|
||||
[newValue]: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<VSCodeTextField
|
||||
value={value}
|
||||
style={{ width: "40%" }}
|
||||
placeholder="Header value"
|
||||
onInput={(e: any) => {
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiHeaders ?? {}),
|
||||
[key]: e.target.value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={() => {
|
||||
const { [key]: _, ...rest } = apiConfiguration?.openAiHeaders ?? {}
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: rest,
|
||||
},
|
||||
})
|
||||
}}>
|
||||
Remove
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={azureApiVersionSelected}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setAzureApiVersionSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
azureApiVersion: "",
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Set Azure API version
|
||||
</VSCodeCheckbox>
|
||||
{azureApiVersionSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.azureApiVersion || ""}
|
||||
style={{ width: "100%", marginTop: 3 }}
|
||||
onInput={handleInputChange("azureApiVersion")}
|
||||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
display: "flex",
|
||||
margin: "10px 0",
|
||||
cursor: "pointer",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onClick={() => setModelConfigurationSelected((val) => !val)}>
|
||||
<span
|
||||
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Model Configuration
|
||||
</span>
|
||||
</div>
|
||||
{modelConfigurationSelected && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
Supports Images
|
||||
</VSCodeCheckbox>
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
Supports browser use
|
||||
</VSCodeCheckbox>
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.isR1FormatRequired}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
|
||||
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
Enable R1 messages format
|
||||
</VSCodeCheckbox>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.contextWindow
|
||||
? apiConfiguration.openAiModelInfo.contextWindow.toString()
|
||||
: openAiModelInfoSaneDefaults.contextWindow?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.contextWindow = Number(input.target.value)
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.maxTokens
|
||||
? apiConfiguration.openAiModelInfo.maxTokens.toString()
|
||||
: openAiModelInfoSaneDefaults.maxTokens?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.maxTokens = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice
|
||||
? apiConfiguration.openAiModelInfo.inputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.inputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.inputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
? apiConfiguration.openAiModelInfo.outputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.outputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.outputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.temperature
|
||||
? apiConfiguration.openAiModelInfo.temperature.toString()
|
||||
: openAiModelInfoSaneDefaults.temperature?.toString()
|
||||
}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
|
||||
// Check if the input ends with a decimal point or has trailing zeros after decimal
|
||||
const value = input.target.value
|
||||
const shouldPreserveFormat =
|
||||
value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
|
||||
|
||||
modelInfo.temperature =
|
||||
value === ""
|
||||
? openAiModelInfoSaneDefaults.temperature
|
||||
: shouldPreserveFormat
|
||||
? value // Keep as string to preserve decimal format
|
||||
: parseFloat(value)
|
||||
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Temperature</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
|
||||
models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "requesty" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
@@ -1893,37 +1544,6 @@ const ApiOptions = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "sambanova" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sambanovaApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("sambanovaApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>SambaNova API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
{!apiConfiguration?.sambanovaApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://docs.sambanova.ai/cloud/docs/get-started/overview"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a SambaNova API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "cerebras" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
@@ -2118,6 +1738,7 @@ const ApiOptions = ({
|
||||
selectedProvider !== "bedrock" &&
|
||||
selectedProvider !== "mistral" &&
|
||||
selectedProvider !== "deepseek" &&
|
||||
selectedProvider !== "sambanova" &&
|
||||
showModelOptions && (
|
||||
<>
|
||||
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
|
||||
@@ -2137,7 +1758,6 @@ const ApiOptions = ({
|
||||
{selectedProvider === "doubao" && createDropdown(doubaoModels)}
|
||||
{selectedProvider === "asksage" && createDropdown(askSageModels)}
|
||||
{selectedProvider === "xai" && createDropdown(xaiModels)}
|
||||
{selectedProvider === "sambanova" && createDropdown(sambanovaModels)}
|
||||
{selectedProvider === "cerebras" && createDropdown(cerebrasModels)}
|
||||
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
|
||||
{selectedProvider === "sapaicore" && createDropdown(sapAiCoreModels)}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { OpenAiModelsRequest } from "@shared/proto/models"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { VSCodeTextField, VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
|
||||
/**
|
||||
* Props for the OpenAICompatibleProvider component
|
||||
*/
|
||||
interface OpenAICompatibleProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The OpenAI Compatible provider configuration component
|
||||
*/
|
||||
export const OpenAICompatibleProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
}: OpenAICompatibleProviderProps) => {
|
||||
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
|
||||
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const debouncedRefreshOpenAiModels = useCallback((baseUrl?: string, apiKey?: string) => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
|
||||
if (baseUrl && apiKey) {
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
ModelsServiceClient.refreshOpenAiModels(
|
||||
OpenAiModelsRequest.create({
|
||||
baseUrl,
|
||||
apiKey,
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.error("Failed to refresh OpenAI models:", error)
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiBaseUrl || ""}
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
type="url"
|
||||
onInput={(e: any) => {
|
||||
const baseUrl = e.target.value
|
||||
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
|
||||
|
||||
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
|
||||
}}
|
||||
placeholder={"Enter base URL..."}>
|
||||
<span style={{ fontWeight: 500 }}>Base URL</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.openAiApiKey || ""}
|
||||
onChange={(e: any) => {
|
||||
const apiKey = e.target.value
|
||||
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
|
||||
|
||||
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
|
||||
}}
|
||||
providerName="OpenAI Compatible"
|
||||
/>
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiModelId || ""}
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
onInput={handleInputChange("openAiModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
{/* OpenAI Compatible Custom Headers */}
|
||||
{(() => {
|
||||
const headerEntries = Object.entries(apiConfiguration?.openAiHeaders ?? {})
|
||||
return (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: 500 }}>Custom Headers</span>
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
const currentHeaders = { ...(apiConfiguration?.openAiHeaders || {}) }
|
||||
const headerCount = Object.keys(currentHeaders).length
|
||||
const newKey = `header${headerCount + 1}`
|
||||
currentHeaders[newKey] = ""
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: currentHeaders,
|
||||
},
|
||||
})
|
||||
}}>
|
||||
Add Header
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div>
|
||||
{headerEntries.map(([key, value], index) => (
|
||||
<div key={index} style={{ display: "flex", gap: 5, marginTop: 5 }}>
|
||||
<VSCodeTextField
|
||||
value={key}
|
||||
style={{ width: "40%" }}
|
||||
placeholder="Header name"
|
||||
onInput={(e: any) => {
|
||||
const currentHeaders = apiConfiguration?.openAiHeaders ?? {}
|
||||
const newValue = e.target.value
|
||||
if (newValue && newValue !== key) {
|
||||
const { [key]: _, ...rest } = currentHeaders
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: {
|
||||
...rest,
|
||||
[newValue]: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<VSCodeTextField
|
||||
value={value}
|
||||
style={{ width: "40%" }}
|
||||
placeholder="Header value"
|
||||
onInput={(e: any) => {
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiHeaders ?? {}),
|
||||
[key]: e.target.value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={() => {
|
||||
const { [key]: _, ...rest } = apiConfiguration?.openAiHeaders ?? {}
|
||||
handleInputChange("openAiHeaders")({
|
||||
target: {
|
||||
value: rest,
|
||||
},
|
||||
})
|
||||
}}>
|
||||
Remove
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
<BaseUrlField
|
||||
value={apiConfiguration?.azureApiVersion}
|
||||
onChange={(value) => handleInputChange("azureApiVersion")({ target: { value } })}
|
||||
label="Set Azure API version"
|
||||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
display: "flex",
|
||||
margin: "10px 0",
|
||||
cursor: "pointer",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onClick={() => setModelConfigurationSelected((val) => !val)}>
|
||||
<span
|
||||
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Model Configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{modelConfigurationSelected && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
Supports Images
|
||||
</VSCodeCheckbox>
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
Supports browser use
|
||||
</VSCodeCheckbox>
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={!!apiConfiguration?.openAiModelInfo?.isR1FormatRequired}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
|
||||
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
Enable R1 messages format
|
||||
</VSCodeCheckbox>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.contextWindow
|
||||
? apiConfiguration.openAiModelInfo.contextWindow.toString()
|
||||
: openAiModelInfoSaneDefaults.contextWindow?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.contextWindow = Number(input.target.value)
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.maxTokens
|
||||
? apiConfiguration.openAiModelInfo.maxTokens.toString()
|
||||
: openAiModelInfoSaneDefaults.maxTokens?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.maxTokens = input.target.value
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice
|
||||
? apiConfiguration.openAiModelInfo.inputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.inputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.inputPrice = input.target.value
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
? apiConfiguration.openAiModelInfo.outputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.outputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.outputPrice = input.target.value
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.temperature
|
||||
? apiConfiguration.openAiModelInfo.temperature.toString()
|
||||
: openAiModelInfoSaneDefaults.temperature?.toString()
|
||||
}
|
||||
onInput={(input: any) => {
|
||||
const modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
|
||||
// Check if the input ends with a decimal point or has trailing zeros after decimal
|
||||
const value = input.target.value
|
||||
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
|
||||
|
||||
modelInfo.temperature =
|
||||
value === ""
|
||||
? openAiModelInfoSaneDefaults.temperature
|
||||
: shouldPreserveFormat
|
||||
? value // Keep as string to preserve decimal format
|
||||
: parseFloat(value)
|
||||
|
||||
handleInputChange("openAiModelInfo")({
|
||||
target: { value: modelInfo },
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Temperature</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude models.
|
||||
Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{showModelOptions && (
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ApiConfiguration, sambanovaModels } from "@shared/api"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
|
||||
/**
|
||||
* Props for the SambanovaProvider component
|
||||
*/
|
||||
interface SambanovaProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The SambaNova provider configuration component
|
||||
*/
|
||||
export const SambanovaProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SambanovaProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.sambanovaApiKey || ""}
|
||||
onChange={handleInputChange("sambanovaApiKey")}
|
||||
providerName="SambaNova"
|
||||
signupUrl="https://docs.sambanova.ai/cloud/docs/get-started/overview"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={sambanovaModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user