feat: add TextChat component with support for reasoning models, file attachments, and model management functionality
This commit is contained in:
1 parent
8fa0a670c1
commit
822e9c9481
5 files changed
+133
-22
No files matched your search
+55
-1
@@ -20,6 +20,7 @@ const llmHttpAgent = new http.Agent({
|
||||
|
||||
const HF_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const hfModelCache = new Map();
|
||||
const hfProjectorCache = new Map();
|
||||
|
||||
function readPort(value, fallback) {
|
||||
const port = parseInt(value, 10);
|
||||
@@ -2139,6 +2140,50 @@ function selectMmprojFile(siblings = [], modelFilename = "") {
|
||||
return scored[0]?.name || "";
|
||||
}
|
||||
|
||||
function parseHuggingFaceResolveUrl(fileUrl) {
|
||||
try {
|
||||
const parsed = new URL(fileUrl);
|
||||
if (!/^(?:www\.)?huggingface\.co$/i.test(parsed.hostname)) return null;
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
const resolveIndex = parts.findIndex((part) => part === "resolve" || part === "blob");
|
||||
if (resolveIndex < 2 || resolveIndex + 2 >= parts.length) return null;
|
||||
const repoId = `${decodeURIComponent(parts[0])}/${decodeURIComponent(parts[1])}`;
|
||||
const revision = decodeURIComponent(parts[resolveIndex + 1] || "main");
|
||||
const repositoryFilename = parts.slice(resolveIndex + 2).map(decodeURIComponent).join("/");
|
||||
return { repoId, revision, repositoryFilename };
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveHuggingFaceProjector(fileUrl, localModelFilename = "") {
|
||||
const parsed = parseHuggingFaceResolveUrl(fileUrl);
|
||||
if (!parsed) return null;
|
||||
|
||||
const cacheKey = `${parsed.repoId}|${parsed.revision}|${parsed.repositoryFilename}`;
|
||||
const cached = hfProjectorCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.createdAt < HF_MODEL_CACHE_TTL_MS) return cached.projector;
|
||||
|
||||
try {
|
||||
const treeUrl = `https://huggingface.co/api/models/${parsed.repoId}/tree/${encodeURIComponent(parsed.revision)}?recursive=true`;
|
||||
const tree = await requestHttpsJson(treeUrl);
|
||||
const projectorRepositoryFilename = selectMmprojFile(tree, parsed.repositoryFilename || localModelFilename);
|
||||
const projector = projectorRepositoryFilename
|
||||
? {
|
||||
url: `https://huggingface.co/${parsed.repoId}/resolve/${encodeURIComponent(parsed.revision)}/${projectorRepositoryFilename.split("/").map(encodeURIComponent).join("/")}`,
|
||||
filename: `${parsed.repoId.replace(/\//g, "--")}--${path.basename(projectorRepositoryFilename)}`,
|
||||
repositoryFilename: projectorRepositoryFilename,
|
||||
}
|
||||
: null;
|
||||
hfProjectorCache.set(cacheKey, { createdAt: Date.now(), projector });
|
||||
return projector;
|
||||
} catch (err) {
|
||||
console.warn(` [download] Could not resolve vision projector for ${parsed.repoId}: ${err.message || err}`);
|
||||
hfProjectorCache.set(cacheKey, { createdAt: Date.now(), projector: null });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function classifyHuggingFaceModel(model, filename) {
|
||||
const searchable = `${model.id || ""} ${filename || ""} ${(model.tags || []).join(" ")}`.toLowerCase();
|
||||
const parameters = getModelParameterBillions(searchable);
|
||||
@@ -4340,8 +4385,17 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
|
||||
if (!filename.toLowerCase().endsWith(".gguf")) {
|
||||
return json(res, 400, { ok: false, error: "Text model URL must point to a .gguf file." });
|
||||
}
|
||||
const projector = body.projectorUrl && body.projectorFilename
|
||||
? { url: String(body.projectorUrl), filename: path.basename(String(body.projectorFilename)) }
|
||||
: await resolveHuggingFaceProjector(directUrl, filename);
|
||||
startModelDownload(directUrl, filename, LLM_MODELS, "text");
|
||||
return json(res, 200, { ok: true, message: "Text model download started", filename });
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
message: "Text model download started",
|
||||
filename,
|
||||
projectorUrl: projector?.url || "",
|
||||
projectorFilename: projector?.filename || "",
|
||||
});
|
||||
} catch (err) {
|
||||
return json(res, 400, { ok: false, error: err.message || String(err) });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user