Compare commits

...

1 Commits

Author SHA1 Message Date
arafatkatze a4d3bac66a Adding huggingface provider 2025-07-17 17:35:17 -07:00
16 changed files with 653 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
adding huggingface provider
+6
View File
@@ -15,6 +15,8 @@ service ModelsService {
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hugging Face models
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
@@ -126,6 +128,7 @@ enum ApiProvider {
SAPAICORE = 25;
CLAUDE_CODE = 26;
MOONSHOT = 27;
HUGGINGFACE = 28;
}
// Model info for OpenAI-compatible models
@@ -246,4 +249,7 @@ message ModelsApiConfiguration {
optional string groq_api_key = 78;
optional string groq_model_id = 79;
optional OpenRouterModelInfo groq_model_info = 80;
optional string hugging_face_api_key = 81;
optional string hugging_face_model_id = 82;
optional OpenRouterModelInfo hugging_face_model_info = 83;
}
+6
View File
@@ -29,6 +29,7 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
import { GroqHandler } from "./providers/groq"
import { HuggingFaceHandler } from "./providers/huggingface"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -195,6 +196,11 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
moonshotApiLine: options.moonshotApiLine,
apiModelId: options.apiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
huggingFaceApiKey: options.huggingFaceApiKey,
apiModelId: options.apiModelId,
})
case "nebius":
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
+141
View File
@@ -0,0 +1,141 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface HuggingFaceHandlerOptions {
huggingFaceApiKey?: string
apiModelId?: string
}
export class HuggingFaceHandler implements ApiHandler {
private options: HuggingFaceHandlerOptions
private client: OpenAI | undefined
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
constructor(options: HuggingFaceHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.huggingFaceApiKey) {
throw new Error("Hugging Face API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.huggingface.co/v1",
apiKey: this.options.huggingFaceApiKey,
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
if (!usage) {
return
}
const inputTokens = usage.prompt_tokens || 0
const outputTokens = usage.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
const usageData = {
type: "usage" as const,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: totalCost,
}
yield usageData
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const stream = (await client.chat.completions.create(requestParams)) as any
let chunkCount = 0
let totalContent = ""
for await (const chunk of stream) {
chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
totalContent += delta.content
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
} catch (error: any) {
throw error
}
}
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
// Return cached model if available
if (this.cachedModel) {
return this.cachedModel
}
const modelId = this.options.apiModelId
// List all available models for debugging
const availableModels = Object.keys(huggingFaceModels)
let result: { id: HuggingFaceModelId; info: ModelInfo }
if (modelId && modelId in huggingFaceModels) {
const id = modelId as HuggingFaceModelId
const modelInfo = huggingFaceModels[id]
result = { id, info: modelInfo }
} else {
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
result = {
id: huggingFaceDefaultModelId,
info: defaultInfo,
}
}
// Cache the result for future calls
this.cachedModel = result
return result
}
}
@@ -0,0 +1,112 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
import { huggingFaceModels } from "@shared/api"
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
try {
await fs.mkdir(cacheDir, { recursive: true })
} catch (error) {
// Directory might already exist
}
return cacheDir
}
/**
* Refreshes the Hugging Face models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Hugging Face models
*/
export async function refreshHuggingFaceModels(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
let models: Record<string, OpenRouterModelInfo> = {}
try {
// Fetch models from Hugging Face API
const response = await axios.get("https://router.huggingface.co/v1/models", {
timeout: 10000,
})
if (response.data?.data) {
const rawModels = response.data.data
// Transform HF models to OpenRouter-compatible format
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: 8192, // HF doesn't provide max_tokens, use default
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
supportsImages: false, // Most models don't support images
supportsPromptCache: false,
inputPrice: 0, // Will be set based on providers
outputPrice: 0, // Will be set based on providers
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
})
// Add model-specific configurations if we have them in our static models
if (rawModel.id in huggingFaceModels) {
const staticModel = huggingFaceModels[rawModel.id as keyof typeof huggingFaceModels]
modelInfo.maxTokens = staticModel.maxTokens
modelInfo.contextWindow = staticModel.contextWindow
modelInfo.supportsImages = staticModel.supportsImages
modelInfo.supportsPromptCache = staticModel.supportsPromptCache
modelInfo.inputPrice = staticModel.inputPrice
modelInfo.outputPrice = staticModel.outputPrice
modelInfo.description = staticModel.description || modelInfo.description
}
models[rawModel.id] = modelInfo
}
// Save to cache
await fs.writeFile(huggingFaceModelsFilePath, JSON.stringify(models, null, 2))
}
} catch (error) {
console.error("Error fetching Hugging Face models:", error)
// Try to load from cache
try {
if (await fileExistsAtPath(huggingFaceModelsFilePath)) {
const cachedModels = await fs.readFile(huggingFaceModelsFilePath, "utf-8")
const parsedModels = JSON.parse(cachedModels)
models = parsedModels
}
} catch (cacheError) {
console.error("Error loading cached Hugging Face models:", cacheError)
}
// If no cache available, use static models as fallback
if (Object.keys(models).length === 0) {
for (const [modelId, modelInfo] of Object.entries(huggingFaceModels)) {
models[modelId] = OpenRouterModelInfo.create({
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: modelInfo.description || "",
})
}
}
}
return OpenRouterCompatibleModelInfo.create({ models })
}
+22
View File
@@ -0,0 +1,22 @@
import * as vscode from "vscode"
import type { Controller } from "../index"
import type { EmptyRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
/**
* Opens the Cline walkthrough in VSCode
* @param controller The controller instance
* @param request Empty request
* @returns Empty response
*/
export async function openWalkthrough(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
telemetryService.captureButtonClick("webview_openWalkthrough")
return Empty.create({})
} catch (error) {
console.error(`Failed to open walkthrough: ${error}`)
throw error
}
}
+3
View File
@@ -21,6 +21,7 @@ export type SecretKey =
| "asksageApiKey"
| "xaiApiKey"
| "moonshotApiKey"
| "huggingFaceApiKey"
| "nebiusApiKey"
| "sambanovaApiKey"
| "cerebrasApiKey"
@@ -107,6 +108,8 @@ export type GlobalStateKey =
| "requestyModelInfo"
| "togetherModelId"
| "fireworksModelId"
| "huggingFaceModelId"
| "huggingFaceModelInfo"
| "sapAiCoreModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
+16
View File
@@ -170,6 +170,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
groqApiKey,
moonshotApiKey,
nebiusApiKey,
huggingFaceApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
@@ -250,6 +251,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "groqApiKey") as Promise<string | undefined>,
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
@@ -308,6 +310,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
sapAiCoreModelId,
huggingFaceModelId,
huggingFaceModelInfo,
] = await Promise.all([
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
@@ -340,6 +344,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "huggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "huggingFaceModelInfo") as Promise<ModelInfo | undefined>,
])
const processingStart = performance.now()
@@ -462,6 +468,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
huggingFaceApiKey,
huggingFaceModelId,
huggingFaceModelInfo,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
@@ -581,6 +590,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
huggingFaceApiKey,
huggingFaceModelId,
huggingFaceModelInfo,
} = apiConfiguration
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
@@ -608,6 +620,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
groqModelId,
groqModelInfo,
sapAiCoreModelId,
huggingFaceModelId,
huggingFaceModelInfo,
// Global state updates (27 keys)
awsRegion,
@@ -672,6 +686,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
huggingFaceApiKey,
}
// Execute batched operations in parallel for maximum performance
@@ -715,6 +730,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"groqApiKey",
"moonshotApiKey",
"nebiusApiKey",
"huggingFaceApiKey",
]
for (const key of secretKeys) {
await storeSecret(context, key, undefined)
+47
View File
@@ -29,6 +29,7 @@ export type ApiProvider =
| "cerebras"
| "sapaicore"
| "groq"
| "huggingface"
export interface ApiHandlerOptions {
apiModelId?: string
@@ -92,6 +93,9 @@ export interface ApiHandlerOptions {
qwenApiLine?: string
moonshotApiLine?: string
moonshotApiKey?: string
huggingFaceApiKey?: string
huggingFaceModelId?: string
huggingFaceModelInfo?: ModelInfo
nebiusApiKey?: string
asksageApiUrl?: string
asksageApiKey?: string
@@ -1074,6 +1078,49 @@ export const deepSeekModels = {
},
} as const satisfies Record<string, ModelInfo>
// Hugging Face Inference Providers
// https://huggingface.co/docs/inference-providers/en/index
export type HuggingFaceModelId = keyof typeof huggingFaceModels
export const huggingFaceDefaultModelId: HuggingFaceModelId = "moonshotai/Kimi-K2-Instruct"
export const huggingFaceModels = {
"moonshotai/Kimi-K2-Instruct": {
maxTokens: 131_072,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
},
"deepseek-ai/DeepSeek-V3-0324": {
maxTokens: 8192,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
},
"deepseek-ai/DeepSeek-R1": {
maxTokens: 8192,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek's reasoning model with step-by-step thinking capabilities.",
},
"meta-llama/Llama-3.1-8B-Instruct": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Efficient 8B parameter Llama model for general-purpose tasks.",
},
} as const satisfies Record<string, ModelInfo>
// Qwen
// https://bailian.console.aliyun.com/
export type MainlandQwenModelId = keyof typeof mainlandQwenModels
@@ -224,6 +224,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.LITELLM
case "moonshot":
return ProtoApiProvider.MOONSHOT
case "huggingface":
return ProtoApiProvider.HUGGINGFACE
case "nebius":
return ProtoApiProvider.NEBIUS
case "fireworks":
@@ -288,6 +290,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "litellm"
case ProtoApiProvider.MOONSHOT:
return "moonshot"
case ProtoApiProvider.HUGGINGFACE:
return "huggingface"
case ProtoApiProvider.NEBIUS:
return "nebius"
case ProtoApiProvider.FIREWORKS:
@@ -374,6 +378,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
qwenApiLine: config.qwenApiLine,
moonshotApiLine: config.moonshotApiLine,
moonshotApiKey: config.moonshotApiKey,
huggingFaceApiKey: config.huggingFaceApiKey,
huggingFaceModelId: config.huggingFaceModelId,
huggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.huggingFaceModelInfo),
nebiusApiKey: config.nebiusApiKey,
asksageApiUrl: config.asksageApiUrl,
asksageApiKey: config.asksageApiKey,
@@ -460,6 +467,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
qwenApiLine: protoConfig.qwenApiLine,
moonshotApiLine: protoConfig.moonshotApiLine,
moonshotApiKey: protoConfig.moonshotApiKey,
huggingFaceApiKey: protoConfig.huggingFaceApiKey,
huggingFaceModelId: protoConfig.huggingFaceModelId,
huggingFaceModelInfo: convertProtoToModelInfo(protoConfig.huggingFaceModelInfo),
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiUrl: protoConfig.asksageApiUrl,
asksageApiKey: protoConfig.asksageApiKey,
@@ -30,6 +30,7 @@ import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
import { BedrockProvider } from "./providers/BedrockProvider"
import { MoonshotProvider } from "./providers/MoonshotProvider"
import { HuggingFaceProvider } from "./providers/HuggingFaceProvider"
import { NebiusProvider } from "./providers/NebiusProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
@@ -159,6 +160,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="moonshot">Moonshot AI</VSCodeOption>
<VSCodeOption value="huggingface">Hugging Face</VSCodeOption>
<VSCodeOption value="nebius">Nebius AI Studio</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">xAI</VSCodeOption>
@@ -261,6 +263,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "huggingface" && (
<HuggingFaceProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
@@ -0,0 +1,200 @@
import { EmptyRequest } from "@shared/proto/common"
import { 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 { huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface HuggingFaceModelPickerProps {
isPopup?: boolean
}
const HuggingFaceModelPicker: React.FC<HuggingFaceModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, huggingFaceModels: dynamicModels, setHuggingFaceModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.huggingFaceModelId || huggingFaceDefaultModelId)
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 allModels = { ...huggingFaceModels, ...dynamicModels }
handleFieldsChange({
huggingFaceModelId: newModelId,
huggingFaceModelInfo: allModels[newModelId as keyof typeof allModels],
})
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
useMount(() => {
ModelsServiceClient.refreshHuggingFaceModels(EmptyRequest.create({}))
.then((response) => {
setHuggingFaceModels({
[huggingFaceDefaultModelId]: huggingFaceModels[huggingFaceDefaultModelId],
...response.models,
})
})
.catch((err) => {
console.error("Failed to refresh Hugging Face models:", err)
})
})
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 allModels = useMemo(() => {
return { ...huggingFaceModels, ...dynamicModels }
}, [dynamicModels])
const modelIds = useMemo(() => {
return Object.keys(allModels).sort((a, b) => a.localeCompare(b))
}, [allModels])
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(() => {
let results: { id: string; html: string }[] = searchTerm
? highlight(fuse.search(searchTerm), "model-item-highlight")
: searchableItems
return results
}, [searchTerm, fuse, searchableItems])
const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (!isDropdownVisible) return
switch (e.key) {
case "ArrowDown":
e.preventDefault()
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : 0))
break
case "ArrowUp":
e.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : modelSearchResults.length - 1))
break
case "Enter":
e.preventDefault()
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
const selectedModelId = modelSearchResults[selectedIndex].id
handleModelChange(selectedModelId)
setIsDropdownVisible(false)
}
break
case "Escape":
e.preventDefault()
setIsDropdownVisible(false)
break
}
}
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && dropdownListRef.current) {
const selectedItem = itemRefs.current[selectedIndex]
const dropdown = dropdownListRef.current
const itemOffsetTop = selectedItem.offsetTop
const itemHeight = selectedItem.offsetHeight
const dropdownScrollTop = dropdown.scrollTop
const dropdownHeight = dropdown.offsetHeight
if (itemOffsetTop < dropdownScrollTop) {
dropdown.scrollTop = itemOffsetTop
} else if (itemOffsetTop + itemHeight > dropdownScrollTop + dropdownHeight) {
dropdown.scrollTop = itemOffsetTop + itemHeight - dropdownHeight
}
}
}, [selectedIndex])
return (
<div className="w-full">
<div className="flex flex-col">
<label htmlFor="hf-model-search">
<span className="font-medium">Model</span>
</label>
<div ref={dropdownRef} className="relative w-full">
<VSCodeTextField
id="hf-model-search"
placeholder="Search models..."
value={searchTerm}
onInput={(e: any) => {
setSearchTerm(e.target.value)
setIsDropdownVisible(true)
setSelectedIndex(-1)
}}
onFocus={() => setIsDropdownVisible(true)}
onKeyDown={handleKeyDown}
className="w-full relative z-[1000]"
/>
{isDropdownVisible && (
<div
ref={dropdownListRef}
className={`absolute top-[calc(100%-3px)] left-0 w-[calc(100%-2px)] ${
isPopup ? "max-h-[90px]" : "max-h-[200px]"
} overflow-y-auto bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-list-activeSelectionBackground)] z-[999] rounded-b-[3px]`}>
{modelSearchResults.map((result, index) => (
<div
key={result.id}
ref={(el: HTMLDivElement | null) => (itemRefs.current[index] = el)}
className={`p-[5px_10px] cursor-pointer break-all whitespace-normal ${
index === selectedIndex ? "bg-[var(--vscode-list-activeSelectionBackground)]" : ""
} hover:bg-[var(--vscode-list-activeSelectionBackground)]`}
onMouseEnter={() => setSelectedIndex(index)}
onClick={() => {
handleModelChange(result.id)
setIsDropdownVisible(false)
}}>
<div
dangerouslySetInnerHTML={{ __html: result.html }}
className="[&_.model-item-highlight]:bg-[var(--vscode-editor-findMatchHighlightBackground)] [&_.model-item-highlight]:text-inherit"
/>
</div>
))}
</div>
)}
</div>
</div>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</div>
)
}
export { HuggingFaceModelPicker }
@@ -0,0 +1,58 @@
import { huggingFaceModels } from "@shared/api"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { HuggingFaceModelPicker } from "../HuggingFaceModelPicker"
/**
* Props for the HuggingFaceProvider component
*/
interface HuggingFaceProviderProps {
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Hugging Face provider configuration component
*/
export const HuggingFaceProvider = ({ showModelOptions, isPopup }: HuggingFaceProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<DebouncedTextField
initialValue={apiConfiguration?.huggingFaceApiKey || ""}
onChange={(value) => handleFieldChange("huggingFaceApiKey", value)}
style={{ width: "100%" }}
type="password"
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Hugging Face API Key</span>
</DebouncedTextField>
<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. We dont show pricing here
because it depends on your Hugging Face provider settings and isnt consistently available via their API{" "}
<a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer">
Get your API key here
</a>
</p>
{showModelOptions && (
<>
<HuggingFaceModelPicker isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -36,6 +36,8 @@ import {
liteLlmModelInfoSaneDefaults,
moonshotModels,
moonshotDefaultModelId,
huggingFaceModels,
huggingFaceDefaultModelId,
nebiusModels,
nebiusDefaultModelId,
cerebrasModels,
@@ -174,6 +176,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return getProviderData(xaiModels, xaiDefaultModelId)
case "moonshot":
return getProviderData(moonshotModels, moonshotDefaultModelId)
case "huggingface":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.huggingFaceModelId || huggingFaceDefaultModelId,
selectedModelInfo: apiConfiguration?.huggingFaceModelInfo || huggingFaceModels[huggingFaceDefaultModelId],
}
case "nebius":
return getProviderData(nebiusModels, nebiusDefaultModelId)
case "sambanova":
@@ -1,4 +1,4 @@
import { useState } from "react"
import { useState, useEffect } from "react"
import { useDebounceEffect } from "@/utils/useDebounceEffect"
/**
@@ -18,6 +18,11 @@ export function useDebouncedInput<T>(
// Local state to prevent jumpy input - initialize once
const [localValue, setLocalValue] = useState(initialValue)
// Update local value when initialValue changes (e.g., when component remounts with new data)
useEffect(() => {
setLocalValue(initialValue)
}, [initialValue])
// Debounced backend save - saves after user stops changing value
useDebounceEffect(
() => {
@@ -26,6 +26,8 @@ import {
requestyDefaultModelInfo,
groqDefaultModelId,
groqModels,
huggingFaceDefaultModelId,
huggingFaceModels,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
@@ -41,6 +43,7 @@ interface ExtensionStateContextType extends ExtensionState {
openAiModels: string[]
requestyModels: Record<string, ModelInfo>
groqModels: Record<string, ModelInfo>
huggingFaceModels: Record<string, ModelInfo>
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
@@ -62,6 +65,7 @@ interface ExtensionStateContextType extends ExtensionState {
setMcpServers: (value: McpServer[]) => void
setRequestyModels: (value: Record<string, ModelInfo>) => void
setGroqModels: (value: Record<string, ModelInfo>) => void
setHuggingFaceModels: (value: Record<string, ModelInfo>) => void
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
@@ -212,6 +216,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [groqModelsState, setGroqModels] = useState<Record<string, ModelInfo>>({
[groqDefaultModelId]: groqModels[groqDefaultModelId],
})
const [huggingFaceModels, setHuggingFaceModels] = useState<Record<string, ModelInfo>>({})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
@@ -638,6 +643,7 @@ export const ExtensionStateContextProvider: React.FC<{
openAiModels,
requestyModels,
groqModels: groqModelsState,
huggingFaceModels,
mcpServers,
mcpMarketplaceCatalog,
filePaths,
@@ -678,6 +684,7 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
setGroqModels: (models: Record<string, ModelInfo>) => setGroqModels(models),
setHuggingFaceModels: (models: Record<string, ModelInfo>) => setHuggingFaceModels(models),
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
closeMcpView,