Compare commits

...

3 Commits

Author SHA1 Message Date
Toshii d113a3e787 Merge branch 'main' into to/nebius-2 2025-06-26 22:35:02 -07:00
0xtoshii 00c427f610 base 3 2025-06-26 22:18:19 -07:00
0xtoshii 4f1b263a28 base 2
Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 21:59:47 -07:00
4 changed files with 61 additions and 313 deletions
@@ -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 &gt; 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>
)
}