Skip to content

Commit 08807bb

Browse files
committed
fix: address PR #6190 review feedback
- Move huggingface-models.ts to src/api/providers/fetchers/huggingface.ts - Remove 'any' types and add proper TypeScript interfaces - Add missing i18n keys and translations for all languages - Replace magic numbers with named constants - Add JSDoc documentation for HuggingFaceModel interface - Improve error handling in API endpoint - Update model capabilities display to match other providers - Remove tool calling display (not used) - Add comprehensive test coverage for new UI features
1 parent 2a6f01d commit 08807bb

File tree

26 files changed

+560
-198
lines changed

26 files changed

+560
-198
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* HuggingFace provider constants
3+
*/
4+
5+
// Default values for HuggingFace models
6+
export const HUGGINGFACE_DEFAULT_MAX_TOKENS = 2048
7+
export const HUGGINGFACE_MAX_TOKENS_FALLBACK = 8192
8+
export const HUGGINGFACE_DEFAULT_CONTEXT_WINDOW = 128_000
9+
10+
// UI constants
11+
export const HUGGINGFACE_SLIDER_STEP = 256
12+
export const HUGGINGFACE_SLIDER_MIN = 1
13+
export const HUGGINGFACE_TEMPERATURE_MAX_VALUE = 2
14+
15+
// API constants
16+
export const HUGGINGFACE_API_URL = "https://router.huggingface.co/v1/models?collection=roocode"
17+
export const HUGGINGFACE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour

packages/types/src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "./deepseek.js"
66
export * from "./gemini.js"
77
export * from "./glama.js"
88
export * from "./groq.js"
9+
export * from "./huggingface.js"
910
export * from "./lite-llm.js"
1011
export * from "./lm-studio.js"
1112
export * from "./mistral.js"

src/api/huggingface-models.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { fetchHuggingFaceModels, type HuggingFaceModel } from "../services/huggingface-models"
1+
import { getHuggingFaceModels as fetchModels, type HuggingFaceModel } from "./providers/fetchers/huggingface"
2+
import type { ModelRecord } from "../shared/api"
23

