mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
fetch hf models and providers
This commit is contained in:
parent
be4fcfa9de
commit
921ede741d
6 changed files with 371 additions and 8 deletions
17
src/api/huggingface-models.ts
Normal file
17
src/api/huggingface-models.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { fetchHuggingFaceModels, type HuggingFaceModel } from "../services/huggingface-models"
|
||||
|
||||
export interface HuggingFaceModelsResponse {
|
||||
models: HuggingFaceModel[]
|
||||
cached: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export async function getHuggingFaceModels(): Promise<HuggingFaceModelsResponse> {
|
||||
const models = await fetchHuggingFaceModels()
|
||||
|
||||
return {
|
||||
models,
|
||||
cached: false, // We could enhance this to track if data came from cache
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
|
@ -674,6 +674,22 @@ export const webviewMessageHandler = async (
|
|||
// TODO: Cache like we do for OpenRouter, etc?
|
||||
provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
|
||||
break
|
||||
case "requestHuggingFaceModels":
|
||||
try {
|
||||
const { getHuggingFaceModels } = await import("../../api/huggingface-models")
|
||||
const huggingFaceModelsResponse = await getHuggingFaceModels()
|
||||
provider.postMessageToWebview({
|
||||
type: "huggingFaceModels",
|
||||
huggingFaceModels: huggingFaceModelsResponse.models,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Hugging Face models:", error)
|
||||
provider.postMessageToWebview({
|
||||
type: "huggingFaceModels",
|
||||
huggingFaceModels: [],
|
||||
})
|
||||
}
|
||||
break
|
||||
case "openImage":
|
||||
openImage(message.text!, { values: message.values })
|
||||
break
|
||||
|
|
|
|||
171
src/services/huggingface-models.ts
Normal file
171
src/services/huggingface-models.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
export interface HuggingFaceModel {
|
||||
_id: string
|
||||
id: string
|
||||
inferenceProviderMapping: InferenceProviderMapping[]
|
||||
trendingScore: number
|
||||
config: ModelConfig
|
||||
tags: string[]
|
||||
pipeline_tag: "text-generation" | "image-text-to-text"
|
||||
library_name?: string
|
||||
}
|
||||
|
||||
export interface InferenceProviderMapping {
|
||||
provider: string
|
||||
providerId: string
|
||||
status: "live" | "staging" | "error"
|
||||
task: "conversational"
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
architectures: string[]
|
||||
model_type: string
|
||||
tokenizer_config?: {
|
||||
chat_template?: string | Array<{ name: string; template: string }>
|
||||
model_max_length?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface HuggingFaceApiParams {
|
||||
pipeline_tag?: "text-generation" | "image-text-to-text"
|
||||
filter: string
|
||||
inference_provider: string
|
||||
limit: number
|
||||
expand: string[]
|
||||
}
|
||||
|
||||
const DEFAULT_PARAMS: HuggingFaceApiParams = {
|
||||
filter: "conversational",
|
||||
inference_provider: "all",
|
||||
limit: 100,
|
||||
expand: [
|
||||
"inferenceProviderMapping",
|
||||
"config",
|
||||
"library_name",
|
||||
"pipeline_tag",
|
||||
"tags",
|
||||
"mask_token",
|
||||
"trendingScore",
|
||||
],
|
||||
}
|
||||
|
||||
const BASE_URL = "https://huggingface.co/api/models"
|
||||
const CACHE_DURATION = 1000 * 60 * 60 // 1 hour
|
||||
|
||||
interface CacheEntry {
|
||||
data: HuggingFaceModel[]
|
||||
timestamp: number
|
||||
status: "success" | "partial" | "error"
|
||||
}
|
||||
|
||||
let cache: CacheEntry | null = null
|
||||
|
||||
function buildApiUrl(params: HuggingFaceApiParams): string {
|
||||
const url = new URL(BASE_URL)
|
||||
|
||||
// Add simple params
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (!Array.isArray(value)) {
|
||||
url.searchParams.append(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
// Handle array params specially
|
||||
params.expand.forEach((item) => {
|
||||
url.searchParams.append("expand[]", item)
|
||||
})
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
const headers: HeadersInit = {
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
Priority: "u=0, i",
|
||||
Pragma: "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
}
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
credentials: "include",
|
||||
headers,
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
}
|
||||
|
||||
export async function fetchHuggingFaceModels(): Promise<HuggingFaceModel[]> {
|
||||
const now = Date.now()
|
||||
|
||||
// Check cache
|
||||
if (cache && now - cache.timestamp < CACHE_DURATION) {
|
||||
console.log("Using cached Hugging Face models")
|
||||
return cache.data
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Fetching Hugging Face models from API...")
|
||||
|
||||
// Fetch both text-generation and image-text-to-text models in parallel
|
||||
const [textGenResponse, imgTextResponse] = await Promise.allSettled([
|
||||
fetch(buildApiUrl({ ...DEFAULT_PARAMS, pipeline_tag: "text-generation" }), requestInit),
|
||||
fetch(buildApiUrl({ ...DEFAULT_PARAMS, pipeline_tag: "image-text-to-text" }), requestInit),
|
||||
])
|
||||
|
||||
let textGenModels: HuggingFaceModel[] = []
|
||||
let imgTextModels: HuggingFaceModel[] = []
|
||||
let hasErrors = false
|
||||
|
||||
// Process text-generation models
|
||||
if (textGenResponse.status === "fulfilled" && textGenResponse.value.ok) {
|
||||
textGenModels = await textGenResponse.value.json()
|
||||
} else {
|
||||
console.error("Failed to fetch text-generation models:", textGenResponse)
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
// Process image-text-to-text models
|
||||
if (imgTextResponse.status === "fulfilled" && imgTextResponse.value.ok) {
|
||||
imgTextModels = await imgTextResponse.value.json()
|
||||
} else {
|
||||
console.error("Failed to fetch image-text-to-text models:", imgTextResponse)
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
// Combine and filter models
|
||||
const allModels = [...textGenModels, ...imgTextModels]
|
||||
.filter((model) => model.inferenceProviderMapping.length > 0)
|
||||
.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()))
|
||||
|
||||
// Update cache
|
||||
cache = {
|
||||
data: allModels,
|
||||
timestamp: now,
|
||||
status: hasErrors ? "partial" : "success",
|
||||
}
|
||||
|
||||
console.log(`Fetched ${allModels.length} Hugging Face models (status: ${cache.status})`)
|
||||
return allModels
|
||||
} catch (error) {
|
||||
console.error("Error fetching Hugging Face models:", error)
|
||||
|
||||
// Return cached data if available
|
||||
if (cache) {
|
||||
console.log("Using stale cached data due to fetch error")
|
||||
cache.status = "error"
|
||||
return cache.data
|
||||
}
|
||||
|
||||
// No cache available, return empty array
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedModels(): HuggingFaceModel[] | null {
|
||||
return cache?.data || null
|
||||
}
|
||||
|
||||
export function clearCache(): void {
|
||||
cache = null
|
||||
}
|
||||
|
|
@ -67,6 +67,7 @@ export interface ExtensionMessage {
|
|||
| "ollamaModels"
|
||||
| "lmStudioModels"
|
||||
| "vsCodeLmModels"
|
||||
| "huggingFaceModels"
|
||||
| "vsCodeLmApiAvailable"
|
||||
| "updatePrompt"
|
||||
| "systemPrompt"
|
||||
|
|
@ -135,6 +136,28 @@ export interface ExtensionMessage {
|
|||
ollamaModels?: string[]
|
||||
lmStudioModels?: string[]
|
||||
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
||||
huggingFaceModels?: Array<{
|
||||
_id: string
|
||||
id: string
|
||||
inferenceProviderMapping: Array<{
|
||||
provider: string
|
||||
providerId: string
|
||||
status: "live" | "staging" | "error"
|
||||
task: "conversational"
|
||||
}>
|
||||
trendingScore: number
|
||||
config: {
|
||||
architectures: string[]
|
||||
model_type: string
|
||||
tokenizer_config?: {
|
||||
chat_template?: string | Array<{ name: string; template: string }>
|
||||
model_max_length?: number
|
||||
}
|
||||
}
|
||||
tags: string[]
|
||||
pipeline_tag: "text-generation" | "image-text-to-text"
|
||||
library_name?: string
|
||||
}>
|
||||
mcpServers?: McpServer[]
|
||||
commits?: GitCommit[]
|
||||
listApiConfig?: ProviderSettingsEntry[]
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export interface WebviewMessage {
|
|||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "requestVsCodeLmModels"
|
||||
| "requestHuggingFaceModels"
|
||||
| "openImage"
|
||||
| "saveImage"
|
||||
| "openFile"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,40 @@
|
|||
import { useCallback } from "react"
|
||||
import { useCallback, useState, useEffect, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
import { SearchableSelect, type SearchableSelectOption } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type HuggingFaceModel = {
|
||||
_id: string
|
||||
id: string
|
||||
inferenceProviderMapping: Array<{
|
||||
provider: string
|
||||
providerId: string
|
||||
status: "live" | "staging" | "error"
|
||||
task: "conversational"
|
||||
}>
|
||||
trendingScore: number
|
||||
config: {
|
||||
architectures: string[]
|
||||
model_type: string
|
||||
tokenizer_config?: {
|
||||
chat_template?: string | Array<{ name: string; template: string }>
|
||||
model_max_length?: number
|
||||
}
|
||||
}
|
||||
tags: string[]
|
||||
pipeline_tag: "text-generation" | "image-text-to-text"
|
||||
library_name?: string
|
||||
}
|
||||
|
||||
type HuggingFaceProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
|
|
@ -15,6 +42,9 @@ type HuggingFaceProps = {
|
|||
|
||||
export const HuggingFace = ({ apiConfiguration, setApiConfigurationField }: HuggingFaceProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [models, setModels] = useState<HuggingFaceModel[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("")
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
|
|
@ -27,6 +57,71 @@ export const HuggingFace = ({ apiConfiguration, setApiConfigurationField }: Hugg
|
|||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
// Fetch models when component mounts
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
vscode.postMessage({ type: "requestHuggingFaceModels" })
|
||||
}, [])
|
||||
|
||||
// Handle messages from extension
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
switch (message.type) {
|
||||
case "huggingFaceModels":
|
||||
setModels(message.huggingFaceModels || [])
|
||||
setLoading(false)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
// Get current model and its providers
|
||||
const currentModel = models.find((m) => m.id === apiConfiguration?.huggingFaceModelId)
|
||||
const availableProviders = useMemo(
|
||||
() => currentModel?.inferenceProviderMapping || [],
|
||||
[currentModel?.inferenceProviderMapping],
|
||||
)
|
||||
|
||||
// Set default provider when model changes
|
||||
useEffect(() => {
|
||||
if (currentModel && availableProviders.length > 0) {
|
||||
const currentProvider = availableProviders.find((p) => p.provider === selectedProvider)
|
||||
if (!currentProvider) {
|
||||
// Set to first available provider or "auto"
|
||||
setSelectedProvider("auto")
|
||||
}
|
||||
}
|
||||
}, [currentModel, availableProviders, selectedProvider])
|
||||
|
||||
const handleModelSelect = (modelId: string) => {
|
||||
setApiConfigurationField("huggingFaceModelId", modelId)
|
||||
// Reset provider selection when model changes
|
||||
setSelectedProvider("auto")
|
||||
}
|
||||
|
||||
const handleProviderSelect = (provider: string) => {
|
||||
setSelectedProvider(provider)
|
||||
// You could store this in a separate field if needed
|
||||
}
|
||||
|
||||
// Format provider name for display
|
||||
const formatProviderName = (provider: string) => {
|
||||
const nameMap: Record<string, string> = {
|
||||
sambanova: "SambaNova",
|
||||
"fireworks-ai": "Fireworks",
|
||||
together: "Together AI",
|
||||
nebius: "Nebius AI Studio",
|
||||
hyperbolic: "Hyperbolic",
|
||||
novita: "Novita",
|
||||
cohere: "Cohere",
|
||||
"hf-inference": "HF Inference API",
|
||||
replicate: "Replicate",
|
||||
}
|
||||
return nameMap[provider] || provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
|
|
@ -37,16 +132,56 @@ export const HuggingFace = ({ apiConfiguration, setApiConfigurationField }: Hugg
|
|||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.huggingFaceApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.huggingFaceModelId || ""}
|
||||
onInput={handleInputChange("huggingFaceModelId")}
|
||||
placeholder="meta-llama/Llama-3.3-70B-Instruct"
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.huggingFaceModelId")}</label>
|
||||
</VSCodeTextField>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="block font-medium text-sm">
|
||||
{t("settings:providers.huggingFaceModelId")}
|
||||
{loading && <span className="text-xs text-gray-400 ml-2">Loading...</span>}
|
||||
{!loading && <span className="text-xs text-gray-400 ml-2">({models.length} models)</span>}
|
||||
</label>
|
||||
|
||||
<SearchableSelect
|
||||
value={apiConfiguration?.huggingFaceModelId || ""}
|
||||
onValueChange={handleModelSelect}
|
||||
options={models.map(
|
||||
(model): SearchableSelectOption => ({
|
||||
value: model.id,
|
||||
label: model.id,
|
||||
}),
|
||||
)}
|
||||
placeholder="Select a model..."
|
||||
searchPlaceholder="Search models..."
|
||||
emptyMessage="No models found"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentModel && availableProviders.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="block font-medium text-sm">Provider</label>
|
||||
<SearchableSelect
|
||||
value={selectedProvider}
|
||||
onValueChange={handleProviderSelect}
|
||||
options={[
|
||||
{ value: "auto", label: "Auto" },
|
||||
...availableProviders.map(
|
||||
(mapping): SearchableSelectOption => ({
|
||||
value: mapping.provider,
|
||||
label: `${formatProviderName(mapping.provider)} (${mapping.status})`,
|
||||
}),
|
||||
),
|
||||
]}
|
||||
placeholder="Select a provider..."
|
||||
searchPlaceholder="Search providers..."
|
||||
emptyMessage="No providers found"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
|
||||
{!apiConfiguration?.huggingFaceApiKey && (
|
||||
<VSCodeButtonLink href="https://huggingface.co/settings/tokens" appearance="secondary">
|
||||
{t("settings:providers.getHuggingFaceApiKey")}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue