mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
fix(db): validate HuggingFace tokens via whoami-v2 auth probe (#4819)
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.
This commit is contained in:
committed by
GitHub
parent
7193b5fa13
commit
86e487bdb3
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string> }[] = [];
|
||||
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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user