Compare commits

...

3 Commits

Author SHA1 Message Date
NightTrek 22898a970b Set LiteLLM default temperature to 1.0 2025-11-01 18:57:24 -07:00
NightTrek 82788e1b9a fix: always show LiteLLM model picker with manual entry fallback
- Remove conditional rendering from LiteLlmProvider - always show picker
- Add manual entry support: onBlur and Enter key with no selection
- Remove early return that hid picker when no models available
- Show helpful message in dropdown when model list is empty
- Convert Tailwind classes to styled-components for consistency
- Update help text to explain manual entry capability

Fixes issue where new users couldn't see picker until models were fetched
2025-11-01 15:39:39 -07:00
NightTrek 2588ec34d8 feat: Add LiteLLM model picker with dropdown support
- Add reusable OpenAI-compatible model fetch helper
- Implement LiteLLM model refresh endpoint (refreshLiteLlmModelsRpc)
- Add LiteLLM models to ExtensionStateContext
- Create LiteLlmModelPicker component with search/filter
- Update LiteLlmProvider to use dropdown when models available
- Add cache file support for LiteLLM models
- Support both x-litellm-api-key and Authorization headers

The LiteLLM provider now displays a searchable model dropdown
similar to OpenRouter, Groq, and Baseten providers, while
maintaining fallback to manual model ID entry when the server
is unavailable or returns an empty list.
2025-10-31 01:44:19 -07:00
9 changed files with 611 additions and 16 deletions
+2
View File
@@ -39,6 +39,8 @@ service ModelsService {
rpc refreshGroqModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Baseten models
rpc refreshBasetenModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns LiteLLM models
rpc refreshLiteLlmModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Fetches available models from SAP AI Core
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
// Fetches available models from OCA
@@ -0,0 +1,77 @@
import { ModelInfo } from "@shared/api"
import axios, { AxiosError } from "axios"
export interface FetchOpenAiCompatibleModelsOptions {
baseUrl: string
headers?: Record<string, string | undefined>
transform?: (model: any) => Partial<ModelInfo>
}
/**
* Normalizes a base URL to ensure it ends with /v1
*/
export function normalizeOpenAiCompatibleBaseUrl(baseUrl: string): string {
const trimmed = baseUrl.trim().replace(/\/+$/, "")
if (trimmed.endsWith("/v1")) {
return trimmed
}
return `${trimmed}/v1`
}
/**
* Fetches models from an OpenAI-compatible /v1/models endpoint and maps them to ModelInfo objects.
*/
export async function fetchOpenAiCompatibleModels({
baseUrl,
headers = {},
transform,
}: FetchOpenAiCompatibleModelsOptions): Promise<Record<string, ModelInfo>> {
const normalizedBaseUrl = normalizeOpenAiCompatibleBaseUrl(baseUrl || "http://localhost:4000")
const url = `${normalizedBaseUrl}/models`
try {
const response = await axios.get(url, {
headers: Object.fromEntries(Object.entries(headers).filter(([_, value]) => Boolean(value))),
timeout: 15000,
})
const rawModels = Array.isArray(response.data?.data) ? response.data.data : []
const models: Record<string, ModelInfo> = {}
for (const rawModel of rawModels) {
if (!rawModel || typeof rawModel.id !== "string") {
continue
}
const transformed = transform ? transform(rawModel) : {}
models[rawModel.id] = {
supportsPromptCache: false,
description:
transformed.description ??
rawModel.description ??
(rawModel.owned_by ? `Provided by ${rawModel.owned_by}` : undefined),
maxTokens: transformed.maxTokens,
contextWindow: transformed.contextWindow,
supportsImages: transformed.supportsImages,
inputPrice: transformed.inputPrice,
outputPrice: transformed.outputPrice,
cacheWritesPrice: transformed.cacheWritesPrice,
cacheReadsPrice: transformed.cacheReadsPrice,
thinkingConfig: transformed.thinkingConfig,
tiers: transformed.tiers,
supportsGlobalEndpoint: transformed.supportsGlobalEndpoint,
}
}
return models
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError
throw new Error(
`Failed to fetch models from ${url}: ${axiosError.response?.status} ${axiosError.response?.statusText || axiosError.message}`,
)
}
throw error
}
}
@@ -0,0 +1,173 @@
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
import { fileExistsAtPath } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import {
fetchOpenAiCompatibleModels,
normalizeOpenAiCompatibleBaseUrl,
} from "@/core/api/providers/shared/fetchOpenAiCompatibleModels"
import { Controller } from ".."
type LiteLlmModelResponseEntry = NonNullable<LiteLlmModelInfoResponse["data"]>[number]
interface LiteLlmModelInfoResponse {
data?: Array<{
model_name?: string
description?: string
litellm_params?: {
model?: string
max_tokens?: number
context_window?: number
supports_images?: boolean
[key: string]: unknown
}
model_info?: {
input_cost_per_token?: number | string
output_cost_per_token?: number | string
cache_creation_input_token_cost?: number | string
cache_read_input_token_cost?: number | string
supports_prompt_caching?: boolean
[key: string]: unknown
}
}>
}
/**
* Converts LiteLLM cost values (per-token) into price per million tokens.
*/
function convertCostToPerMillion(value?: number | string): number | undefined {
if (value === undefined || value === null) {
return undefined
}
const numericValue = typeof value === "number" ? value : parseFloat(value)
if (Number.isNaN(numericValue)) {
return undefined
}
return numericValue * 1_000_000
}
/**
* Refreshes the LiteLLM models and returns application types.
* @param controller The controller instance
* @returns Record of model ID to ModelInfo (application types)
*/
export async function refreshLiteLlmModels(controller: Controller): Promise<Record<string, ModelInfo>> {
const liteLlmModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.liteLlmModels)
const liteLlmApiKey = controller.stateManager.getSecretKey("liteLlmApiKey")
const rawBaseUrl = controller.stateManager.getGlobalSettingsKey("liteLlmBaseUrl")
const normalizedBaseUrl = getNormalizedLiteLlmBaseUrl(rawBaseUrl)
let models: Record<string, ModelInfo> = {}
try {
const headers: Record<string, string | undefined> = {
"x-litellm-api-key": liteLlmApiKey,
Authorization: liteLlmApiKey ? `Bearer ${liteLlmApiKey}` : undefined,
"Content-Type": "application/json",
"User-Agent": "Cline-VSCode-Extension",
}
const transformedModels = await fetchOpenAiCompatibleModels({
baseUrl: normalizedBaseUrl,
headers,
transform: (rawModel: LiteLlmModelResponseEntry): Partial<ModelInfo> => {
const modelInfo = rawModel.model_info ?? {}
const params = rawModel.litellm_params ?? {}
const partial: Partial<ModelInfo> = {}
if (typeof params.max_tokens === "number") {
partial.maxTokens = params.max_tokens
}
if (typeof params.context_window === "number") {
partial.contextWindow = params.context_window
}
if (typeof params.supports_images === "boolean") {
partial.supportsImages = params.supports_images
}
if (typeof modelInfo.supports_prompt_caching === "boolean") {
partial.supportsPromptCache = modelInfo.supports_prompt_caching
}
const inputCost = convertCostToPerMillion(modelInfo.input_cost_per_token)
if (inputCost !== undefined) {
partial.inputPrice = inputCost
}
const outputCost = convertCostToPerMillion(modelInfo.output_cost_per_token)
if (outputCost !== undefined) {
partial.outputPrice = outputCost
}
const cacheWriteCost = convertCostToPerMillion(modelInfo.cache_creation_input_token_cost)
if (cacheWriteCost !== undefined) {
partial.cacheWritesPrice = cacheWriteCost
}
const cacheReadCost = convertCostToPerMillion(modelInfo.cache_read_input_token_cost)
if (cacheReadCost !== undefined) {
partial.cacheReadsPrice = cacheReadCost
}
if (rawModel.description) {
partial.description = rawModel.description
}
return partial
},
})
if (Object.keys(transformedModels).length > 0) {
models = Object.fromEntries(
Object.entries(transformedModels).map(([modelId, modelInfo]) => [
modelId,
{
...liteLlmModelInfoSaneDefaults,
...modelInfo,
},
]),
)
await fs.writeFile(liteLlmModelsFilePath, JSON.stringify(models))
} else {
console.warn("LiteLLM model list was empty; retaining previous cache if available.")
const cachedModels = await readLiteLlmModels()
if (cachedModels) {
models = cachedModels
}
}
} catch (error) {
console.error("Error fetching LiteLLM models:", error)
const cachedModels = await readLiteLlmModels()
if (cachedModels) {
models = cachedModels
}
}
return models
}
/**
* Reads cached LiteLLM models from disk (application types)
*/
async function readLiteLlmModels(): Promise<Record<string, ModelInfo> | undefined> {
const liteLlmModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.liteLlmModels)
if (await fileExistsAtPath(liteLlmModelsFilePath)) {
try {
const fileContents = await fs.readFile(liteLlmModelsFilePath, "utf8")
return JSON.parse(fileContents) as Record<string, ModelInfo>
} catch (error) {
console.error("Error reading cached LiteLLM models:", error)
}
}
return undefined
}
/**
* Utility to resolve the effective LiteLLM base URL with default normalization.
*/
export function getNormalizedLiteLlmBaseUrl(baseUrl: string | undefined): string {
return normalizeOpenAiCompatibleBaseUrl(baseUrl || "http://localhost:4000")
}
@@ -0,0 +1,19 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { toProtobufModels } from "@/shared/proto-conversions/models/typeConversion"
import { Controller } from ".."
import { refreshLiteLlmModels } from "./refreshLiteLlmModels"
/**
* Handles protobuf conversion for gRPC service
* @param controller The controller instance
* @param _request Empty request object
* @returns Response containing LiteLLM models (protobuf types)
*/
export async function refreshLiteLlmModelsRpc(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const models = await refreshLiteLlmModels(controller)
return OpenRouterCompatibleModelInfo.create({ models: toProtobufModels(models) })
}
+1
View File
@@ -21,6 +21,7 @@ export const GlobalFileNames = {
vercelAiGatewayModels: "vercel_ai_gateway_models.json",
groqModels: "groq_models.json",
basetenModels: "baseten_models.json",
liteLlmModels: "litellm_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
workflows: ".clinerules/workflows",
+1 -1
View File
@@ -2441,7 +2441,7 @@ export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
temperature: 0,
temperature: 1,
}
// AskSage Models
@@ -0,0 +1,330 @@
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { EmptyRequest } from "@shared/proto/cline/common"
import { fromProtobufModels } from "@shared/proto-conversions/models/typeConversion"
import { Mode } from "@shared/storage/types"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import { useMount } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { getModeSpecificFields, normalizeApiConfiguration } from "./utils/providerUtils"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface LiteLlmModelPickerProps {
isPopup?: boolean
currentMode: Mode
}
const LiteLlmModelPicker: React.FC<LiteLlmModelPickerProps> = ({ isPopup, currentMode }) => {
const { apiConfiguration, liteLlmModels: dynamicLiteLlmModels, setLiteLlmModels } = useExtensionState()
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.liteLlmModelId || liteLlmDefaultModelId)
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
const modelInfo = dynamicLiteLlmModels?.[newModelId] || liteLlmModelInfoSaneDefaults
handleModeFieldsChange(
{
liteLlmModelId: { plan: "planModeLiteLlmModelId", act: "actModeLiteLlmModelId" },
liteLlmModelInfo: { plan: "planModeLiteLlmModelInfo", act: "actModeLiteLlmModelInfo" },
},
{
liteLlmModelId: newModelId,
liteLlmModelInfo: modelInfo,
},
currentMode,
)
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
useMount(() => {
ModelsServiceClient.refreshLiteLlmModelsRpc(EmptyRequest.create({}))
.then((response) => {
setLiteLlmModels(fromProtobufModels(response.models))
})
.catch((err) => {
console.error("Failed to refresh LiteLLM models:", err)
})
})
// Sync external changes when the modelId changes
useEffect(() => {
const currentModelId = modeFields.liteLlmModelId || liteLlmDefaultModelId
setSearchTerm(currentModelId)
}, [modeFields.liteLlmModelId])
// Debounce search term to reduce re-renders
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm)
}, 300)
return () => clearTimeout(timer)
}, [searchTerm])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
const allLiteLlmModels = dynamicLiteLlmModels || {}
const modelIds = useMemo(() => {
return Object.keys(allLiteLlmModels).sort((a, b) => a.localeCompare(b))
}, [allLiteLlmModels])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
id,
html: id,
}))
}, [modelIds])
const fuse = useMemo(() => {
return new Fuse(searchableItems, {
keys: ["html"],
threshold: 0.6,
shouldSort: true,
isCaseSensitive: false,
ignoreLocation: false,
includeMatches: true,
minMatchCharLength: 1,
})
}, [searchableItems])
const modelSearchResults = useMemo(() => {
const results: { id: string; html: string }[] = debouncedSearchTerm
? highlight(fuse.search(debouncedSearchTerm), "model-item-highlight")
: searchableItems
return results
}, [searchableItems, debouncedSearchTerm, fuse])
const parseHighlightedText = React.useCallback((htmlString: string) => {
const parts = htmlString.split(/(<span class="model-item-highlight">.*?<\/span>)/g)
return parts
.map((part) => {
if (part.startsWith('<span class="model-item-highlight">')) {
const text = part.replace(/<span class="model-item-highlight">(.*?)<\/span>/, "$1")
return (
<span className="model-item-highlight" key={`highlight-${text}`}>
{text}
</span>
)
}
return part || null
})
.filter((part) => part !== null && part !== "")
}, [])
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (!isDropdownVisible) {
return
}
switch (event.key) {
case "ArrowDown":
event.preventDefault()
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
break
case "ArrowUp":
event.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case "Enter":
event.preventDefault()
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
handleModelChange(modelSearchResults[selectedIndex].id)
} else {
// User typed a custom model ID
handleModelChange(searchTerm)
}
setIsDropdownVisible(false)
break
case "Escape":
setIsDropdownVisible(false)
setSelectedIndex(-1)
break
}
}
const hasInfo = useMemo(() => {
return selectedModelInfo && selectedModelInfo.description
}, [selectedModelInfo])
useEffect(() => {
setSelectedIndex(-1)
if (dropdownListRef.current) {
dropdownListRef.current.scrollTop = 0
}
}, [searchTerm])
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
itemRefs.current[selectedIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth",
})
}
}, [selectedIndex])
return (
<div style={{ width: "100%" }}>
<style>
{`
.model-item-highlight {
background-color: var(--vscode-editor-findMatchHighlightBackground);
color: inherit;
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column" }}>
<label htmlFor="litellm-model-search">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownWrapper ref={dropdownRef}>
<VSCodeTextField
id="litellm-model-search"
onBlur={() => {
if (searchTerm !== selectedModelId) {
handleModelChange(searchTerm)
}
}}
onFocus={() => setIsDropdownVisible(true)}
onInput={(e) => {
setSearchTerm((e.target as HTMLInputElement)?.value || "")
setIsDropdownVisible(true)
}}
onKeyDown={handleKeyDown}
placeholder="Search and select a model..."
style={{
width: "100%",
zIndex: LITELLM_MODEL_PICKER_Z_INDEX,
position: "relative",
}}
value={searchTerm}>
{searchTerm && (
<div
aria-label="Clear search"
className="input-icon-button codicon codicon-close"
onClick={() => {
setSearchTerm("")
setIsDropdownVisible(true)
}}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
{isDropdownVisible && (
<DropdownList ref={dropdownListRef}>
{modelSearchResults.length > 0 ? (
modelSearchResults.map((item, index) => (
<DropdownItem
isSelected={index === selectedIndex}
key={item.id}
onClick={() => {
handleModelChange(item.id)
setIsDropdownVisible(false)
}}
onMouseEnter={() => setSelectedIndex(index)}
ref={(el) => (itemRefs.current[index] = el)}>
{parseHighlightedText(item.html)}
</DropdownItem>
))
) : (
<DropdownItem isSelected={false} style={{ cursor: "default", opacity: 0.7 }}>
No models found. Type a model ID manually.
</DropdownItem>
)}
</DropdownList>
)}
</DropdownWrapper>
</div>
{hasInfo ? (
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
) : (
<p
style={{
fontSize: "12px",
marginTop: 0,
color: "var(--vscode-descriptionForeground)",
}}>
The extension automatically fetches the latest list of models from your configured LiteLLM server.{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
Learn more about LiteLLM.
</VSCodeLink>{" "}
If the model list fails to load, you can manually type a model ID (e.g.,{" "}
<strong>anthropic/claude-sonnet-4-20250514</strong>).
</p>
)}
</div>
)
}
export const LITELLM_MODEL_PICKER_Z_INDEX = 1_000
export default LiteLlmModelPicker
// Dropdown
const DropdownWrapper = styled.div`
position: relative;
width: 100%;
`
const DropdownList = styled.div`
position: absolute;
top: calc(100% - 3px);
left: 0;
width: calc(100% - 2px);
max-height: 200px;
overflow-y: auto;
background-color: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-list-activeSelectionBackground);
z-index: ${LITELLM_MODEL_PICKER_Z_INDEX - 1};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
`
const DropdownItem = styled.div<{ isSelected: boolean }>`
padding: 5px 10px;
cursor: pointer;
word-break: break-all;
white-space: normal;
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
}
`
@@ -8,6 +8,7 @@ import { ModelsServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import LiteLlmModelPicker from "../LiteLlmModelPicker"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/providerUtils"
@@ -30,7 +31,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Get mode-specific fields
const { liteLlmModelId, liteLlmModelInfo } = getModeSpecificFields(apiConfiguration, currentMode)
const { liteLlmModelInfo } = getModeSpecificFields(apiConfiguration, currentMode)
// Local state for collapsible model configuration section
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
@@ -69,20 +70,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
type="password">
<span style={{ fontWeight: 500 }}>API Key</span>
</DebouncedTextField>
<DebouncedTextField
initialValue={liteLlmModelId || ""}
onChange={async (value) => {
await ModelsServiceClient.updateApiConfiguration(
UpdateApiConfigurationRequestNew.create({
options:
currentMode === "plan" ? { planModeLiteLlmModelId: value } : { actModeLiteLlmModelId: value },
}),
)
}}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}
style={{ width: "100%" }}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</DebouncedTextField>
<LiteLlmModelPicker currentMode={currentMode} isPopup={isPopup} />
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
{selectedModelInfo.supportsPromptCache && (
@@ -42,6 +42,7 @@ export interface ExtensionStateContextType extends ExtensionState {
basetenModels: Record<string, ModelInfo>
huggingFaceModels: Record<string, ModelInfo>
vercelAiGatewayModels: Record<string, ModelInfo>
liteLlmModels: Record<string, ModelInfo>
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
totalTasksSize: number | null
@@ -71,6 +72,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setBasetenModels: (value: Record<string, ModelInfo>) => void
setHuggingFaceModels: (value: Record<string, ModelInfo>) => void
setVercelAiGatewayModels: (value: Record<string, ModelInfo>) => void
setLiteLlmModels: (value: Record<string, ModelInfo>) => void
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
@@ -256,6 +258,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [vercelAiGatewayModels, setVercelAiGatewayModels] = useState<Record<string, ModelInfo>>({
[vercelAiGatewayDefaultModelId]: vercelAiGatewayDefaultModelInfo,
})
const [liteLlmModels, setLiteLlmModels] = useState<Record<string, ModelInfo>>({})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
@@ -642,6 +645,7 @@ export const ExtensionStateContextProvider: React.FC<{
basetenModels: basetenModelsState,
huggingFaceModels,
vercelAiGatewayModels,
liteLlmModels,
mcpServers,
mcpMarketplaceCatalog,
totalTasksSize,
@@ -688,6 +692,7 @@ export const ExtensionStateContextProvider: React.FC<{
setBasetenModels: (models: Record<string, ModelInfo>) => setBasetenModels(models),
setHuggingFaceModels: (models: Record<string, ModelInfo>) => setHuggingFaceModels(models),
setVercelAiGatewayModels: (models: Record<string, ModelInfo>) => setVercelAiGatewayModels(models),
setLiteLlmModels: (models: Record<string, ModelInfo>) => setLiteLlmModels(models),
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
closeMcpView,