mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d113a3e787 | |||
| 00c427f610 | |||
| 4f1b263a28 |
@@ -1,7 +1,7 @@
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { ApiConfiguration, geminiModels, liteLlmModelInfoSaneDefaults, ModelInfo, nebiusModels } from "@shared/api"
|
||||
import { ApiConfiguration, geminiModels, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
|
||||
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
@@ -21,7 +21,6 @@ import * as vscodemodels from "vscode"
|
||||
import OllamaModelPicker from "./OllamaModelPicker"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
import { formatPrice } from "./utils/pricingUtils"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
|
||||
import { ClineProvider } from "./providers/ClineProvider"
|
||||
@@ -47,6 +46,7 @@ import { OllamaProvider } from "./providers/OllamaProvider"
|
||||
import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
|
||||
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
|
||||
import { BedrockProvider } from "./providers/BedrockProvider"
|
||||
import { NebiusProvider } from "./providers/NebiusProvider"
|
||||
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
|
||||
import { LMStudioProvider } from "./providers/LMStudioProvider"
|
||||
|
||||
@@ -94,7 +94,6 @@ const ApiOptions = ({
|
||||
const { apiConfiguration, setApiConfiguration, uriScheme } = extensionState
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
||||
const newValue = event.target.value
|
||||
@@ -624,50 +623,13 @@ const ApiOptions = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "nebius" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.nebiusApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("nebiusApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Nebius 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?.nebiusApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://studio.nebius.com/settings/api-keys"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a Nebius API key by signing up here.{" "}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{apiErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
margin: "-10px 0 4px 0",
|
||||
fontSize: 12,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiErrorMessage}
|
||||
</p>
|
||||
{apiConfiguration && selectedProvider === "nebius" && (
|
||||
<NebiusProvider
|
||||
apiConfiguration={apiConfiguration}
|
||||
handleInputChange={handleInputChange}
|
||||
showModelOptions={showModelOptions}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
)}
|
||||
|
||||
{apiConfiguration && selectedProvider === "xai" && (
|
||||
@@ -708,50 +670,6 @@ const ApiOptions = ({
|
||||
{apiErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedProvider !== "openrouter" &&
|
||||
selectedProvider !== "cline" &&
|
||||
selectedProvider !== "anthropic" &&
|
||||
selectedProvider !== "asksage" &&
|
||||
selectedProvider !== "claude-code" &&
|
||||
selectedProvider !== "openai" &&
|
||||
selectedProvider !== "ollama" &&
|
||||
selectedProvider !== "lmstudio" &&
|
||||
selectedProvider !== "vscode-lm" &&
|
||||
selectedProvider !== "litellm" &&
|
||||
selectedProvider !== "requesty" &&
|
||||
selectedProvider !== "bedrock" &&
|
||||
selectedProvider !== "mistral" &&
|
||||
selectedProvider !== "deepseek" &&
|
||||
selectedProvider !== "sambanova" &&
|
||||
selectedProvider !== "openai-native" &&
|
||||
selectedProvider !== "gemini" &&
|
||||
selectedProvider !== "doubao" &&
|
||||
selectedProvider !== "qwen" &&
|
||||
selectedProvider !== "vertex" &&
|
||||
selectedProvider !== "gemini-cli" &&
|
||||
selectedProvider !== "fireworks" &&
|
||||
selectedProvider !== "xai" &&
|
||||
selectedProvider !== "cerebras" &&
|
||||
selectedProvider !== "sapaicore" &&
|
||||
showModelOptions && (
|
||||
<>
|
||||
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
|
||||
<label htmlFor="model-id">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
|
||||
</DropdownContainer>
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{modelIdErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
@@ -766,208 +684,4 @@ const ApiOptions = ({
|
||||
)
|
||||
}
|
||||
|
||||
// Returns an array of formatted tier strings
|
||||
const formatTiers = (
|
||||
tiers: ModelInfo["tiers"],
|
||||
priceType: "inputPrice" | "outputPrice" | "cacheReadsPrice" | "cacheWritesPrice",
|
||||
): JSX.Element[] => {
|
||||
if (!tiers || tiers.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tiers
|
||||
.map((tier, index, arr) => {
|
||||
const prevLimit = index > 0 ? arr[index - 1].contextWindow : 0
|
||||
const price = tier[priceType]
|
||||
|
||||
if (price === undefined) return null
|
||||
|
||||
return (
|
||||
<span style={{ paddingLeft: "15px" }} key={index}>
|
||||
{formatPrice(price)}/million tokens (
|
||||
{tier.contextWindow === Number.POSITIVE_INFINITY ? (
|
||||
<span>
|
||||
{">"} {prevLimit.toLocaleString()}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{"<="} {tier.contextWindow.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{" tokens)"}
|
||||
{index < arr.length - 1 && <br />}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
.filter((element): element is JSX.Element => element !== null)
|
||||
}
|
||||
|
||||
export const ModelInfoView = ({
|
||||
selectedModelId,
|
||||
modelInfo,
|
||||
isDescriptionExpanded,
|
||||
setIsDescriptionExpanded,
|
||||
isPopup,
|
||||
}: {
|
||||
selectedModelId: string
|
||||
modelInfo: ModelInfo
|
||||
isDescriptionExpanded: boolean
|
||||
setIsDescriptionExpanded: (isExpanded: boolean) => void
|
||||
isPopup?: boolean
|
||||
}) => {
|
||||
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
|
||||
const hasThinkingConfig = !!modelInfo.thinkingConfig
|
||||
const hasTiers = !!modelInfo.tiers && modelInfo.tiers.length > 0
|
||||
|
||||
// Create elements for input pricing
|
||||
const inputPriceElement = hasTiers ? (
|
||||
<Fragment key="inputPriceTiers">
|
||||
<span style={{ fontWeight: 500 }}>Input price:</span>
|
||||
<br />
|
||||
{formatTiers(modelInfo.tiers, "inputPrice")}
|
||||
</Fragment>
|
||||
) : modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 ? (
|
||||
<span key="inputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
|
||||
</span>
|
||||
) : null
|
||||
|
||||
// --- Output Price Logic ---
|
||||
let outputPriceElement = null
|
||||
if (hasThinkingConfig && modelInfo.outputPrice !== undefined && modelInfo.thinkingConfig?.outputPrice !== undefined) {
|
||||
// Display both standard and thinking budget prices
|
||||
outputPriceElement = (
|
||||
<Fragment key="outputPriceConditional">
|
||||
<span style={{ fontWeight: 500 }}>Output price (Standard):</span> {formatPrice(modelInfo.outputPrice)}/million
|
||||
tokens
|
||||
<br />
|
||||
<span style={{ fontWeight: 500 }}>Output price (Thinking Budget > 0):</span>{" "}
|
||||
{formatPrice(modelInfo.thinkingConfig.outputPrice)}/million tokens
|
||||
</Fragment>
|
||||
)
|
||||
} else if (hasTiers) {
|
||||
// Display tiered output pricing
|
||||
outputPriceElement = (
|
||||
<Fragment key="outputPriceTiers">
|
||||
<span style={{ fontWeight: 500 }}>Output price:</span>
|
||||
<span style={{ fontStyle: "italic" }}> (based on input tokens)</span>
|
||||
<br />
|
||||
{formatTiers(modelInfo.tiers, "outputPrice")}
|
||||
</Fragment>
|
||||
)
|
||||
} else if (modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0) {
|
||||
// Display single standard output price
|
||||
outputPriceElement = (
|
||||
<span key="outputPrice">
|
||||
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
|
||||
</span>
|
||||
)
|
||||
}
|
||||
// --- End Output Price Logic ---
|
||||
|
||||
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="supportsBrowserUse"
|
||||
isSupported={modelInfo.supportsImages ?? false} // cline browser tool uses image recognition for navigation (requires model image support).
|
||||
supportsLabel="Supports browser use"
|
||||
doesNotSupportLabel="Does not support browser 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>
|
||||
)
|
||||
}
|
||||
|
||||
const ModelInfoSupportsItem = ({
|
||||
isSupported,
|
||||
supportsLabel,
|
||||
doesNotSupportLabel,
|
||||
}: {
|
||||
isSupported: boolean
|
||||
supportsLabel: string
|
||||
doesNotSupportLabel: string
|
||||
}) => (
|
||||
<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(ApiOptions)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useRemark } from "react-remark"
|
||||
import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./ApiOptions"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
import FeaturedModelCard from "./FeaturedModelCard"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
@@ -66,7 +66,6 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
@@ -311,13 +310,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
||||
)}
|
||||
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
) : (
|
||||
<p
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView } from "./ApiOptions"
|
||||
import { ModelInfoView } from "./common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "./utils/providerUtils"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
|
||||
@@ -25,7 +25,6 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
const dropdownListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
@@ -230,13 +229,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
|
||||
{showBudgetSlider && (
|
||||
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
||||
)}
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
isPopup={isPopup}
|
||||
/>
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
) : (
|
||||
<p
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ApiConfiguration, nebiusModels } 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 NebiusProvider component
|
||||
*/
|
||||
interface NebiusProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Nebius AI Studio provider configuration component
|
||||
*/
|
||||
export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: NebiusProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.nebiusApiKey || ""}
|
||||
onChange={handleInputChange("nebiusApiKey")}
|
||||
providerName="Nebius"
|
||||
signupUrl="https://studio.nebius.com/settings/api-keys"
|
||||
helpText="This key is stored locally and only used to make API requests from this extension. (Note: Cline uses complex prompts and works best with Claude models. Less capable models may not work as expected.)"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={nebiusModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user