{"version":3,"file":"huggingface.d.ts","sourceRoot":"","sources":["../../../src/extensions/llama/huggingface.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IACjC,aAAa,EAAE,uBAAuB,EAAE,CAAC;CACzC;AAsBD,wBAAsB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAe5G;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,YAAY,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,SAA2B,EAG7D;YAEa,OAAO;IA4Bf,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgB7E;IAEK,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAuChF;CACD","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nconst DEFAULT_HUGGING_FACE_URL = \"https://huggingface.co\";\nconst QUANTIZATION_PATTERN =\n\t/(?:^|[-_.])((?:UD-)?(?:IQ\\d(?:_[A-Z0-9]+)+|Q\\d(?:_[A-Z0-9]+)+|BF16|F16|F32|MXFP\\d(?:_[A-Z0-9]+)*))$/iu;\nconst SHARD_SUFFIX_PATTERN = /-\\d{5}-of-\\d{5}$/u;\n\nexport interface HuggingFaceModel {\n\tid: string;\n\tdownloads: number;\n}\n\nexport interface HuggingFaceQuantization {\n\tname: string;\n\tsize?: number;\n}\n\nexport interface HuggingFaceModelDetails {\n\tid: string;\n\tgated: false | \"auto\" | \"manual\";\n\tquantizations: HuggingFaceQuantization[];\n}\n\nfunction payloadError(payload: unknown, fallback: string): string {\n\tif (typeof payload !== \"object\" || payload === null) return fallback;\n\tconst error = (payload as { error?: unknown }).error;\n\treturn typeof error === \"string\" && error ? error : fallback;\n}\n\nfunction parseRateLimitDelay(value: string | null): number | undefined {\n\tconst match = value?.match(/(?:^|;)t=(\\d+)/u);\n\treturn match ? Number(match[1]) : undefined;\n}\n\nasync function readToken(path: string): Promise<string | undefined> {\n\ttry {\n\t\tconst token = (await readFile(path, \"utf8\")).trim();\n\t\treturn token || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport async function findHuggingFaceToken(env: NodeJS.ProcessEnv = process.env): Promise<string | undefined> {\n\tconst fromEnvironment = env.HF_TOKEN?.trim();\n\tif (fromEnvironment) return fromEnvironment;\n\n\tconst paths = [\n\t\tenv.HF_TOKEN_PATH,\n\t\tenv.HF_HOME ? join(env.HF_HOME, \"token\") : undefined,\n\t\tenv.XDG_CACHE_HOME ? join(env.XDG_CACHE_HOME, \"huggingface\", \"token\") : undefined,\n\t\tjoin(homedir(), \".cache\", \"huggingface\", \"token\"),\n\t].filter((path): path is string => Boolean(path));\n\tfor (const path of new Set(paths)) {\n\t\tconst token = await readToken(path);\n\t\tif (token) return token;\n\t}\n\treturn undefined;\n}\n\nexport class HuggingFaceClient {\n\tprivate readonly token: string | undefined;\n\tprivate readonly baseUrl: string;\n\n\tconstructor(token?: string, baseUrl = DEFAULT_HUGGING_FACE_URL) {\n\t\tthis.token = token;\n\t\tthis.baseUrl = baseUrl.replace(/\\/+$/u, \"\");\n\t}\n\n\tprivate async request(path: string, signal?: AbortSignal): Promise<unknown> {\n\t\tconst headers = new Headers();\n\t\tif (this.token) headers.set(\"Authorization\", `Bearer ${this.token}`);\n\t\tconst timeout = AbortSignal.timeout(15_000);\n\t\tconst response = await fetch(`${this.baseUrl}${path}`, {\n\t\t\theaders,\n\t\t\tsignal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n\t\t});\n\t\tlet payload: unknown;\n\t\ttry {\n\t\t\tpayload = await response.json();\n\t\t} catch {\n\t\t\tpayload = undefined;\n\t\t}\n\t\tif (!response.ok) {\n\t\t\tconst fallback = `Hugging Face returned HTTP ${response.status}`;\n\t\t\tif (response.status === 429) {\n\t\t\t\tconst delay =\n\t\t\t\t\tNumber(response.headers.get(\"retry-after\")) || parseRateLimitDelay(response.headers.get(\"ratelimit\"));\n\t\t\t\tthrow new Error(\n\t\t\t\t\tdelay ? `Hugging Face rate limit reached; retry in ${delay}s` : \"Hugging Face rate limit reached\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow new Error(payloadError(payload, fallback));\n\t\t}\n\t\treturn payload;\n\t}\n\n\tasync search(query: string, signal?: AbortSignal): Promise<HuggingFaceModel[]> {\n\t\tconst params = new URLSearchParams({\n\t\t\tsearch: query,\n\t\t\tfilter: \"gguf\",\n\t\t\tsort: \"downloads\",\n\t\t\tdirection: \"-1\",\n\t\t\tlimit: \"20\",\n\t\t});\n\t\tconst payload = await this.request(`/api/models?${params}`, signal);\n\t\tif (!Array.isArray(payload)) throw new Error(\"Hugging Face returned invalid search results\");\n\t\treturn payload.flatMap((value) => {\n\t\t\tif (typeof value !== \"object\" || value === null || typeof (value as { id?: unknown }).id !== \"string\")\n\t\t\t\treturn [];\n\t\t\tconst model = value as { id: string; downloads?: unknown };\n\t\t\treturn [{ id: model.id, downloads: typeof model.downloads === \"number\" ? model.downloads : 0 }];\n\t\t});\n\t}\n\n\tasync details(id: string, signal?: AbortSignal): Promise<HuggingFaceModelDetails> {\n\t\tconst encodedId = id.split(\"/\").map(encodeURIComponent).join(\"/\");\n\t\tconst payload = await this.request(`/api/models/${encodedId}?blobs=true`, signal);\n\t\tif (typeof payload !== \"object\" || payload === null) {\n\t\t\tthrow new Error(\"Hugging Face returned invalid model details\");\n\t\t}\n\t\tconst model = payload as { id?: unknown; gated?: unknown; siblings?: unknown };\n\t\tconst sizes = new Map<string, { total: number; complete: boolean }>();\n\t\tif (Array.isArray(model.siblings)) {\n\t\t\tfor (const value of model.siblings) {\n\t\t\t\tif (typeof value !== \"object\" || value === null) continue;\n\t\t\t\tconst file = value as { rfilename?: unknown; size?: unknown };\n\t\t\t\tif (typeof file.rfilename !== \"string\" || !file.rfilename.toLowerCase().endsWith(\".gguf\")) continue;\n\t\t\t\tconst filename = file.rfilename.split(\"/\").at(-1)!;\n\t\t\t\tif (filename.toLowerCase().startsWith(\"mmproj\")) continue;\n\t\t\t\tconst stem = filename.slice(0, -5).replace(SHARD_SUFFIX_PATTERN, \"\");\n\t\t\t\tconst quantization = stem.match(QUANTIZATION_PATTERN)?.[1]?.toUpperCase();\n\t\t\t\tif (!quantization) continue;\n\t\t\t\tconst current = sizes.get(quantization) ?? { total: 0, complete: true };\n\t\t\t\tif (typeof file.size === \"number\") current.total += file.size;\n\t\t\t\telse current.complete = false;\n\t\t\t\tsizes.set(quantization, current);\n\t\t\t}\n\t\t}\n\t\tconst quantizations = [...sizes]\n\t\t\t.map(([name, size]) => ({ name, size: size.complete ? size.total : undefined }))\n\t\t\t.sort((left, right) => {\n\t\t\t\tif (left.name === \"Q4_K_M\") return -1;\n\t\t\t\tif (right.name === \"Q4_K_M\") return 1;\n\t\t\t\treturn (\n\t\t\t\t\t(left.size ?? Number.MAX_SAFE_INTEGER) - (right.size ?? Number.MAX_SAFE_INTEGER) ||\n\t\t\t\t\tleft.name.localeCompare(right.name)\n\t\t\t\t);\n\t\t\t});\n\t\treturn {\n\t\t\tid: typeof model.id === \"string\" ? model.id : id,\n\t\t\tgated: model.gated === \"auto\" || model.gated === \"manual\" ? model.gated : false,\n\t\t\tquantizations,\n\t\t};\n\t}\n}\n"]}