From 331eb0d439217cf122ec8d4831808c7c28129294 Mon Sep 17 00:00:00 2001 From: mushuiyu886 Date: Sun, 19 Jul 2026 04:31:24 +0800 Subject: [PATCH] fix(huggingface): discover models when HTTP proxy is required (#110924) --- extensions/huggingface/models.proxy.test.ts | 49 +++++++++++++++++++++ extensions/huggingface/models.ts | 27 +++++++----- 2 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 extensions/huggingface/models.proxy.test.ts diff --git a/extensions/huggingface/models.proxy.test.ts b/extensions/huggingface/models.proxy.test.ts new file mode 100644 index 000000000000..03ecd5a7a9c4 --- /dev/null +++ b/extensions/huggingface/models.proxy.test.ts @@ -0,0 +1,49 @@ +// Hugging Face proxy tests cover the live model discovery transport policy. +import { afterEach, describe, expect, it, vi } from "vitest"; + +const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => ({ + ...(await importOriginal()), + fetchWithSsrFGuard: fetchWithSsrFGuardMock, +})); + +import { discoverHuggingfaceModels } from "./models.js"; + +const ORIGINAL_VITEST = process.env.VITEST; +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; + +function restoreEnv(key: "VITEST" | "NODE_ENV", value: string | undefined) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + +afterEach(() => { + restoreEnv("VITEST", ORIGINAL_VITEST); + restoreEnv("NODE_ENV", ORIGINAL_NODE_ENV); + fetchWithSsrFGuardMock.mockReset(); + vi.restoreAllMocks(); +}); + +describe("Hugging Face model discovery proxy policy", () => { + it("allows the guarded official catalog request to use an eligible HTTP proxy", async () => { + process.env.VITEST = "false"; + process.env.NODE_ENV = "development"; + const response = new Response("unavailable", { status: 503 }); + const release = vi.fn(async () => undefined); + fetchWithSsrFGuardMock.mockResolvedValue({ response, release }); + + await discoverHuggingfaceModels("hf_test_token"); + + expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith( + expect.objectContaining({ + mode: "trusted_env_proxy", + url: "https://router.huggingface.co/v1/models", + }), + ); + expect(release).toHaveBeenCalledTimes(1); + }); +}); diff --git a/extensions/huggingface/models.ts b/extensions/huggingface/models.ts index 6543b9915562..0c6afa7031a3 100644 --- a/extensions/huggingface/models.ts +++ b/extensions/huggingface/models.ts @@ -1,4 +1,5 @@ // Huggingface plugin module implements models behavior. +import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime"; import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types"; @@ -139,19 +140,21 @@ export async function discoverHuggingfaceModels( try { const requestTimeoutMs = resolveTimerTimeoutMs(timeoutMs, HUGGINGFACE_DISCOVERY_TIMEOUT_MS); - const { response, release } = await fetchWithSsrFGuard({ - url: `${HUGGINGFACE_BASE_URL}/models`, - init: { - signal: AbortSignal.timeout(requestTimeoutMs), - headers: { - Authorization: `Bearer ${trimmedKey}`, - "Content-Type": "application/json", + const { response, release } = await fetchWithSsrFGuard( + withTrustedEnvProxyGuardedFetchMode({ + url: `${HUGGINGFACE_BASE_URL}/models`, + init: { + signal: AbortSignal.timeout(requestTimeoutMs), + headers: { + Authorization: `Bearer ${trimmedKey}`, + "Content-Type": "application/json", + }, }, - }, - timeoutMs: requestTimeoutMs, - policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(HUGGINGFACE_BASE_URL), - auditContext: "huggingface-model-discovery", - }); + timeoutMs: requestTimeoutMs, + policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(HUGGINGFACE_BASE_URL), + auditContext: "huggingface-model-discovery", + }), + ); try { if (!response.ok) { await response.body?.cancel().catch(() => undefined);