Compare commits

...

6 Commits

Author SHA1 Message Date
frostbournesb c8c0033bf7 Merge remote-tracking branch 'origin/main' into frostbourne/eng-503 2025-04-18 17:57:48 -07:00
frostbournesb 8bf86592f0 ModelPicker cleanup 2025-04-18 17:53:52 -07:00
frostbournesb c08ee165ee changeset 2025-04-18 05:44:01 -07:00
frostbournesb 85d723db29 fix local models options, more cleanup 2025-04-18 05:43:50 -07:00
frostbournesb 8397aee744 cleanup 2025-04-18 04:21:21 -07:00
frostbournesb c921a7c2b3 first pass 2025-04-17 21:18:31 -07:00
46 changed files with 2531 additions and 2336 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Break up ApiOptions.tsx, simplifying provider adding process, and clean up styles for each provider option
@@ -28,13 +28,14 @@ import { vscode } from "@/utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import Thumbnails from "@/components/common/Thumbnails"
import Tooltip from "@/components/common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import ApiOptions from "@/components/settings/ApiOptions/ApiOptions"
import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
import ContextMenu from "@/components/chat/ContextMenu"
import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
import { ChatSettings } from "@shared/ChatSettings"
import ServersToggleModal from "./ServersToggleModal"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
import { normalizeApiConfiguration } from "@/utils/providers"
interface ChatTextAreaProps {
inputValue: string
+1 -1
View File
@@ -19,7 +19,6 @@ import { getApiMetrics } from "@shared/getApiMetrics"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import Announcement from "@/components/chat/Announcement"
import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
@@ -27,6 +26,7 @@ import ChatRow from "@/components/chat/ChatRow"
import ChatTextArea from "@/components/chat/ChatTextArea"
import TaskHeader from "@/components/chat/TaskHeader"
import TelemetryBanner from "@/components/common/TelemetryBanner"
import { normalizeApiConfiguration } from "@/utils/providers"
interface ChatViewProps {
isHidden: boolean
@@ -8,7 +8,7 @@ import { formatLargeNumber } from "@/utils/format"
import { formatSize } from "@/utils/format"
import { vscode } from "@/utils/vscode"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { normalizeApiConfiguration } from "@/utils/providers"
import { validateSlashCommand } from "@/utils/slash-commands"
interface TaskHeaderProps {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,141 @@
import { memo, useCallback, useMemo } from "react"
import { ApiConfiguration } from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import OpenRouterModelPicker from "./model/OpenRouterModelPicker"
import RequestyModelPicker from "./model/RequestyModelPicker"
import ProviderSelectDropdown from "./ProviderSelectDropdown"
import { normalizeApiConfiguration } from "@/utils/providers"
import * as ProviderOptions from "./providers"
import OpenRouterProviderSorter from "./OpenRouterProviderSorter"
import ModelPicker from "./model/ModelPicker"
interface ApiOptionsProps {
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
}
interface ProviderOptionKey {
id: string
component: keyof typeof ProviderOptions
}
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup }: ApiOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
const newValue = event.target.value
// Update local state
setApiConfiguration({
...apiConfiguration,
[field]: newValue,
})
// If the field is the provider, save it immediately
// Necessary for favorite model selection to work without undoing provider changes
if (field === "apiProvider") {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: {
...apiConfiguration,
apiProvider: newValue,
},
})
}
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const providerOptionsList: ProviderOptionKey[] = [
{ id: "cline", component: "ClineOptions" },
{ id: "asksage", component: "AskSageOptions" },
{ id: "anthropic", component: "AnthropicOptions" },
{ id: "openai-native", component: "OpenAIOptions" },
{ id: "deepseek", component: "DeepseekOptions" },
{ id: "qwen", component: "QwenOptions" },
{ id: "doubao", component: "DoubaoOptions" },
{ id: "mistral", component: "MistralOptions" },
{ id: "openrouter", component: "OpenRouterOptions" },
{ id: "bedrock", component: "BedrockOptions" },
{ id: "vertex", component: "VertexOptions" },
{ id: "gemini", component: "GeminiOptions" },
{ id: "openai", component: "OpenAICompatOptions" },
{ id: "requesty", component: "RequestyOptions" },
{ id: "together", component: "TogetherOptions" },
{ id: "vscode-lm", component: "VscodeLMOptions" },
{ id: "lmstudio", component: "LMStudioOptions" },
{ id: "litellm", component: "LiteLLMOptions" },
{ id: "ollama", component: "OllamaOptions" },
{ id: "xai", component: "XAIOptions" },
{ id: "sambanova", component: "SambaNovaOptions" },
]
// Render the provider options based on the selected provider
const renderProviderOptions = useCallback(() => {
const providerOption = providerOptionsList.find((option) => option.id === selectedProvider)
if (!providerOption) return null
const ProviderOptionsComponent = ProviderOptions[providerOption.component]
return <ProviderOptionsComponent handleInputChange={handleInputChange} />
}, [selectedProvider, handleInputChange])
const usesSpecialModelPickers = ["openrouter", "cline", "openai", "ollama", "lmstudio", "vscode-lm", "litellm", "requesty"]
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
<ProviderSelectDropdown selectedProvider={selectedProvider} onChange={handleInputChange("apiProvider")} />
{renderProviderOptions()}
{apiErrorMessage && (
<p
style={{
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
<>
<OpenRouterProviderSorter />
<OpenRouterModelPicker isPopup={isPopup} />
</>
)}
{selectedProvider === "requesty" && showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
{/* Default model picker for providers that aren't handled separately */}
{!(selectedProvider in usesSpecialModelPickers) && showModelOptions && (
<ModelPicker
selectedProvider={selectedProvider}
selectedModelId={selectedModelId}
selectedModelInfo={selectedModelInfo}
isPopup={isPopup}
handleInputChange={handleInputChange}
/>
)}
{modelIdErrorMessage && (
<p
style={{
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
{modelIdErrorMessage}
</p>
)}
</div>
)
}
export default memo(ApiOptions)
@@ -0,0 +1,18 @@
import styled from "styled-components"
// Higher than the OpenRouterModelPicker's z-index
export const DROPDOWN_Z_INDEX = 1002
const DropdownContainer = styled.div<{ zIndex?: number }>`
position: relative;
z-index: ${(props) => props.zIndex || DROPDOWN_Z_INDEX};
// Force dropdowns to open downward
& vscode-dropdown::part(listbox) {
position: absolute !important;
top: 100% !important;
bottom: auto !important;
}
`
export default DropdownContainer
@@ -0,0 +1,62 @@
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useState } from "react"
import DropdownContainer from "./DropdownContainer"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./model/OpenRouterModelPicker"
import { useExtensionState } from "@/context/ExtensionStateContext"
const OpenRouterProviderSorter = () => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
return (
<>
<VSCodeCheckbox
style={{ marginTop: -10 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: "",
})
}
}}>
Sort underlying provider routing
</VSCodeCheckbox>
{providerSortingSelected && (
<div style={{ marginBottom: -6 }}>
<DropdownContainer className="dropdown-container" zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX + 1}>
<VSCodeDropdown
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: e.target.value,
})
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
<VSCodeOption value="throughput">Throughput</VSCodeOption>
<VSCodeOption value="latency">Latency</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
{!apiConfiguration?.openRouterProviderSorting &&
"Default behavior is to load balance requests across providers (like AWS, Google Vertex, Anthropic), prioritizing price while considering provider uptime"}
{apiConfiguration?.openRouterProviderSorting === "price" &&
"Sort providers by price, prioritizing the lowest cost provider"}
{apiConfiguration?.openRouterProviderSorting === "throughput" &&
"Sort providers by throughput, prioritizing the provider with the highest throughput (may increase cost)"}
{apiConfiguration?.openRouterProviderSorting === "latency" &&
"Sort providers by response time, prioritizing the provider with the lowest latency"}
</p>
</div>
)}
</>
)
}
export default memo(OpenRouterProviderSorter)
@@ -0,0 +1,59 @@
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { ApiProvider } from "@shared/api"
import DropdownContainer from "./DropdownContainer"
// Provider list array with name and id
export const providerList = [
{ id: "cline", name: "Cline" },
{ id: "openrouter", name: "OpenRouter" },
{ id: "anthropic", name: "Anthropic" },
{ id: "bedrock", name: "AWS Bedrock" },
{ id: "openai", name: "OpenAI Compatible" },
{ id: "vertex", name: "GCP Vertex AI" },
{ id: "gemini", name: "Google Gemini" },
{ id: "deepseek", name: "DeepSeek" },
{ id: "mistral", name: "Mistral" },
{ id: "openai-native", name: "OpenAI" },
{ id: "vscode-lm", name: "VS Code LM API" },
{ id: "requesty", name: "Requesty" },
{ id: "together", name: "Together" },
{ id: "qwen", name: "Alibaba Qwen" },
{ id: "doubao", name: "Bytedance Doubao" },
{ id: "lmstudio", name: "LM Studio" },
{ id: "ollama", name: "Ollama" },
{ id: "litellm", name: "LiteLLM" },
{ id: "asksage", name: "AskSage" },
{ id: "xai", name: "xAI" },
{ id: "sambanova", name: "SambaNova" },
]
interface ProviderDropdownProps {
selectedProvider: ApiProvider
onChange: (event: any) => void
}
const ProviderSelectDropdown = ({ selectedProvider, onChange }: ProviderDropdownProps) => {
return (
<DropdownContainer className="dropdown-container mb-2">
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown
id="api-provider"
value={selectedProvider}
onChange={onChange}
style={{
minWidth: 130,
position: "relative",
}}>
{providerList.map((provider) => (
<VSCodeOption key={provider.id} value={provider.id}>
{provider.name}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
)
}
export default ProviderSelectDropdown
@@ -0,0 +1,132 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useRef, useState } from "react"
import { useRemark } from "react-remark"
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
interface ModelDescriptionMarkdownProps {
markdown?: string
key: string
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}
const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 12px;
color: var(--vscode-descriptionForeground);
p,
li,
ol,
ul {
line-height: 1.25;
margin: 0;
}
ol,
ul {
padding-left: 1.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
text-decoration: none;
}
a {
&:hover {
text-decoration: underline;
}
}
`
const ModelDescriptionMarkdown = ({ markdown, key, isExpanded, setIsExpanded, isPopup }: ModelDescriptionMarkdownProps) => {
const [reactContent, setMarkdown] = useRemark()
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMarkdown(markdown || "")
}, [markdown, setMarkdown])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
const { scrollHeight } = textRef.current
const { clientHeight } = textContainerRef.current
const isOverflowing = scrollHeight > clientHeight
setShowSeeMore(isOverflowing)
}
}, [reactContent, setIsExpanded])
return (
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
overflowY: isExpanded ? "auto" : "hidden",
position: "relative",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}>
{reactContent}
</div>
{!isExpanded && showSeeMore && (
<div
style={{
position: "absolute",
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
}}>
<div
style={{
width: 30,
height: "1.2em",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
style={{
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more
</VSCodeLink>
</div>
)}
</div>
</StyledMarkdown>
)
}
export default memo(ModelDescriptionMarkdown)
@@ -0,0 +1,29 @@
import { memo } from "react"
interface ModelInfoSupportsItemProps {
isSupported: boolean
supportsLabel: string
doesNotSupportLabel: string
}
const ModelInfoSupportsItem = ({ isSupported, supportsLabel, doesNotSupportLabel }: ModelInfoSupportsItemProps) => (
<span
style={{
fontWeight: 500,
color: isSupported ? "var(--vscode-charts-green)" : "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 default memo(ModelInfoSupportsItem)
@@ -0,0 +1,139 @@
import { Fragment, memo } from "react"
import { ModelInfo, geminiModels } from "@shared/api"
import { formatTiers } from "@/utils/providers"
import { formatPrice } from "@/utils/format"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import ModelDescriptionMarkdown from "./ModelDescriptionMarkdown"
import ModelInfoSupportsItem from "./ModelInfoSupportsItem"
interface ModelInfoViewProps {
selectedModelId: string
modelInfo: ModelInfo
isDescriptionExpanded: boolean
setIsDescriptionExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}
const ModelInfoView = ({
selectedModelId,
modelInfo,
isDescriptionExpanded,
setIsDescriptionExpanded,
isPopup,
}: ModelInfoViewProps) => {
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
// Create elements for tiered pricing separately
const inputPriceElement = modelInfo.inputPriceTiers ? (
<Fragment key="inputPriceTiers">
<span style={{ fontWeight: 500 }}>Input price:</span>
<br />
{formatTiers(modelInfo.inputPriceTiers).map((tierString, i, arr) => (
<Fragment key={`inputTierFrag${i}`}>
<span style={{ paddingLeft: "15px" }}>{tierString}</span>
{i < arr.length - 1 && <br />}
</Fragment>
))}
</Fragment>
) : modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 ? (
<span key="inputPrice">
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
</span>
) : null
const outputPriceElement = modelInfo.outputPriceTiers ? (
<Fragment key="outputPriceTiers">
<span style={{ fontWeight: 500 }}>Output price:</span>
<span style={{ fontStyle: "italic" }}> (based on input tokens)</span>
<br />
{formatTiers(modelInfo.outputPriceTiers).map((tierString, i, arr) => (
<Fragment key={`outputTierFrag${i}`}>
<span style={{ paddingLeft: "15px" }}>{tierString}</span>
{i < arr.length - 1 && <br />}
</Fragment>
))}
</Fragment>
) : modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 ? (
<span key="outputPrice">
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
</span>
) : null
const infoItems = [
modelInfo.description && (
<ModelDescriptionMarkdown
key="description"
markdown={modelInfo.description}
isExpanded={isDescriptionExpanded}
setIsExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
),
<ModelInfoSupportsItem
key="supportsImages"
isSupported={modelInfo.supportsImages ?? false}
supportsLabel="Supports images"
doesNotSupportLabel="Does not support images"
/>,
<ModelInfoSupportsItem
key="supportsComputerUse"
isSupported={modelInfo.supportsComputerUse ?? false}
supportsLabel="Supports computer use"
doesNotSupportLabel="Does not support computer use"
/>,
!isGemini && (
<ModelInfoSupportsItem
key="supportsPromptCache"
isSupported={modelInfo.supportsPromptCache}
supportsLabel="Supports prompt caching"
doesNotSupportLabel="Does not support prompt caching"
/>
),
modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && (
<span key="maxTokens">
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens?.toLocaleString()} tokens
</span>
),
inputPriceElement, // Add the generated input price block
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
<span key="cacheWritesPrice">
<span style={{ fontWeight: 500 }}>Cache writes price:</span> {formatPrice(modelInfo.cacheWritesPrice || 0)}
/million tokens
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (
<span key="cacheReadsPrice">
<span style={{ fontWeight: 500 }}>Cache reads price:</span> {formatPrice(modelInfo.cacheReadsPrice || 0)}/million
tokens
</span>
),
outputPriceElement, // Add the generated output price block
isGemini && (
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that,
billing depends on prompt size.{" "}
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
For more info, see pricing details.
</VSCodeLink>
</span>
),
].filter(Boolean)
return (
<p
style={{
fontSize: "12px",
marginTop: "2px",
color: "var(--vscode-descriptionForeground)",
}}>
{infoItems.map((item, index) => (
<Fragment key={index}>
{item}
{index < infoItems.length - 1 && <br />}
</Fragment>
))}
</p>
)
}
export default memo(ModelInfoView)
@@ -0,0 +1,175 @@
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useState } from "react"
import {
ApiConfiguration,
ModelInfo,
anthropicModels,
bedrockModels,
vertexModels,
geminiModels,
openAiNativeModels,
deepSeekModels,
mainlandQwenModels,
internationalQwenModels,
doubaoModels,
mistralModels,
askSageModels,
xaiModels,
sambanovaModels,
} from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import DropdownContainer from "../DropdownContainer"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import ModelInfoView from "./ModelInfoView"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX - 1
interface ModelPickerProps {
selectedProvider: string
selectedModelId: string
selectedModelInfo: any
isPopup?: boolean
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
type ModelList = Record<string, ModelInfo>
type ModelMap = Record<string, ModelList>
const ModelPicker = ({ selectedProvider, selectedModelId, selectedModelInfo, isPopup, handleInputChange }: ModelPickerProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!apiConfiguration?.reasoningEffort)
const showThinkingBudgetSlider =
(selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219") ||
(selectedProvider === "bedrock" && selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(selectedProvider === "vertex" && selectedModelId === "claude-3-7-sonnet@20250219")
const showReasoningEffort = selectedProvider === "xai" && selectedModelId.includes("3-mini")
const createModelDropdown = () => {
let models: ModelList = {}
const modelMap: ModelMap = {
anthropic: anthropicModels,
bedrock: bedrockModels,
vertex: vertexModels,
gemini: geminiModels,
"openai-native": openAiNativeModels,
deepseek: deepSeekModels,
doubao: doubaoModels,
mistral: mistralModels,
asksage: askSageModels,
xai: xaiModels,
sambanova: sambanovaModels,
}
// Handle special case for qwen separately
if (selectedProvider === "qwen") {
models = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
} else {
models = modelMap[selectedProvider] || {}
}
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 (
<>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{createModelDropdown()}
</DropdownContainer>
{showThinkingBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
{showReasoningEffort && (
<>
<VSCodeCheckbox
style={{ marginTop: 0 }}
checked={reasoningEffortSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setReasoningEffortSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: "",
})
}
}}>
Modify reasoning effort
</VSCodeCheckbox>
{reasoningEffortSelected && (
<div>
<label htmlFor="reasoning-effort-dropdown">
<span style={{}}>Reasoning Effort</span>
</label>
<DropdownContainer className="dropdown-container" zIndex={DROPDOWN_Z_INDEX - 100}>
<VSCodeDropdown
id="reasoning-effort-dropdown"
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.reasoningEffort || "high"}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: e.target.value,
})
}}>
<VSCodeOption value="low">low</VSCodeOption>
<VSCodeOption value="high">high</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
marginBottom: 0,
color: "var(--vscode-descriptionForeground)",
}}>
High effort may produce more thorough analysis but takes longer and uses more tokens.
</p>
</div>
)}
</>
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
</>
)
}
export default memo(ModelPicker)
@@ -5,7 +5,7 @@ import { useRemark } from "react-remark"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { highlight } from "../history/HistoryView"
import { highlight } from "@/components/history/HistoryView"
const OpenAiModelPicker: React.FC = () => {
const { apiConfiguration, setApiConfiguration, openAiModels } = useExtensionState()
@@ -1,17 +1,16 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark"
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import { useMount } from "react-use"
import styled from "styled-components"
import { openRouterDefaultModelId } from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { highlight } from "../../../history/HistoryView"
import ModelInfoView from "./ModelInfoView"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import FeaturedModelCard from "./FeaturedModelCard"
import FeaturedModelCard from "../../FeaturedModelCard"
import { normalizeApiConfiguration } from "@/utils/providers"
// Star icon for favorites
const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: React.MouseEvent) => void }) => {
@@ -212,7 +211,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10 }}>
<label htmlFor="model-search">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
@@ -381,158 +380,3 @@ const DropdownItem = styled.div<{ isSelected: boolean }>`
background-color: var(--vscode-list-activeSelectionBackground);
}
`
// Markdown
const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 12px;
color: var(--vscode-descriptionForeground);
p,
li,
ol,
ul {
line-height: 1.25;
margin: 0;
}
ol,
ul {
padding-left: 1.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
text-decoration: none;
}
a {
&:hover {
text-decoration: underline;
}
}
`
export const ModelDescriptionMarkdown = memo(
({
markdown,
key,
isExpanded,
setIsExpanded,
isPopup,
}: {
markdown?: string
key: string
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}) => {
const [reactContent, setMarkdown] = useRemark()
// const [isExpanded, setIsExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMarkdown(markdown || "")
}, [markdown, setMarkdown])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
const { scrollHeight } = textRef.current
const { clientHeight } = textContainerRef.current
const isOverflowing = scrollHeight > clientHeight
setShowSeeMore(isOverflowing)
// if (!isOverflowing) {
// setIsExpanded(false)
// }
}
}, [reactContent, setIsExpanded])
return (
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
overflowY: isExpanded ? "auto" : "hidden",
position: "relative",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
// whiteSpace: "pre-wrap",
// wordBreak: "break-word",
// overflowWrap: "anywhere",
}}>
{reactContent}
</div>
{!isExpanded && showSeeMore && (
<div
style={{
position: "absolute",
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
}}>
<div
style={{
width: 30,
height: "1.2em",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
style={{
// cursor: "pointer",
// color: "var(--vscode-textLink-foreground)",
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more
</VSCodeLink>
</div>
)}
</div>
{/* {isExpanded && showSeeMore && (
<div
style={{
cursor: "pointer",
color: "var(--vscode-textLink-foreground)",
marginLeft: "auto",
textAlign: "right",
paddingRight: 2,
}}
onClick={() => setIsExpanded(false)}>
See less
</div>
)} */}
</StyledMarkdown>
)
},
)
@@ -4,13 +4,14 @@ import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from
import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId } from "../../../../src/shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { requestyDefaultModelId } from "../../../../../../src/shared/api"
import { useExtensionState } from "../../../../context/ExtensionStateContext"
import { vscode } from "../../../../utils/vscode"
import { highlight } from "../../../history/HistoryView"
import ModelInfoView from "./ModelInfoView"
import { CODE_BLOCK_BG_COLOR } from "../../../common/CodeBlock"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "@/utils/providers"
export interface RequestyModelPickerProps {
isPopup?: boolean
@@ -145,7 +146,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
}, [selectedModelId])
return (
<div style={{ width: "100%" }}>
<div style={{ width: "100%", marginTop: 10 }}>
<style>
{`
.model-item-highlight {
@@ -0,0 +1,68 @@
import { VSCodeCheckbox, VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const AnthropicOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
return (
<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>
<VSCodeCheckbox
checked={anthropicBaseUrlSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setAnthropicBaseUrlSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
anthropicBaseUrl: "",
})
}
}}>
Use custom base URL
</VSCodeCheckbox>
{anthropicBaseUrlSelected && (
<VSCodeTextField
value={apiConfiguration?.anthropicBaseUrl || ""}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={handleInputChange("anthropicBaseUrl")}
placeholder="Default: https://api.anthropic.com"
/>
)}
<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?.apiKey && (
<VSCodeLink
href="https://console.anthropic.com/settings/keys"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get an Anthropic API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default AnthropicOptions
@@ -0,0 +1,39 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { askSageDefaultURL } from "@shared/api"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const AskSageOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.asksageApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("asksageApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>AskSage 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.
</p>
<VSCodeTextField
value={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("asksageApiUrl")}
placeholder="Enter AskSage API URL...">
<span style={{ fontWeight: 500 }}>AskSage API URL</span>
</VSCodeTextField>
</div>
)
}
export default AskSageOptions
@@ -0,0 +1,187 @@
import {
VSCodeTextField,
VSCodeRadioGroup,
VSCodeRadio,
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
} from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import DropdownContainer from "../DropdownContainer"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../model/OpenRouterModelPicker"
import { useState } from "react"
const BedrockOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 5,
}}>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
<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>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</>
)}
<DropdownContainer zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX - 1} 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-central-2">eu-central-2</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>
<VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption>
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption>
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</DropdownContainer>
<div style={{ display: "flex", flexDirection: "column" }}>
<VSCodeCheckbox
checked={awsEndpointSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
<VSCodeCheckbox
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsUseCrossRegionInference: isChecked,
})
}}>
Use cross-region inference
</VSCodeCheckbox>
<VSCodeCheckbox
checked={apiConfiguration?.awsBedrockUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsBedrockUsePromptCache: isChecked,
})
}}>
Use prompt caching
</VSCodeCheckbox>
</div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
{apiConfiguration?.awsUseProfile ? (
<>
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
profile. These credentials are only used locally to make API requests from this extension.
</>
) : (
<>
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
from this extension.
</>
)}
</p>
</div>
)
}
export default BedrockOptions
@@ -0,0 +1,11 @@
import { ClineAccountInfoCard } from "../../ClineAccountInfoCard"
const ClineOptions = () => {
return (
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
)
}
export default ClineOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const DeepseekOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.deepSeekApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>DeepSeek 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?.deepSeekApiKey && (
<VSCodeLink
href="https://www.deepseek.com/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a DeepSeek API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default DeepseekOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const DoubaoOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.doubaoApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("doubaoApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Doubao 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?.doubaoApiKey && (
<VSCodeLink
href="https://console.volcengine.com/home"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Doubao API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default DoubaoOptions
@@ -0,0 +1,68 @@
import { VSCodeTextField, VSCodeLink, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import { useState } from "react"
const GeminiOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [geminiBaseUrlSelected, setGeminiBaseUrlSelected] = useState(!!apiConfiguration?.geminiBaseUrl)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.geminiApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("geminiApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Gemini API Key</span>
</VSCodeTextField>
<VSCodeCheckbox
checked={geminiBaseUrlSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setGeminiBaseUrlSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
geminiBaseUrl: "",
})
}
}}>
Use custom base URL
</VSCodeCheckbox>
{geminiBaseUrlSelected && (
<VSCodeTextField
value={apiConfiguration?.geminiBaseUrl || ""}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={handleInputChange("geminiBaseUrl")}
placeholder="Default: https://generativelanguage.googleapis.com"
/>
)}
<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?.geminiApiKey && (
<VSCodeLink
href="https://aistudio.google.com/apikey"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Gemini API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default GeminiOptions
@@ -0,0 +1,80 @@
import { VSCodeTextField, VSCodeLink, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import { useCallback, useEffect, useState } from "react"
import { useEvent, useInterval } from "react-use"
import { vscode } from "@/utils/vscode"
import type { ExtensionMessage } from "@shared/ExtensionMessage"
const LMStudioOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
// Request LM Studio models
const requestLmStudioModels = useCallback(() => {
vscode.postMessage({
type: "requestLmStudioModels",
text: apiConfiguration?.lmStudioBaseUrl,
})
}, [apiConfiguration?.lmStudioBaseUrl])
// Request LM Studio models when component mounts
useEffect(() => {
requestLmStudioModels()
}, [requestLmStudioModels])
// Poll LM Studio models periodically
useInterval(requestLmStudioModels, 2000)
// Handle message events for LM Studio models
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "lmStudioModels" && message.lmStudioModels) {
setLmStudioModels(message.lmStudioModels)
}
}, [])
useEvent("message", handleMessage)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.lmStudioBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("lmStudioBaseUrl")}
placeholder={"Default: http://localhost:1234"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
style={{ width: "100%", marginTop: 10 }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LM Studio allows you to run models locally on your computer. For instructions on how to get started, see their
<VSCodeLink href="https://lmstudio.ai/docs" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
You will also need to start LM Studio's{" "}
<VSCodeLink href="https://lmstudio.ai/docs/basics/server" style={{ display: "inline", fontSize: "inherit" }}>
local server
</VSCodeLink>{" "}
feature to use it with this extension.{" "}
<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>
)
}
export default LMStudioOptions
@@ -0,0 +1,84 @@
import { VSCodeTextField, VSCodeLink, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import ThinkingBudgetSlider from "../model/ThinkingBudgetSlider"
const LiteLLMOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%", marginTop: 10 }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%", marginTop: 10 }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. gpt-4"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
<VSCodeCheckbox
checked={apiConfiguration?.liteLlmUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
liteLlmUsePromptCache: isChecked,
})
}}
style={{ fontWeight: 500, color: "var(--vscode-charts-green)" }}>
Use prompt caching (GA)
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-charts-green)" }}>
Prompt caching requires a supported provider and model
</p>
</div>
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Extended thinking is available for models as Sonnet-3-7, o3-mini, Deepseek R1, etc. More info on{" "}
<VSCodeLink
href="https://docs.litellm.ai/docs/reasoning_content"
style={{ display: "inline", fontSize: "inherit" }}>
thinking mode configuration
</VSCodeLink>
</p>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
</div>
)
}
export default LiteLLMOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const MistralOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.mistralApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("mistralApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Mistral 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?.mistralApiKey && (
<VSCodeLink
href="https://console.mistral.ai/codestral"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Mistral API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default MistralOptions
@@ -0,0 +1,84 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import { useCallback, useEffect, useState } from "react"
import { useEvent, useInterval } from "react-use"
import { vscode } from "@/utils/vscode"
import type { ExtensionMessage } from "@shared/ExtensionMessage"
const OllamaOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
// Request Ollama models
const requestOllamaModels = useCallback(() => {
vscode.postMessage({
type: "requestOllamaModels",
text: apiConfiguration?.ollamaBaseUrl,
})
}, [apiConfiguration?.ollamaBaseUrl])
// Request Ollama models when component mounts
useEffect(() => {
requestOllamaModels()
}, [requestOllamaModels])
// Poll Ollama models periodically
useInterval(requestOllamaModels, 2000)
// Handle message events for Ollama models
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "ollamaModels" && message.ollamaModels) {
setOllamaModels(message.ollamaModels)
}
}, [])
useEvent("message", handleMessage)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.ollamaBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("ollamaBaseUrl")}
placeholder={"Default: http://localhost:11434"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaModelId || ""}
style={{ width: "100%", marginTop: 10 }}
onInput={handleInputChange("ollamaModelId")}
placeholder={"e.g. llama3.1"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
style={{ width: "100%", marginTop: 10 }}
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
placeholder={"e.g. 32768"}>
<span style={{ fontWeight: 500 }}>Model Context Window</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Ollama allows you to run models locally on your computer. For instructions on how to get started, see their
<VSCodeLink
href="https://github.com/ollama/ollama/blob/main/README.md"
style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
<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>
)
}
export default OllamaOptions
@@ -0,0 +1,338 @@
import { VSCodeCheckbox, VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { openAiModelInfoSaneDefaults, azureOpenAiDefaultApiVersion } from "@shared/api"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const OpenAICompatOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, setApiConfiguration } = useExtensionState()
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.openAiBaseUrl || ""}
style={{ width: "100%", marginBottom: 10 }}
type="url"
onInput={handleInputChange("openAiBaseUrl")}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.openAiApiKey || ""}
style={{ width: "100%", marginBottom: 10 }}
type="password"
onInput={handleInputChange("openAiApiKey")}
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?.supportsComputerUse}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, supportsComputerUse: isChecked }
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
Supports Computer 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>
)
}
export default OpenAICompatOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const OpenAIOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.openAiNativeApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openAiNativeApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>OpenAI 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?.openAiNativeApiKey && (
<VSCodeLink
href="https://platform.openai.com/api-keys"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get an OpenAI API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default OpenAIOptions
@@ -0,0 +1,37 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import { getOpenRouterAuthUrl } from "@/utils/providers"
import VSCodeButtonLink from "../../../common/VSCodeButtonLink"
const OpenRouterOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration, uriScheme } = useExtensionState()
return (
<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>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} style={{ margin: "5px 0 0 0" }} appearance="secondary">
Get OpenRouter API Key
</VSCodeButtonLink>
)}
<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.
</p>
</div>
)
}
export default OpenRouterOptions
@@ -0,0 +1,66 @@
import { VSCodeTextField, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import DropdownContainer from "../DropdownContainer"
const QwenOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="china">China API</VSCodeOption>
<VSCodeOption value="international">International API</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Please select the appropriate API interface based on your location. If you are in China, choose the China API
interface. Otherwise, choose the International API interface.
</p>
<VSCodeTextField
value={apiConfiguration?.qwenApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("qwenApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Qwen 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?.qwenApiKey && (
<VSCodeLink
href="https://bailian.console.aliyun.com/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Qwen API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default QwenOptions
@@ -0,0 +1,23 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const RequestyOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.requestyApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("requestyApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
{!apiConfiguration?.requestyApiKey && <a href="https://app.requesty.ai/manage-api">Get API Key</a>}
</div>
)
}
export default RequestyOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const SambaNovaOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<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://sambanova.ai/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a SambaNova API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
export default SambaNovaOptions
@@ -0,0 +1,40 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const TogetherOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.togetherApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("togetherApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
style={{ width: "100%", marginTop: 10 }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<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>
)
}
export default TogetherOptions
@@ -0,0 +1,63 @@
import { VSCodeTextField, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import DropdownContainer from "../DropdownContainer"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../model/OpenRouterModelPicker"
const VertexOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<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>
<DropdownContainer zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX - 1} 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>
</DropdownContainer>
<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", fontSize: "inherit" }}>
{"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", fontSize: "inherit" }}>
{"2) install the Google Cloud CLI configure Application Default Credentials."}
</VSCodeLink>
</p>
</div>
)
}
export default VertexOptions
@@ -0,0 +1,109 @@
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
import { useCallback, useEffect, useState } from "react"
import { useEvent, useInterval } from "react-use"
import { vscode } from "@/utils/vscode"
import DropdownContainer from "../DropdownContainer"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../model/OpenRouterModelPicker"
import type { ExtensionMessage } from "@shared/ExtensionMessage"
import * as vscodemodels from "vscode"
declare module "vscode" {
interface LanguageModelChatSelector {
vendor?: string
family?: string
version?: string
id?: string
}
}
const VscodeLMOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
// Request VS Code LM models
const requestVsCodeLmModels = useCallback(() => {
vscode.postMessage({ type: "requestVsCodeLmModels" })
}, [])
// Request VS Code LM models when component mounts
useEffect(() => {
requestVsCodeLmModels()
}, [requestVsCodeLmModels])
// Poll VS Code LM models periodically
useInterval(requestVsCodeLmModels, 2000)
// Handle message events for VS Code LM models
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "vsCodeLmModels" && message.vsCodeLmModels) {
setVsCodeLmModels(message.vsCodeLmModels)
}
}, [])
useEvent("message", handleMessage)
return (
<div>
<DropdownContainer zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX - 2} className="dropdown-container">
<label htmlFor="vscode-lm-model">
<span style={{ fontWeight: 500 }}>Language Model</span>
</label>
{vsCodeLmModels.length > 0 ? (
<VSCodeDropdown
id="vscode-lm-model"
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement).value
if (!value) {
return
}
const [vendor, family] = value.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family },
},
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{vsCodeLmModels.map((model) => (
<VSCodeOption key={`${model.vendor}/${model.family}`} value={`${model.vendor}/${model.family}`}>
{model.vendor} - {model.family}
</VSCodeOption>
))}
</VSCodeDropdown>
) : (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
The VS Code Language Model API allows you to run models provided by other VS Code extensions (including
but not limited to GitHub Copilot). The easiest way to get started is to install the Copilot extension
from the VS Marketplace and enabling Claude 3.7 Sonnet.
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
fontWeight: 500,
}}>
Note: This is a very experimental integration and may not work as expected.
</p>
</DropdownContainer>
</div>
)
}
export default VscodeLMOptions
@@ -0,0 +1,49 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ProviderOptionsProps } from "./types/ProviderOptions"
const XAIOptions = ({ handleInputChange }: ProviderOptionsProps) => {
const { apiConfiguration } = useExtensionState()
return (
<div>
<VSCodeTextField
value={apiConfiguration?.xaiApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("xaiApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>X AI 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?.xaiApiKey && (
<VSCodeLink href="https://x.ai" style={{ display: "inline", fontSize: "inherit" }}>
You can get an X AI API key by signing up here.
</VSCodeLink>
)}
</p>
{/* Note: To fully implement this, you would need to add a handler in ClineProvider.ts */}
{/* {apiConfiguration?.xaiApiKey && (
<button
onClick={() => {
vscode.postMessage({
type: "requestXAIModels",
text: apiConfiguration?.xaiApiKey,
})
}}
style={{ margin: "5px 0 0 0" }}
className="vscode-button">
Fetch Available Models
</button>
)} */}
</div>
)
}
export default XAIOptions
@@ -0,0 +1,21 @@
export { default as AnthropicOptions } from "./AnthropicOptions"
export { default as AskSageOptions } from "./AskSageOptions"
export { default as BedrockOptions } from "./BedrockOptions"
export { default as ClineOptions } from "./ClineOptions"
export { default as DeepseekOptions } from "./DeepseekOptions"
export { default as DoubaoOptions } from "./DoubaoOptions"
export { default as GeminiOptions } from "./GeminiOptions"
export { default as LiteLLMOptions } from "./LiteLLMOptions"
export { default as LMStudioOptions } from "./LMStudioOptions"
export { default as MistralOptions } from "./MistralOptions"
export { default as OllamaOptions } from "./OllamaOptions"
export { default as OpenAICompatOptions } from "./OpenAICompatOptions"
export { default as OpenAIOptions } from "./OpenAIOptions"
export { default as OpenRouterOptions } from "./OpenRouterOptions"
export { default as QwenOptions } from "./QwenOptions"
export { default as RequestyOptions } from "./RequestyOptions"
export { default as SambaNovaOptions } from "./SambaNovaOptions"
export { default as TogetherOptions } from "./TogetherOptions"
export { default as VertexOptions } from "./VertexOptions"
export { default as VscodeLMOptions } from "./VscodeLMOptions"
export { default as XAIOptions } from "./XAIOptions"
@@ -0,0 +1,5 @@
import { ApiConfiguration } from "@shared/api"
export interface ProviderOptionsProps {
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
@@ -1,138 +0,0 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useRef, useState } from "react"
import { useRemark } from "react-remark"
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 12px;
color: var(--vscode-descriptionForeground);
p,
li,
ol,
ul {
line-height: 1.25;
margin: 0;
}
ol,
ul {
padding-left: 1.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
text-decoration: none;
}
a {
&:hover {
text-decoration: underline;
}
}
`
export const ModelDescriptionMarkdown = memo(
({
markdown,
key,
isExpanded,
setIsExpanded,
isPopup,
}: {
markdown?: string
key: string
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}) => {
const [reactContent, setMarkdown] = useRemark()
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMarkdown(markdown || "")
}, [markdown, setMarkdown])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
const { scrollHeight } = textRef.current
const { clientHeight } = textContainerRef.current
const isOverflowing = scrollHeight > clientHeight
setShowSeeMore(isOverflowing)
}
}, [reactContent, setIsExpanded])
return (
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
overflowY: isExpanded ? "auto" : "hidden",
position: "relative",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}>
{reactContent}
</div>
{!isExpanded && showSeeMore && (
<div
style={{
position: "absolute",
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
}}>
<div
style={{
width: 30,
height: "1.2em",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
style={{
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more
</VSCodeLink>
</div>
)}
</div>
</StyledMarkdown>
)
},
)
export default ModelDescriptionMarkdown
@@ -4,7 +4,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import SettingsButton from "@/components/common/SettingsButton"
import ApiOptions from "./ApiOptions"
import ApiOptions from "./ApiOptions/ApiOptions"
import { TabButton } from "../mcp/configuration/McpConfigurationView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "@shared/ExtensionMessage"
@@ -182,7 +182,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
/>
)}
<div className="mb-[5px]">
<div className="mb-[5px] mt-4">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
@@ -1,6 +1,6 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import ApiOptions from "../ApiOptions"
import ApiOptions from "../ApiOptions/ApiOptions"
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
@@ -3,7 +3,7 @@ import { useEffect, useState, memo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { validateApiConfiguration } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import ApiOptions from "@/components/settings/ApiOptions"
import ApiOptions from "@/components/settings/ApiOptions/ApiOptions"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
const WelcomeView = memo(() => {
+9
View File
@@ -1,5 +1,14 @@
import prettyBytes from "pretty-bytes"
export function formatPrice(price: number) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
export function formatLargeNumber(num: number): string {
if (num >= 1e9) {
return (num / 1e9).toFixed(1) + "b"
+167
View File
@@ -0,0 +1,167 @@
import {
ApiConfiguration,
ApiProvider,
ModelInfo,
anthropicModels,
anthropicDefaultModelId,
bedrockModels,
bedrockDefaultModelId,
vertexModels,
vertexDefaultModelId,
geminiModels,
geminiDefaultModelId,
openAiNativeModels,
openAiNativeDefaultModelId,
deepSeekModels,
deepSeekDefaultModelId,
mainlandQwenModels,
internationalQwenModels,
mainlandQwenDefaultModelId,
internationalQwenDefaultModelId,
doubaoModels,
doubaoDefaultModelId,
mistralModels,
mistralDefaultModelId,
askSageModels,
askSageDefaultModelId,
xaiModels,
xaiDefaultModelId,
sambanovaModels,
sambanovaDefaultModelId,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
openAiModelInfoSaneDefaults,
liteLlmModelInfoSaneDefaults,
} from "@shared/api"
import { formatPrice } from "./format"
export function getOpenRouterAuthUrl(uriScheme?: string) {
return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter`
}
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): {
selectedProvider: ApiProvider
selectedModelId: string
selectedModelInfo: ModelInfo
} {
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
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 "bedrock":
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId)
case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "qwen":
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
const qwenDefaultId =
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenDefaultModelId : internationalQwenDefaultModelId
return getProviderData(qwenModels, qwenDefaultId)
case "doubao":
return getProviderData(doubaoModels, doubaoDefaultModelId)
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "asksage":
return getProviderData(askSageModels, askSageDefaultModelId)
case "openrouter":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "requesty":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
}
case "cline":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "openai":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openAiModelId || "",
selectedModelInfo: apiConfiguration?.openAiModelInfo || openAiModelInfoSaneDefaults,
}
case "ollama":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.ollamaModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
case "lmstudio":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.lmStudioModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
case "vscode-lm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: liteLlmModelInfoSaneDefaults,
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
}
// Returns an array of formatted tier strings
export const formatTiers = (tiers: ModelInfo["inputPriceTiers"]): string[] => {
if (!tiers || tiers.length === 0) {
return []
}
return tiers.map((tier, index, arr) => {
const prevLimit = index > 0 ? arr[index - 1].tokenLimit : 0
const limitText =
tier.tokenLimit === Infinity
? `> ${prevLimit.toLocaleString()}` // Assumes sorted and Infinity is last
: `<= ${tier.tokenLimit.toLocaleString()}`
return `${formatPrice(tier.price)}/million tokens (${limitText} tokens)`
})
}