Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 42 additions & 38 deletions packages/app/src/backend/routes/api/component/HuggingFace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,56 +11,60 @@ const router = express.Router();
const HUGGING_FACE_MODELS_SETTINGS_KEY = 'HuggingFaceModels';

router.post('/', async (req: Request, res: Response<APIResponse>) => {
let { data = null } = req.body;
try {
let { data = null } = req.body;

const modelName = data?.resourceKey;
const modelName = data?.resourceKey;

if (modelName) {
const modelRes = await getModelInfo(modelName);
if (modelName) {
const modelRes = await getModelInfo(modelName);

if (!modelRes?.success) {
return res.status(400).json({ success: false, error: modelRes?.error });
if (!modelRes?.success) {
return res.status(400).json({ success: false, error: modelRes?.error });
}

data = modelRes?.data;
}

data = modelRes?.data;
}
if (!data?.name) {
return res.status(400).json({ success: false, error: `Model not found!` });
}

if (!data?.name) {
return res.status(400).json({ success: false, error: `Model not found!` });
}
if (!data?.inference) {
return res
.status(400)
.json({ success: false, error: `Currently, we support models with Hosted Inference API.` });
}

if (!data?.inference) {
return res
.status(400)
.json({ success: false, error: `Currently, we support models with Hosted Inference API.` });
}
if (!data?.modelTask) {
return res
.status(400)
.json({ success: false, error: `Currently, we support models with "task"` });
}

if (!data?.modelTask) {
return res
.status(400)
.json({ success: false, error: `Currently, we support models with "task"` });
}
if (!supportedHfTasks.includes(data?.modelTask)) {
return res.status(400).json({
success: false,
error: `Currently, Models under "${kebabToCapitalize(
data?.modelTask,
)}" task is not supported`,
});
}

if (!supportedHfTasks.includes(data?.modelTask)) {
return res.status(400).json({
success: false,
error: `Currently, Models under "${kebabToCapitalize(
data?.modelTask,
)}" task is not supported`,
});
}
const settingsRes = await userData.saveUserSettings(
req?.user?.accessToken,
HUGGING_FACE_MODELS_SETTINGS_KEY,
data,
);

const settingsRes = await userData.saveUserSettings(
req?.user?.accessToken,
HUGGING_FACE_MODELS_SETTINGS_KEY,
data,
);
if (!settingsRes?.success) {
return res.status(400).json({ success: false, error: settingsRes?.error });
}

if (!settingsRes?.success) {
return res.status(400).json({ success: false, error: settingsRes?.error });
res.send({ success: true, data });
} catch (error) {
return res.status(500).json({ success: false, error: error?.message || 'Something went wrong!' });
}

res.send({ success: true, data });
});

router.get('/', async (req, res) => {
Expand Down
97 changes: 79 additions & 18 deletions packages/app/src/backend/services/huggingFace.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import axios from 'axios';
import http from 'http';
import https from 'https';
import { load as cheerioLoad } from 'cheerio';
import he from 'he';

Expand All @@ -9,13 +11,32 @@ import * as openai from './openai-helper';
import Cache from './Cache.class';
import Store from './Store.class';

// Node.js 20+ enables the Happy Eyeballs algorithm (autoSelectFamily) by default, which
// attempts IPv6 and IPv4 connections in parallel. The default fallback timeout is 250ms,
// which can be too short on networks with broken or slow IPv6 — the IPv4 fallback doesn't
// get enough time to connect before the whole attempt is aborted (ETIMEDOUT).
//
// We increase autoSelectFamilyAttemptTimeout to 2000ms to give the IPv4 fallback enough
// time to establish a connection when IPv6 fails.
//
// Troubleshooting: If HuggingFace requests still time out locally, try increasing the
// value (e.g. 2000, 5000). To diagnose, run in terminal:
// curl -4 --connect-timeout 5 https://huggingface.co/api/models?limit=1
// If curl works but Node.js doesn't, the issue is Happy Eyeballs — increase the timeout.
// If curl also fails, it's a network/firewall issue unrelated to this setting.
const hfAxios = axios.create({
timeout: 30000,
httpAgent: new http.Agent({ keepAlive: true, autoSelectFamilyAttemptTimeout: 2000 }),
httpsAgent: new https.Agent({ keepAlive: true, autoSelectFamilyAttemptTimeout: 2000 }),
});

const modelInfoCache = new Cache({ directory: 'hf-model-info' });
const modelResultCache = new Cache({ directory: 'hf-model-result' });
const store = new Store('huggingFace/leftover-result');

const _getModelLogo = async (modelName: string): Promise<string> => {
try {
const res = await axios.get(`https://huggingface.co/${modelName}`);
const res = await hfAxios.get(`https://huggingface.co/${modelName}`);

const $ = await cheerioLoad(res?.data);
const imageElement = $('main header h1 > div:first-child > div:first-child img');
Expand Down Expand Up @@ -70,21 +91,35 @@ type ModelInfo = {
*/
const _crawlModelInfo = async (modelName: string): Promise<ModelInfo> => {
try {
const res = await axios.get(`https://huggingface.co/${modelName}`);
const res = await hfAxios.get(`https://huggingface.co/${modelName}`);

const $ = await cheerioLoad(res?.data);

const dataElm = $('main > .SVELTE_HYDRATER.contents');
const data = dataElm.attr('data-props');
if (!data) return null;
const decodedStr = he.decode(data);
const modelInfo = JSON.parse(decodedStr);

const modelId = modelInfo?.model?.id;
const modelTask = modelInfo?.model?.pipeline_tag;
let modelTask = modelInfo?.model?.pipeline_tag;

// Make sure we have the required info
if (!modelId || !modelTask) return null;

// Resolve the effective task using inference provider mappings.
// The crawled data may include the mapping; if not, fetch it from the API for text-generation models.
let providerMapping = modelInfo?.model?.inferenceProviderMapping;
if (!providerMapping && modelTask === 'text-generation') {
try {
const apiModel = await _fetchModel(modelName);
providerMapping = apiModel?.inferenceProviderMapping;
} catch {
// If API call fails, keep the crawled task
}
}
modelTask = _resolveEffectiveTask(modelTask, providerMapping);

const inference = modelInfo?.model?.inference;

let logoUrl = modelInfo?.author?.avatarUrl;
Expand Down Expand Up @@ -114,13 +149,38 @@ const _crawlModelInfo = async (modelName: string): Promise<ModelInfo> => {

const _fetchModel = async (modelName: string) => {
try {
const res = await axios.get(`https://huggingface.co/api/models/${modelName}`);
const res = await hfAxios.get(`https://huggingface.co/api/models/${modelName}`, {
params: { 'expand[]': 'inferenceProviderMapping' },
});
return res?.data;
} catch (error) {
throw { message: error?.response?.data?.error || `Hugging Face Model not found!` };
}
};

/**
* Resolve the effective task by checking inference provider mappings.
* Modern LLMs often have pipeline_tag "text-generation" but inference providers
* only support "conversational" (chatCompletion). This detects the correct task.
*/
const _resolveEffectiveTask = (pipelineTag: string, inferenceProviderMapping: any): string => {
if (!pipelineTag || !inferenceProviderMapping) return pipelineTag;

const mappings = Array.isArray(inferenceProviderMapping)
? inferenceProviderMapping
: Object.entries(inferenceProviderMapping).map(
([provider, mapping]: [string, any]) => ({ provider, task: mapping.task }),
);

if (mappings.length === 0) return pipelineTag;

const exactMatch = mappings.find((m: any) => m.task === pipelineTag);
if (exactMatch) return pipelineTag;

const resolvedTask = mappings[0]?.task;
return resolvedTask && supportedHfTasks.includes(resolvedTask) ? resolvedTask : pipelineTag;
};

/**
* If the crawling approach fails, then fallback to this approach
* @param {string} modelName
Expand All @@ -132,7 +192,7 @@ const _fallbackModelInfo = async (modelName: string): Promise<ModelInfo> => {

const id = model?.id;
const modelId = model?.modelId;
const modelTask = model?.pipeline_tag;
const modelTask = _resolveEffectiveTask(model?.pipeline_tag, model?.inferenceProviderMapping);
const inference = model?.cardData?.inference;

const logoUrl = await _getModelLogo(modelName);
Expand Down Expand Up @@ -179,7 +239,7 @@ const _fetchModels = async ({

if (cursor) params['cursor'] = cursor;

const result = await axios.get(url, { params });
const result = await hfAxios.get(url, { params });

const cursors = getCursorFromLinkHeader(result?.headers?.link);

Expand All @@ -191,30 +251,30 @@ const _fetchModels = async ({

const _getModelInfo = async (modelName: string, modelTask: string): Promise<ModelInfo> => {
try {
let modelsInfo = await modelInfoCache.get(modelTask);
modelsInfo = modelsInfo?.data;

let modelInfo = {};
if (modelTask) {
let modelsInfo = await modelInfoCache.get(modelTask);
modelsInfo = modelsInfo?.data;

// If we have the model info in cache, then return it
if (isValidObj(modelsInfo)) {
modelInfo = modelsInfo?.[modelName];
// If we have the model info in cache, then return it
if (isValidObj(modelsInfo)) {
const cachedInfo = modelsInfo?.[modelName];

if (isValidObj(modelInfo)) {
return modelInfo;
if (isValidObj(cachedInfo)) {
return cachedInfo;
}
}
}

// If we don't have the model info in cache, then crawl it
modelInfo = await _crawlModelInfo(modelName);
let modelInfo = await _crawlModelInfo(modelName);

// If crawling fails, then fallback to the API approach
if (!modelInfo) {
modelInfo = await _fallbackModelInfo(modelName);
}

// Update the cache
if (isValidObj(modelInfo)) {
if (modelTask && isValidObj(modelInfo)) {
modelInfoCache.update(modelTask, modelName, modelInfo);
}

Expand Down Expand Up @@ -422,8 +482,9 @@ const _filterModels: FilterModels = async ({ retry = 0, search = '', cursors, pa
export async function getModelInfo(modelName: string): Promise<APIResponse> {
try {
const model = await _fetchModel(modelName);
const effectiveTask = _resolveEffectiveTask(model?.pipeline_tag, model?.inferenceProviderMapping);

const modelInfo = await _getModelInfo(modelName, model?.pipeline_tag);
const modelInfo = await _getModelInfo(modelName, effectiveTask);

return { success: true, data: modelInfo };
} catch (error) {
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/builder-ui/components/HuggingFace.class.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Component } from './Component.class';
import hfParams from '../params/hugging-face.params.json';
import {
kebabToCapitalize,
handleKvFieldEditBtn,
setLogoForDynamicComp,
kebabToCapitalize,
promptVaultInfo,
setLogoForDynamicComp,
} from '../utils';
import { Component } from './Component.class';

declare var Metro;

Expand Down
Loading