34
export interface HuggingFaceModelsResponse {
45
models: HuggingFaceModel[]
@@ -7,11 +8,35 @@ export interface HuggingFaceModelsResponse {
78
}
89

910
export async function getHuggingFaceModels(): Promise<HuggingFaceModelsResponse> {
10-
const models = await fetchHuggingFaceModels()
11+
// Fetch models as ModelRecord
12+
const modelRecord = await fetchModels()
13+
14+
// Convert ModelRecord to array of HuggingFaceModel for backward compatibility
15+
// Note: This is a temporary solution to maintain API compatibility
16+
const models: HuggingFaceModel[] = Object.entries(modelRecord).map(([id, info]) => ({
17+
id,
18+
object: "model" as const,
19+
created: Date.now(),
20+
owned_by: "huggingface",
21+
providers: [
22+
{
23+
provider: "auto",
24+
status: "live" as const,
25+
context_length: info.contextWindow,
26+
pricing:
27+
info.inputPrice && info.outputPrice
28+
? {
29+
input: info.inputPrice,
30+
output: info.outputPrice,
31+
}
32+
: undefined,
33+
},
34+
],
35+
}))
1136

1237
return {
1338
models,
14-
cached: false, // We could enhance this to track if data came from cache
39+
cached: false,
1540
timestamp: Date.now(),
1641
}
1742
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import axios from "axios"
2+
import { z } from "zod"
3+
import type { ModelInfo } from "@roo-code/types"
4+
import {
5+
HUGGINGFACE_API_URL,
6+
HUGGINGFACE_CACHE_DURATION,
7+
HUGGINGFACE_DEFAULT_MAX_TOKENS,
8+
HUGGINGFACE_DEFAULT_CONTEXT_WINDOW,
9+
} from "@roo-code/types"
10+
import type { ModelRecord } from "../../../shared/api"
11+
12+
/**
13+
* HuggingFace Provider Schema
14+
*/
15+
const huggingFaceProviderSchema = z.object({
16+
provider: z.string(),
17+
status: z.enum(["live", "staging", "error"]),
18+
supports_tools: z.boolean().optional(),
19+
supports_structured_output: z.boolean().optional(),
20+
context_length: z.number().optional(),
21+
pricing: z
22+
.object({
23+
input: z.number(),
24+
output: z.number(),
25+
})
26+
.optional(),
27+
})
28+
29+
/**
30+
* Represents a provider that can serve a HuggingFace model
31+
* @property provider - The provider identifier (e.g., "sambanova", "together")
32+
* @property status - The current status of the provider
33+
* @property supports_tools - Whether the provider supports tool/function calling
34+
* @property supports_structured_output - Whether the provider supports structured output
35+
* @property context_length - The maximum context length supported by this provider
36+
* @property pricing - The pricing information for input/output tokens
37+
*/
38+
export type HuggingFaceProvider = z.infer<typeof huggingFaceProviderSchema>
39+
40+
/**
41+
* HuggingFace Model Schema
42+
*/
43+
const huggingFaceModelSchema = z.object({
44+
id: z.string(),
45+
object: z.literal("model"),
46+
created: z.number(),
47+
owned_by: z.string(),
48+
providers: z.array(huggingFaceProviderSchema),
49+
})
50+
51+
/**
52+
* Represents a HuggingFace model available through the router API
53+
* @property id - The unique identifier of the model
54+
* @property object - The object type (always "model")
55+
* @property created - Unix timestamp of when the model was created
56+
* @property owned_by - The organization that owns the model
57+
* @property providers - List of providers that can serve this model
58+
*/
59+
export type HuggingFaceModel = z.infer<typeof huggingFaceModelSchema>
60+
61+
/**
62+
* HuggingFace API Response Schema
63+
*/
64+
const huggingFaceApiResponseSchema = z.object({
65+
object: z.string(),
66+
data: z.array(huggingFaceModelSchema),
67+
})
68+
69+
/**
70+
* Represents the response from the HuggingFace router API
71+
* @property object - The response object type
72+
* @property data - Array of available models
73+
*/
74+
type HuggingFaceApiResponse = z.infer<typeof huggingFaceApiResponseSchema>
75+
76+
/**
77+
* Cache entry for storing fetched models
78+
* @property data - The cached model records
79+
* @property timestamp - Unix timestamp of when the cache was last updated
80+
*/
81+
interface CacheEntry {
82+
data: ModelRecord
83+
timestamp: number
84+
}
85+
86+
let cache: CacheEntry | null = null
87+
88+
/**
89+
* Parse a HuggingFace model into ModelInfo format
90+
* @param model - The HuggingFace model to parse
91+
* @param provider - Optional specific provider to use for capabilities
92+
* @returns ModelInfo object compatible with the application's model system
93+
*/
94+
function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFaceProvider): ModelInfo {
95+
// Use provider-specific values if available, otherwise find first provider with values
96+
const contextLength =
97+
provider?.context_length ||
98+
model.providers.find((p) => p.context_length)?.context_length ||
99+
HUGGINGFACE_DEFAULT_CONTEXT_WINDOW
100+
101+
const pricing = provider?.pricing || model.providers.find((p) => p.pricing)?.pricing
102+
103+
return {
104+
maxTokens: Math.min(contextLength, HUGGINGFACE_DEFAULT_MAX_TOKENS),
105+
contextWindow: contextLength,
106+
supportsImages: false, // HuggingFace API doesn't provide this info yet
107+
supportsPromptCache: false,
108+
supportsComputerUse: false,
109+
inputPrice: pricing?.input,
110+
outputPrice: pricing?.output,
111+
description: `${model.id} via HuggingFace`,
112+
}
113+
}
114+
115+
/**
116+
* Fetches available models from HuggingFace
117+
*
118+
* @returns A promise that resolves to a record of model IDs to model info
119+
* @throws Will throw an error if the request fails
120+
*/
121+
export async function getHuggingFaceModels(): Promise<ModelRecord> {
122+
const now = Date.now()
123+
124+
// Check cache
125+
if (cache && now - cache.timestamp < HUGGINGFACE_CACHE_DURATION) {
126+
console.log("Using cached HuggingFace models")
127+
return cache.data
128+
}
129+
130+
const models: ModelRecord = {}
131+
132+
try {
133+
console.log("Fetching HuggingFace models from API...")
134+
135+
const response = await axios.get<HuggingFaceApiResponse>(HUGGINGFACE_API_URL, {
136+
headers: {
137+
"Upgrade-Insecure-Requests": "1",
138+
"Sec-Fetch-Dest": "document",
139+
"Sec-Fetch-Mode": "navigate",
140+
"Sec-Fetch-Site": "none",
141+
"Sec-Fetch-User": "?1",
142+
Priority: "u=0, i",
143+
Pragma: "no-cache",
144+
"Cache-Control": "no-cache",
145+
},
146+
timeout: 10000, // 10 second timeout
147+
})
148+
149+
const result = huggingFaceApiResponseSchema.safeParse(response.data)
150+
151+
if (!result.success) {
152+
console.error("HuggingFace models response validation failed:", result.error.format())
153+
throw new Error("Invalid response format from HuggingFace API")
154+
}
155+
156+
const validModels = result.data.data.filter((model) => model.providers.length > 0)
157+
158+
for (const model of validModels) {
159+
// Add the base model
160+
models[model.id] = parseHuggingFaceModel(model)
161+
162+
// Add provider-specific variants if they have different capabilities
163+
for (const provider of model.providers) {
164+
if (provider.status === "live") {
165+
const providerKey = `${model.id}:${provider.provider}`
166+
const providerModel = parseHuggingFaceModel(model, provider)
167+
168+
// Only add provider variant if it differs from base model
169+
if (JSON.stringify(models[model.id]) !== JSON.stringify(providerModel)) {
170+
models[providerKey] = providerModel
171+
}
172+
}
173+
}
174+
}
175+
176+
// Update cache
177+
cache = {
178+
data: models,
179+
timestamp: now,
180+
}
181+
182+
console.log(`Fetched ${Object.keys(models).length} HuggingFace models`)
183+
return models
184+
} catch (error) {
185+
console.error("Error fetching HuggingFace models:", error)
186+
187+
// Return cached data if available
188+
if (cache) {
189+
console.log("Using stale cached data due to fetch error")
190+
return cache.data
191+
}
192+
193+
// Re-throw with more context
194+
if (axios.isAxiosError(error)) {
195+
if (error.response) {
196+
throw new Error(
197+
`Failed to fetch HuggingFace models: ${error.response.status} ${error.response.statusText}`,
198+
)
199+
} else if (error.request) {
200+
throw new Error(
201+
"Failed to fetch HuggingFace models: No response from server. Check your internet connection.",
202+
)
203+
}
204+
}
205+
206+
throw new Error(
207+
`Failed to fetch HuggingFace models: ${error instanceof Error ? error.message : "Unknown error"}`,
208+
)
209+
}
210+
}
211+
212+
/**
213+
* Get cached models without making an API request
214+
*/
215+
export function getCachedHuggingFaceModels(): ModelRecord | null {
216+
return cache?.data || null
217+
}
218+
219+
/**
220+
* Clear the cache
221+
*/
222+
export function clearHuggingFaceCache(): void {
223+
cache = null
224+
}

src/api/providers/huggingface.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import OpenAI from "openai"
22
import { Anthropic } from "@anthropic-ai/sdk"
33

4-
import type { ApiHandlerOptions } from "../../shared/api"
4+
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
55
import { ApiStream } from "../transform/stream"
66
import { convertToOpenAiMessages } from "../transform/openai-format"
77
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
88
import { DEFAULT_HEADERS } from "./constants"
99
import { BaseProvider } from "./base-provider"
10+
import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface"
1011

1112
export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler {
1213
private client: OpenAI
1314
private options: ApiHandlerOptions
15+
private modelCache: ModelRecord | null = null
1416

1517
constructor(options: ApiHandlerOptions) {
1618
super()
@@ -25,6 +27,20 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion
2527
apiKey: this.options.huggingFaceApiKey,
2628
defaultHeaders: DEFAULT_HEADERS,
2729
})
30+
31+
// Try to get cached models first
32+
this.modelCache = getCachedHuggingFaceModels()
33+
34+
// Fetch models asynchronously
35+
this.fetchModels()
36+
}
37+
38+
private async fetchModels() {
39+
try {
40+
this.modelCache = await getHuggingFaceModels()
41+
} catch (error) {
42+
console.error("Failed to fetch HuggingFace models:", error)
43+
}
2844
}
2945

3046
override async *createMessage(
@@ -43,6 +59,11 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion
4359
stream_options: { include_usage: true },
4460
}
4561

62+
// Add max_tokens if specified
63+
if (this.options.includeMaxTokens && this.options.modelMaxTokens) {
64+
params.max_tokens = this.options.modelMaxTokens
65+
}
66+
4667
const stream = await this.client.chat.completions.create(params)
4768

4869
for await (const chunk of stream) {
@@ -86,6 +107,18 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion
86107

87108
override getModel() {
88109
const modelId = this.options.huggingFaceModelId || "meta-llama/Llama-3.3-70B-Instruct"
110+
111+
// Try to get model info from cache
112+
const modelInfo = this.modelCache?.[modelId]
113+
114+
if (modelInfo) {
115+
return {
116+
id: modelId,
117+
info: modelInfo,
118+
}
119+
}
120+
121+
// Fallback to default values if model not found in cache
89122
return {
90123
id: modelId,
91124
info: {

0 commit comments

Comments
 (0)