Studio: fall back to anonymous HF browsing on a malformed token (#6605)

* Studio: fall back to anonymous HF browsing on a malformed token

The Discover/Recommended feeds call the Hugging Face JS client
(`listModels`/`listDatasets`) directly from the browser. That client
throws `Your access token must start with 'hf_'` when handed a non-empty
token that isn't a well-formed HF token, instead of falling back to
anonymous access. A single bad value left in the HF token field (e.g. a
placeholder someone typed) therefore takes down the entire discovery feed
even though it works fine with no token at all.

Add `hfApiToken()` to the HF token store, which returns the token only
when it looks like a real `hf_...` credential and `undefined` otherwise,
and route hub-page's four HF call sites through it. Malformed tokens now
degrade to anonymous public browsing rather than erroring. The raw token
is still stored and shown in the settings field unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Studio: trim hfApiToken comments

Collapse the 11-line JSDoc to a 2-line note and drop the redundant
call-site comment in hub-page. AST signature check confirms code is
unchanged (comments only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Han 2026-06-23 05:16:10 -07:00 committed by GitHub
commit 71e6b1874a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 5 deletions

View file

@ -14,7 +14,10 @@ import { useHubInfiniteScroll } from "@/features/hub/hooks/use-hub-infinite-scro
import { ggufVariantsMatch, modelIdsMatch } from "@/features/hub/lib/model-identity";
import { cn } from "@/lib/utils";
import { usePlatformStore } from "@/config/env";
import { useHfTokenStore } from "@/features/hub/stores/hf-token-store";
import {
hfApiToken,
useHfTokenStore,
} from "@/features/hub/stores/hf-token-store";
import {
getInferenceStatus,
isExternalModelId,
@ -551,6 +554,7 @@ export function ModelsPage() {
const deferredDebouncedQuery = useDeferredValue(debouncedQuery);
const hfToken = useHfTokenStore((s) => s.token);
const debouncedHfToken = useDebouncedValue(hfToken, 500);
const apiHfToken = hfApiToken(debouncedHfToken);
const deferredFormatFilter = useDeferredValue(formatFilter);
const deferredCapabilityFilter = useDeferredValue(capabilityFilter);
@ -605,7 +609,7 @@ export function ModelsPage() {
handleRetrySearch,
} = useDiscoverSearch({
debouncedQuery,
accessToken: debouncedHfToken || undefined,
accessToken: apiHfToken,
isDiscoverTab,
isDatasetMode,
sortBy: effectiveSort,
@ -619,7 +623,7 @@ export function ModelsPage() {
channelId: isChannelListMode ? activeChannelId : null,
results,
isLoading,
accessToken: debouncedHfToken || undefined,
accessToken: apiHfToken,
});
const {
@ -702,7 +706,7 @@ export function ModelsPage() {
const listRows = filteredDiscoverRows;
const hubFeed = useHubFeed({
accessToken: debouncedHfToken || undefined,
accessToken: apiHfToken,
online,
enabled: isFeedMode,
deviceType,
@ -908,7 +912,7 @@ export function ModelsPage() {
filteredCachedRows,
filteredLocalRows,
results: selectionResults,
accessToken: debouncedHfToken || undefined,
accessToken: apiHfToken,
online,
});

View file

@ -116,3 +116,11 @@ export const useHfTokenStore = create<HfTokenStore>((set) => {
export function getHfToken(): string {
return useHfTokenStore.getState().token;
}
// HF's JS client throws on a non-empty token that isn't `hf_...` instead of
// browsing anonymously, so treat anything malformed as no token.
export function hfApiToken(
token: string | undefined | null,
): string | undefined {
return token && token.startsWith("hf_") ? token : undefined;
}