mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4d3bac66a |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
adding huggingface provider
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 don’t show pricing here
|
||||
because it depends on your Hugging Face provider settings and isn’t 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,
|
||||
|
||||
Reference in New Issue
Block a user