Compare commits

...

11 Commits

Author SHA1 Message Date
0xtoshii d3233f82ba nit 2025-06-18 11:41:07 -07:00
0xtoshii 77897ec651 Merge branch 'main' into to/refactor-providers 2025-06-18 11:38:42 -07:00
0xtoshii 3191b894a8 Merge branch 'main' into to/refactor-providers 2025-06-15 01:29:26 -07:00
0xtoshii 06fbc1c019 readme + helpers
Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-14 02:01:20 -07:00
0xtoshii 2dbfcbd02b ModelInfoView replication
Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-14 01:33:58 -07:00
0xtoshii 2e945bd73e nits 2025-06-13 23:56:15 -07:00
0xtoshii 60ca42c24c mistral base
Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-13 22:45:13 -07:00
0xtoshii ae95bfb468 import 2025-06-13 22:45:12 -07:00
0xtoshii 76f08b3d62 base 2 2025-06-13 22:45:12 -07:00
0xtoshii d10f5408be base or 2025-06-13 22:45:12 -07:00
0xtoshii 7d80295be4 base
Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-13 22:44:34 -07:00
16 changed files with 1062 additions and 286 deletions
@@ -4,7 +4,8 @@ import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
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"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, StateServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import {
+1 -1
View File
@@ -20,7 +20,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { TaskServiceClient, SlashServiceClient, FileServiceClient, UiServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import Announcement from "@/components/chat/Announcement"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
import ChatRow from "@/components/chat/ChatRow"
@@ -1,6 +1,6 @@
import HeroTooltip from "@/components/common/HeroTooltip"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
import { formatLargeNumber, formatSize } from "@/utils/format"
+23 -281
View File
@@ -1,54 +1,30 @@
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import {
anthropicDefaultModelId,
anthropicModels,
ApiConfiguration,
ApiProvider,
askSageDefaultModelId,
askSageDefaultURL,
askSageModels,
azureOpenAiDefaultApiVersion,
bedrockDefaultModelId,
bedrockModels,
cerebrasDefaultModelId,
cerebrasModels,
claudeCodeDefaultModelId,
claudeCodeModels,
deepSeekDefaultModelId,
deepSeekModels,
doubaoDefaultModelId,
doubaoModels,
geminiDefaultModelId,
geminiModels,
internationalQwenDefaultModelId,
internationalQwenModels,
liteLlmModelInfoSaneDefaults,
mainlandQwenDefaultModelId,
mainlandQwenModels,
mistralDefaultModelId,
mistralModels,
ModelInfo,
nebiusDefaultModelId,
nebiusModels,
openAiModelInfoSaneDefaults,
openAiNativeDefaultModelId,
openAiNativeModels,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
sambanovaDefaultModelId,
sambanovaModels,
vertexDefaultModelId,
vertexGlobalModels,
vertexModels,
xaiDefaultModelId,
xaiModels,
sapAiCoreDefaultModelId,
sapAiCoreModels,
} from "@shared/api"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
@@ -68,12 +44,16 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } fro
import { useInterval } from "react-use"
import styled from "styled-components"
import * as vscodemodels from "vscode"
import { useOpenRouterKeyInfo } from "../ui/hooks/useOpenRouterKeyInfo"
import { ClineAccountInfoCard } from "./ClineAccountInfoCard"
import OllamaModelPicker from "./OllamaModelPicker"
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
import RequestyModelPicker from "./RequestyModelPicker"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { formatPrice } from "./utils/pricingUtils"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import { OpenRouterProvider } from "./providers/OpenRouterProvider"
import { MistralProvider } from "./providers/MistralProvider"
import { ExtensionMessage } from "@shared/ExtensionMessage"
interface ApiOptionsProps {
@@ -84,44 +64,6 @@ interface ApiOptionsProps {
saveImmediately?: boolean // Add prop to control immediate saving
}
const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const { data: keyInfo, isLoading, error } = useOpenRouterKeyInfo(apiKey)
if (isLoading) {
return <span style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>Loading...</span>
}
if (error || !keyInfo || keyInfo.limit === null) {
// Don't show anything if there's an error, no info, or no limit set
return null
}
// Calculate remaining balance
const remainingBalance = keyInfo.limit - keyInfo.usage
const formattedBalance = remainingBalance.toLocaleString("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
})
return (
<VSCodeLink
href="https://openrouter.ai/settings/keys"
title={`Remaining balance: ${formattedBalance}\nLimit: ${keyInfo.limit.toLocaleString("en-US", { style: "currency", currency: "USD" })}\nUsage: ${keyInfo.usage.toLocaleString("en-US", { style: "currency", currency: "USD" })}`}
style={{
fontSize: "12px",
color: "var(--vscode-foreground)",
textDecoration: "none",
fontWeight: 500,
paddingLeft: 4,
cursor: "pointer",
}}>
Balance: {formattedBalance}
</VSCodeLink>
)
}
const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
anthropic: ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
vertex: [
@@ -646,75 +588,23 @@ const ApiOptions = ({
</div>
)}
{selectedProvider === "mistral" && (
<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>
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
)}
{selectedProvider === "openrouter" && (
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
{apiConfiguration?.openRouterApiKey && (
<OpenRouterBalanceDisplay apiKey={apiConfiguration.openRouterApiKey} />
)}
</div>
</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.{" "}
{/* {!apiConfiguration?.openRouterApiKey && (
<span style={{ color: "var(--vscode-charts-green)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> OpenRouter is recommended for high rate
limits, prompt caching, and wider selection of models.)
</span>
)} */}
</p>
</div>
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
uriScheme={uriScheme}
/>
)}
{selectedProvider === "bedrock" && (
@@ -2210,7 +2100,7 @@ const ApiOptions = ({
</>
)}
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
{selectedProvider === "cline" && showModelOptions && (
<>
<VSCodeCheckbox
style={{ marginTop: -10 }}
@@ -2270,6 +2160,7 @@ const ApiOptions = ({
selectedProvider !== "litellm" &&
selectedProvider !== "requesty" &&
selectedProvider !== "bedrock" &&
selectedProvider !== "mistral" &&
showModelOptions && (
<>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
@@ -2288,7 +2179,6 @@ const ApiOptions = ({
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels,
)}
{selectedProvider === "doubao" && createDropdown(doubaoModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
{selectedProvider === "asksage" && createDropdown(askSageModels)}
{selectedProvider === "xai" && createDropdown(xaiModels)}
{selectedProvider === "sambanova" && createDropdown(sambanovaModels)}
@@ -2366,9 +2256,7 @@ const ApiOptions = ({
</>
)}
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
<OpenRouterModelPicker isPopup={isPopup} />
)}
{selectedProvider === "cline" && showModelOptions && <OpenRouterModelPicker isPopup={isPopup} />}
{selectedProvider === "requesty" && showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
{modelIdErrorMessage && (
@@ -2385,19 +2273,6 @@ const ApiOptions = ({
)
}
export function getOpenRouterAuthUrl(uriScheme?: string) {
return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter`
}
export const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
// Returns an array of formatted tier strings
const formatTiers = (
tiers: ModelInfo["tiers"],
@@ -2602,137 +2477,4 @@ const ModelInfoSupportsItem = ({
</span>
)
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 "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId
return {
selectedProvider: provider,
selectedModelId: modelId || bedrockDefaultModelId,
selectedModelInfo: (baseModelId && bedrockModels[baseModelId]) || bedrockModels[bedrockDefaultModelId],
}
}
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":
const openRouterModelId = apiConfiguration?.openRouterModelId || openRouterDefaultModelId
const openRouterModelInfo = apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo
return {
selectedProvider: provider,
selectedModelId: openRouterModelId,
// TODO: remove this once we have a better way to handle free models on Cline
// Free grok 3 promotion
selectedModelInfo:
openRouterModelId === "x-ai/grok-3"
? { ...openRouterModelInfo, inputPrice: 0, outputPrice: 0 }
: openRouterModelInfo,
}
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: apiConfiguration?.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "nebius":
return getProviderData(nebiusModels, nebiusDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
case "cerebras":
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
case "sapaicore":
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
}
export default memo(ApiOptions)
@@ -10,7 +10,8 @@ import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
import { ModelInfoView } from "./ApiOptions"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -0,0 +1,160 @@
# API Options Component Architecture
This directory contains the refactored API Options components for the Cline extension. The refactoring aims to improve maintainability, code organization, and reduce complexity by separating provider-specific code into modular components.
## Directory Structure
```
settings/
├── ApiOptions.tsx # Main component that renders provider-specific components
├── common/ # Reusable UI components
│ ├── ApiKeyField.tsx # API key input with standard styling
│ ├── BaseUrlField.tsx # Base URL input with standard styling
│ ├── ErrorMessage.tsx # Standard error message display
│ ├── ModelInfoView.tsx # Model information display
│ └── ModelSelector.tsx # Model selection dropdown
├── providers/ # Provider-specific components
│ ├── ClineProvider.tsx # Cline configuration
│ ├── AnthropicProvider.tsx # Anthropic-specific configuration
│ ├── BedrockProvider.tsx # AWS Bedrock configuration
│ ├── GeminiProvider.tsx # Google Gemini configuration
│ ├── MistralProvider.tsx # Mistral configuration
│ ├── OllamaProvider.tsx # Ollama configuration
│ ├── OpenAICompatibleProvider.tsx # OpenAI compatible API configuration
│ ├── OpenRouterProvider.tsx # OpenRouter configuration
│ └── ...
└── utils/ # Utility functions
├── pricingUtils.ts # Pricing formatting utilities
└── providerUtils.ts # API configuration normalization
```
## Architecture
### Component Hierarchy
```
ApiOptions
└── [ProviderComponent] (based on selected provider)
├── ApiKeyField (if needed)
├── BaseUrlField (if needed)
├── ModelSelector (if showing model options)
└── ModelInfoView (if showing model options)
```
### Data Flow
1. `ApiOptions` receives the current API configuration from the extension state
2. When a provider is selected, it renders the corresponding provider component
3. Provider-specific components receive `apiConfiguration` and `handleInputChange` to manage their state
4. Changes are propagated back to the extension via the `handleInputChange` callback
## Adding a New Provider
To add a new provider:
1. Create a new file in the `providers` directory, e.g. `MyNewProvider.tsx`
2. Implement the provider component using this template:
```tsx
import { ApiConfiguration, myNewProviderModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the MyNewProvider component
*/
interface MyNewProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The MyNewProvider configuration component
*/
export const MyNewProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: MyNewProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
{/* Add provider-specific fields */}
<ApiKeyField
value={apiConfiguration?.myNewProviderApiKey || ""}
onChange={handleInputChange("myNewProviderApiKey")}
providerName="My New Provider"
signupUrl="https://mynewprovider.com/signup"
/>
{/* Optional: Base URL field if the provider supports custom endpoints */}
<BaseUrlField
value={apiConfiguration?.myNewProviderBaseUrl}
onChange={handleInputChange("myNewProviderBaseUrl")}
defaultPlaceholder="https://api.mynewprovider.com"
/>
{showModelOptions && (
<>
<ModelSelector
models={myNewProviderModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isPopup={isPopup}
/>
</>
)}
</div>
)
}
```
3. Import and add the new provider component to `ApiOptions.tsx`:
```tsx
import { MyNewProvider } from "./providers/MyNewProvider"
// ...
{apiConfiguration && selectedProvider === "mynewprovider" && (
<MyNewProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
)}
```
4. Add the provider to the dropdown options:
```tsx
<VSCodeOption value="mynewprovider">My New Provider</VSCodeOption>
```
## Best Practices
1. **Reuse Common Components**: Use the common components for consistent UI and behavior
2. **Provider-Specific Logic**: Keep provider-specific logic within the provider component
3. **Type Safety**: Ensure all props and state are properly typed
4. **Error Handling**: Handle edge cases gracefully, such as missing configurations
5. **Documentation**: Document any provider-specific behaviors or requirements
## Testing
Each provider component should be tested in isolation to ensure it renders correctly and handles user input properly.
@@ -10,7 +10,8 @@ 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, normalizeApiConfiguration } from "./ApiOptions"
import { ModelInfoView } from "./ApiOptions"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
export interface RequestyModelPickerProps {
@@ -0,0 +1,49 @@
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
/**
* Props for the ApiKeyField component
*/
interface ApiKeyFieldProps {
value: string
onChange: (e: any) => void
providerName: string
signupUrl?: string
placeholder?: string
helpText?: string
}
/**
* A reusable component for API key input fields with standard styling and help text for signing up for key
*/
export const ApiKeyField = ({
value,
onChange,
providerName,
signupUrl,
placeholder = "Enter API Key...",
helpText,
}: ApiKeyFieldProps) => (
<div>
<VSCodeTextField value={value} style={{ width: "100%" }} type="password" onInput={onChange} placeholder={placeholder}>
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{helpText || "This key is stored locally and only used to make API requests from this extension."}
{!value && signupUrl && (
<VSCodeLink
href={signupUrl}
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a{/^[aeiou]/i.test(providerName) ? "n" : ""} {providerName} API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
@@ -0,0 +1,57 @@
import { useState, useEffect } from "react"
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
/**
* Props for the BaseUrlField component
*/
interface BaseUrlFieldProps {
value: string | undefined
onChange: (value: string) => void
defaultValue?: string
label?: string
placeholder?: string
}
/**
* A reusable component for toggling and entering custom base URLs
*/
export const BaseUrlField = ({
value,
onChange,
defaultValue = "",
label = "Use custom base URL",
placeholder = "Default: https://api.example.com",
}: BaseUrlFieldProps) => {
const [isEnabled, setIsEnabled] = useState(!!value)
// When value changes externally, update isEnabled state
useEffect(() => {
setIsEnabled(!!value)
}, [value])
const handleToggle = (e: any) => {
const checked = e.target.checked === true
setIsEnabled(checked)
if (!checked) {
onChange("")
}
}
return (
<div>
<VSCodeCheckbox checked={isEnabled} onChange={handleToggle}>
{label}
</VSCodeCheckbox>
{isEnabled && (
<VSCodeTextField
value={value || ""}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={(e: any) => onChange(e.target.value)}
placeholder={placeholder}
/>
)}
</div>
)
}
@@ -0,0 +1,26 @@
import React from "react"
/**
* Props for the ErrorMessage component
*/
interface ErrorMessageProps {
message: string
style?: React.CSSProperties
}
/**
* A reusable component for displaying error messages
*/
export const ErrorMessage = ({ message, style }: ErrorMessageProps) => {
return (
<p
style={{
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
...style,
}}>
{message}
</p>
)
}
@@ -0,0 +1,224 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Fragment, useState } from "react"
import { ModelInfo, geminiModels } from "@shared/api"
import { ModelDescriptionMarkdown } from "../OpenRouterModelPicker"
import {
formatPrice,
hasThinkingBudget,
supportsImages,
supportsBrowserUse,
supportsPromptCache,
formatTokenPrice,
formatTokenLimit,
} from "../utils/pricingUtils"
/**
* 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)
}
/**
* Props for the ModelInfoSupportsItem component
*/
interface ModelInfoSupportsItemProps {
isSupported: boolean
supportsLabel: string
doesNotSupportLabel: string
}
/**
* A component to show a feature support indicator with an icon
*/
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>
)
/**
* Props for the ModelInfoView component
*/
interface ModelInfoViewProps {
selectedModelId: string
modelInfo: ModelInfo
isPopup?: boolean
}
/**
* A reusable component for displaying model information
* This component manages its own description expansion state
*/
export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfoViewProps) => {
// Internal state management for description expansion
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
const hasThinkingConfig = hasThinkingBudget(modelInfo)
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> {formatTokenPrice(modelInfo.inputPrice)}
</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> {formatTokenPrice(modelInfo.outputPrice)}
<br />
<span style={{ fontWeight: 500 }}>Output price (Thinking Budget &gt; 0):</span>{" "}
{formatTokenPrice(modelInfo.thinkingConfig.outputPrice)}
</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> {formatTokenPrice(modelInfo.outputPrice)}
</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={supportsImages(modelInfo)}
supportsLabel="Supports images"
doesNotSupportLabel="Does not support images"
/>,
<ModelInfoSupportsItem
key="supportsBrowserUse"
isSupported={supportsBrowserUse(modelInfo)}
supportsLabel="Supports browser use"
doesNotSupportLabel="Does not support browser use"
/>,
!isGemini && (
<ModelInfoSupportsItem
key="supportsPromptCache"
isSupported={supportsPromptCache(modelInfo)}
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> {formatTokenLimit(modelInfo.maxTokens)} tokens
</span>
),
inputPriceElement, // Add the generated input price block
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
<span key="cacheWritesPrice">
<span style={{ fontWeight: 500 }}>Cache writes price:</span> {formatTokenPrice(modelInfo.cacheWritesPrice || 0)}
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (
<span key="cacheReadsPrice">
<span style={{ fontWeight: 500 }}>Cache reads price:</span> {formatTokenPrice(modelInfo.cacheReadsPrice || 0)}
</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>
)
}
@@ -0,0 +1,61 @@
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { ModelInfo } from "@shared/api"
import styled from "styled-components"
/**
* Container for dropdowns that ensures proper z-index handling
* This is necessary to ensure dropdown opens downward
*/
export const DropdownContainer = styled.div.attrs<{ zIndex?: number }>(({ zIndex }) => ({
style: {
zIndex: zIndex || 1000,
},
}))`
position: relative;
// Force dropdowns to open downward
& vscode-dropdown::part(listbox) {
position: absolute !important;
top: 100% !important;
bottom: auto !important;
}
`
/**
* Props for the ModelSelector component
*/
interface ModelSelectorProps {
models: Record<string, ModelInfo>
selectedModelId: string | undefined
onChange: (e: any) => void
zIndex?: number
label?: string
}
/**
* A reusable component for selecting models from a dropdown
*/
export const ModelSelector = ({ models, selectedModelId, onChange, zIndex, label = "Model" }: ModelSelectorProps) => {
return (
<DropdownContainer className="dropdown-container" zIndex={zIndex}>
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>{label}</span>
</label>
<VSCodeDropdown id="model-id" value={selectedModelId} onChange={onChange} 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>
</DropdownContainer>
)
}
@@ -0,0 +1,47 @@
import { ApiConfiguration, mistralModels } 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 MistralProvider component
*/
interface MistralProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Mistral provider configuration component
*/
export const MistralProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: MistralProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.mistralApiKey || ""}
onChange={handleInputChange("mistralApiKey")}
providerName="Mistral"
signupUrl="https://console.mistral.ai/codestral"
/>
{showModelOptions && (
<>
<ModelSelector
models={mistralModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,157 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { DropdownContainer } from "../common/ModelSelector"
import { useState } from "react"
import { getOpenRouterAuthUrl } from "../utils/providerUtils"
import { useOpenRouterKeyInfo } from "../../ui/hooks/useOpenRouterKeyInfo"
import VSCodeButtonLink from "../../common/VSCodeButtonLink"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { formatPrice } from "../utils/pricingUtils"
/**
* Component to display OpenRouter balance information
*/
const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const { data: keyInfo, isLoading, error } = useOpenRouterKeyInfo(apiKey)
if (isLoading) {
return <span style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>Loading...</span>
}
if (error || !keyInfo || keyInfo.limit === null) {
// Don't show anything if there's an error, no info, or no limit set
return null
}
// Calculate remaining balance
const remainingBalance = keyInfo.limit - keyInfo.usage
const formattedBalance = formatPrice(remainingBalance)
return (
<VSCodeLink
href="https://openrouter.ai/settings/keys"
title={`Remaining balance: ${formattedBalance}\nLimit: ${formatPrice(keyInfo.limit)}\nUsage: ${formatPrice(keyInfo.usage)}`}
style={{
fontSize: "12px",
color: "var(--vscode-foreground)",
textDecoration: "none",
fontWeight: 500,
paddingLeft: 4,
cursor: "pointer",
}}>
Balance: {formattedBalance}
</VSCodeLink>
)
}
/**
* Props for the OpenRouterProvider component
*/
interface OpenRouterProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
uriScheme?: string
}
/**
* The OpenRouter provider configuration component
*/
export const OpenRouterProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
uriScheme,
}: OpenRouterProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
{apiConfiguration?.openRouterApiKey && (
<OpenRouterBalanceDisplay apiKey={apiConfiguration.openRouterApiKey} />
)}
</div>
</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>
{showModelOptions && (
<>
<VSCodeCheckbox
style={{ marginTop: -10 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("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) => {
handleFieldChange("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>
)}
<OpenRouterModelPicker isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,56 @@
import { ModelInfo } from "@shared/api"
/**
* Formats a price as a currency string
*/
export const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
/**
* Helper function to format token prices for display
* @param price The price per million tokens
*/
export const formatTokenPrice = (price: number) => {
return `${formatPrice(price)}/million tokens`
}
/**
* Helper function to determine if a model supports thinking budget
*/
export const hasThinkingBudget = (modelInfo: ModelInfo): boolean => {
return !!modelInfo.thinkingConfig
}
/**
* Helper function to check if a model supports images
*/
export const supportsImages = (modelInfo: ModelInfo): boolean => {
return !!modelInfo.supportsImages
}
/**
* Helper function to check if a model supports browser use
*/
export const supportsBrowserUse = (modelInfo: ModelInfo): boolean => {
return !!modelInfo.supportsImages // browser tool uses image recognition
}
/**
* Helper function to check if a model supports prompt caching
*/
export const supportsPromptCache = (modelInfo: ModelInfo): boolean => {
return !!modelInfo.supportsPromptCache
}
/**
* Helper function to format token limits for display
*/
export const formatTokenLimit = (limit: number): string => {
return limit.toLocaleString()
}
@@ -0,0 +1,194 @@
import {
ApiConfiguration,
ApiProvider,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
geminiDefaultModelId,
geminiModels,
mistralDefaultModelId,
mistralModels,
openAiModelInfoSaneDefaults,
openAiNativeDefaultModelId,
openAiNativeModels,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
mainlandQwenModels,
internationalQwenModels,
mainlandQwenDefaultModelId,
internationalQwenDefaultModelId,
vertexDefaultModelId,
vertexModels,
askSageModels,
askSageDefaultModelId,
xaiDefaultModelId,
xaiModels,
sambanovaModels,
sambanovaDefaultModelId,
doubaoModels,
doubaoDefaultModelId,
liteLlmModelInfoSaneDefaults,
nebiusModels,
nebiusDefaultModelId,
cerebrasModels,
cerebrasDefaultModelId,
sapAiCoreModels,
sapAiCoreDefaultModelId,
claudeCodeDefaultModelId,
claudeCodeModels,
} from "@shared/api"
/**
* Interface for normalized API configuration
*/
export interface NormalizedApiConfig {
selectedProvider: ApiProvider
selectedModelId: string
selectedModelInfo: ModelInfo
}
/**
* Normalizes API configuration to ensure consistent values
*/
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): NormalizedApiConfig {
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 "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId
return {
selectedProvider: provider,
selectedModelId: modelId || bedrockDefaultModelId,
selectedModelInfo: (baseModelId && bedrockModels[baseModelId]) || bedrockModels[bedrockDefaultModelId],
}
}
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":
const openRouterModelId = apiConfiguration?.openRouterModelId || openRouterDefaultModelId
const openRouterModelInfo = apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo
return {
selectedProvider: provider,
selectedModelId: openRouterModelId,
// TODO: remove this once we have a better way to handle free models on Cline
// Free grok 3 promotion
selectedModelInfo:
openRouterModelId === "x-ai/grok-3"
? { ...openRouterModelInfo, inputPrice: 0, outputPrice: 0 }
: openRouterModelInfo,
}
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: apiConfiguration?.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "nebius":
return getProviderData(nebiusModels, nebiusDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
case "cerebras":
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
case "sapaicore":
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
}
/**
* Gets the OpenRouter authentication URL
*/
export function getOpenRouterAuthUrl(uriScheme?: string) {
return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter`
}