From 86e487bdb30d658bcf9a70491bbf39f3bc047948 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:39:43 -0300 Subject: [PATCH] fix(db): validate HuggingFace tokens via whoami-v2 auth probe (#4819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.37 — defining commit re-homed onto the god-file-split validation module (validateHuggingFaceProvider in validation/openaiFormat.ts + map wiring); 115/115 green. --- CHANGELOG.md | 2 + src/lib/providers/validation.ts | 2 + src/lib/providers/validation/openaiFormat.ts | 29 +++++++++++ .../provider-validation-specialty.test.ts | 49 +++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca8ece1cf..de08c5bf11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes +- **huggingface**: validate API tokens via the `whoami-v2` endpoint as a pure auth probe so fine-grained Inference-Provider tokens (valid even when model/task endpoints reject them) are no longer falsely marked invalid; only 401/403 means an invalid key, other non-OK statuses surface as transient upstream errors. (thanks @Delcado19) + - **sse/kiro**: reject the Anthropic-only `[1m]` context-1m suffix in `buildKiroPayload` before it reaches AWS Bedrock — Kiro is Bedrock-backed and cannot honor the beta, so a forwarded `kr/*[1m]` model id was malformed upstream; callers now get a clear error pointing them at a direct-Anthropic provider for 1M-context routing (thanks @Delcado19). - **Engine Combos editor: saving a pipeline no longer fails silently with HTTP 400 (#4955).** The diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index eaf6062478..091cc51aa6 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -95,6 +95,7 @@ import { validateOpenAILikeProvider, validateCommandCodeProvider, validateGeminiLikeProvider, + validateHuggingFaceProvider, validateOpenAICompatibleProvider, } from "./validation/openaiFormat"; import { @@ -280,6 +281,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi } }, "command-code": validateCommandCodeProvider, + huggingface: validateHuggingFaceProvider, deepgram: validateDeepgramProvider, assemblyai: validateAssemblyAIProvider, "fal-ai": ({ apiKey, providerSpecificData }: any) => diff --git a/src/lib/providers/validation/openaiFormat.ts b/src/lib/providers/validation/openaiFormat.ts index b7e63cf317..a2813cf6b3 100644 --- a/src/lib/providers/validation/openaiFormat.ts +++ b/src/lib/providers/validation/openaiFormat.ts @@ -227,6 +227,35 @@ export async function validateCommandCodeProvider({ apiKey, providerSpecificData } +// HuggingFace fine-grained Inference-Provider tokens are valid even when +// model/task endpoints reject them, so the generic OpenAI-like probe against +// router.huggingface.co/v1/models falsely marks them invalid. Validate the +// token strictly as an auth check via the whoami-v2 endpoint instead: only +// 401/403 means the token is invalid; any other non-OK status is a transient +// upstream failure, NOT an invalid key. +export async function validateHuggingFaceProvider({ apiKey }: any) { + try { + const response = await validationRead("https://huggingface.co/api/whoami-v2", { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}` }, + }); + + if (response.ok) { + return { valid: true, error: null, method: "huggingface_whoami" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + // Non-auth, non-OK status — surface as a transient upstream failure rather + // than declaring the (potentially valid) fine-grained token invalid. + return { valid: false, error: `HuggingFace token check returned ${response.status}` }; + } catch (error: unknown) { + return toValidationErrorResult(error); + } +} + export async function validateGeminiLikeProvider({ apiKey, baseUrl, diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index 51187247ea..b7fe234906 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -2749,3 +2749,52 @@ test("isSecurityBlockError: a URL-guard block remains a security block", () => { }); assert.equal(isSecurityBlockError(guardBlock), true); }); + +// ─── huggingface validator (whoami-v2 auth probe) ──────────────────────────── +// Fine-grained HF Inference-Provider tokens are valid even when model/task +// endpoints reject them. The validator must probe whoami-v2 as a pure auth +// check: only 401/403 is invalid; any other non-OK status is transient. + +test("huggingface validator accepts a token whoami-v2 recognizes", async () => { + const calls: { url: string; headers: Record }[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), headers: toPlainHeaders(init.headers) }); + return new Response(JSON.stringify({ name: "hf-user", auth: { type: "access_token" } }), { + status: 200, + }); + }; + + const result = await validateProviderApiKey({ provider: "huggingface", apiKey: "hf_validtoken" }); + + assert.equal(result.valid, true); + assert.equal(result.error, null); + assert.equal(result.method, "huggingface_whoami"); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://huggingface.co/api/whoami-v2"); + assert.equal(calls[0].headers.Authorization, "Bearer hf_validtoken"); +}); + +test("huggingface validator treats 401/403 as an invalid token", async () => { + globalThis.fetch = async () => new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + const unauthorized = await validateProviderApiKey({ provider: "huggingface", apiKey: "hf_bad" }); + assert.equal(unauthorized.valid, false); + assert.equal(unauthorized.error, "Invalid API key"); + + globalThis.fetch = async () => new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 }); + const forbidden = await validateProviderApiKey({ provider: "huggingface", apiKey: "hf_bad" }); + assert.equal(forbidden.valid, false); + assert.equal(forbidden.error, "Invalid API key"); +}); + +test("huggingface validator does NOT mark a fine-grained token invalid on a non-auth status", async () => { + // This is the false-negative the port fixes: a 503/404 from a model/task + // probe used to read as "invalid key". whoami-v2 returning a non-auth, + // non-OK status must surface as a transient error, never "Invalid API key". + globalThis.fetch = async () => new Response("upstream down", { status: 503 }); + + const result = await validateProviderApiKey({ provider: "huggingface", apiKey: "hf_finegrained" }); + + assert.equal(result.valid, false); + assert.notEqual(result.error, "Invalid API key"); + assert.match(result.error || "", /HuggingFace token check returned 503/); +});