Skip to content

feat: UI to reset HF token #7988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions invokeai/app/api/routers/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,12 @@ def set_token(cls, token: str) -> HFTokenStatus:
huggingface_hub.login(token=token, add_to_git_credential=False)
return cls.get_status()

@classmethod
def reset_token(cls) -> HFTokenStatus:
with SuppressOutput(), contextlib.suppress(Exception):
huggingface_hub.logout()
return cls.get_status()


@model_manager_router.get("/hf_login", operation_id="get_hf_login_status", response_model=HFTokenStatus)
async def get_hf_login_status() -> HFTokenStatus:
Expand All @@ -915,3 +921,8 @@ async def do_hf_login(
ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")

return token_status


@model_manager_router.delete("/hf_login", operation_id="reset_hf_token", response_model=HFTokenStatus)
async def reset_hf_token() -> HFTokenStatus:
return HFTokenHelper.reset_token()
1 change: 1 addition & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@
"hfTokenUnableToVerify": "Unable to Verify HF Token",
"hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.",
"hfTokenSaved": "HF Token Saved",
"hfTokenReset": "HF Token Reset",
"urlUnauthorizedErrorMessage": "You may need to configure an API token to access this model.",
"urlUnauthorizedErrorMessage2": "Learn how here.",
"imageEncoderModelId": "Image Encoder Model ID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,63 +12,45 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery, useSetHFTokenMutation } from 'services/api/endpoints/models';
import { UNAUTHORIZED_TOAST_ID } from 'services/events/onModelInstallError';
import {
useGetHFTokenStatusQuery,
useResetHFTokenMutation,
useSetHFTokenMutation,
} from 'services/api/endpoints/models';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';

export const HFToken = () => {
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const [token, setToken] = useState('');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
const [trigger, { isLoading, isUninitialized }] = useSetHFTokenMutation();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
}, []);
const onClick = useCallback(() => {
trigger({ token })
.unwrap()
.then((res) => {
if (res === 'valid') {
setToken('');
toast({
id: UNAUTHORIZED_TOAST_ID,
title: t('modelManager.hfTokenSaved'),
status: 'success',
duration: 3000,
});
}
});
}, [t, token, trigger]);

const error = useMemo(() => {
if (!currentData || isUninitialized || isLoading) {
return null;
}
if (currentData === 'invalid') {
return t('modelManager.hfTokenInvalidErrorMessage');
}
if (currentData === 'unknown') {
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
switch (currentData) {
case 'invalid':
return t('modelManager.hfTokenInvalidErrorMessage');
case 'unknown':
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
case 'valid':
case undefined:
return null;
default:
assert<Equals<never, typeof currentData>>(false, 'Unexpected HF token status');
}
return null;
}, [currentData, isLoading, isUninitialized, t]);
}, [currentData, t]);

if (!currentData || currentData === 'valid') {
if (!currentData) {
return null;
}

return (
<Flex borderRadius="base" w="full">
<FormControl isInvalid={!isUninitialized && Boolean(error)} orientation="vertical">
<FormControl isInvalid={Boolean(error)} orientation="vertical">
<FormLabel>{t('modelManager.hfTokenLabel')}</FormLabel>
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={token} onChange={onChange} />
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
{t('common.save')}
</Button>
</Flex>
{error && <SetHFTokenInput />}
{!error && <ResetHFTokenButton />}
<FormHelperText>
<ExternalLink label={t('modelManager.hfTokenHelperText')} href="https://huggingface.co/settings/tokens" />
</FormHelperText>
Expand All @@ -77,3 +59,73 @@ export const HFToken = () => {
</Flex>
);
};

const PLACEHOLDER_TOKEN = Array.from({ length: 37 }, () => 'a').join('');

const ResetHFTokenButton = memo(() => {
const { t } = useTranslation();
const [resetHFToken, { isLoading }] = useResetHFTokenMutation();

const onClick = useCallback(() => {
resetHFToken()
.unwrap()
.then(() => {
toast({
title: t('modelManager.hfTokenReset'),
status: 'info',
});
});
}, [resetHFToken, t]);

return (
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={PLACEHOLDER_TOKEN} isDisabled />
<Button onClick={onClick} size="sm" isLoading={isLoading}>
{t('common.reset')}
</Button>
</Flex>
);
});
ResetHFTokenButton.displayName = 'ResetHFTokenButton';

const SetHFTokenInput = memo(() => {
const { t } = useTranslation();
const [token, setToken] = useState('');
const [trigger, { isLoading }] = useSetHFTokenMutation();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
}, []);
const onClick = useCallback(() => {
trigger({ token })
.unwrap()
.then((res) => {
switch (res) {
case 'valid':
setToken('');
toast({
title: t('modelManager.hfTokenSaved'),
status: 'success',
});
break;
case 'invalid':
case 'unknown':
default:
toast({
title: t('modelManager.hfTokenUnableToVerify'),
status: 'error',
});
break;
}
});
}, [t, token, trigger]);

return (
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={token} onChange={onChange} />
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
{t('common.save')}
</Button>
</Flex>
);
});
SetHFTokenInput.displayName = 'SetHFTokenInput';
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';

import { HFToken } from './HFToken';
import { HuggingFaceResults } from './HuggingFaceResults';
Expand All @@ -16,7 +15,6 @@ export const HuggingFaceForm = memo(() => {
const [errorMessage, setErrorMessage] = useState('');
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);

const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery();
const [installModel] = useInstallModel();
Expand Down Expand Up @@ -68,7 +66,7 @@ export const HuggingFaceForm = memo(() => {
<FormHelperText>{t('modelManager.huggingFaceHelper')}</FormHelperText>
{!!errorMessage.length && <FormErrorMessage>{errorMessage}</FormErrorMessage>}
</FormControl>
{currentData !== 'valid' && <HFToken />}
{isHFTokenEnabled && <HFToken />}
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
</Flex>
);
Expand Down
18 changes: 12 additions & 6 deletions invokeai/frontend/web/src/services/api/endpoints/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import queryString from 'query-string';
import type { operations, paths } from 'services/api/schema';
import {
type AnyModelConfig,
type GetHFTokenStatusResponse,
isNonRefinerMainModelConfig,
type SetHFTokenArg,
type SetHFTokenResponse,
import type {
AnyModelConfig,
GetHFTokenStatusResponse,
ResetHFTokenResponse,
SetHFTokenArg,
SetHFTokenResponse,
} from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import type { Param0 } from 'tsafe';

import type { ApiTagDescription } from '..';
Expand Down Expand Up @@ -293,6 +294,10 @@ export const modelsApi = api.injectEndpoints({
}
},
}),
resetHFToken: build.mutation<ResetHFTokenResponse, void>({
query: () => ({ url: buildModelsUrl('hf_login'), method: 'DELETE' }),
invalidatesTags: ['HFTokenStatus'],
}),
emptyModelCache: build.mutation<void, void>({
query: () => ({ url: buildModelsUrl('empty_model_cache'), method: 'POST' }),
}),
Expand All @@ -316,6 +321,7 @@ export const {
useGetStarterModelsQuery,
useGetHFTokenStatusQuery,
useSetHFTokenMutation,
useResetHFTokenMutation,
useEmptyModelCacheMutation,
} = modelsApi;

Expand Down
23 changes: 22 additions & 1 deletion invokeai/frontend/web/src/services/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ export type paths = {
put?: never;
/** Do Hf Login */
post: operations["do_hf_login"];
delete?: never;
/** Reset Hf Token */
delete: operations["reset_hf_token"];
options?: never;
head?: never;
patch?: never;
Expand Down Expand Up @@ -22502,6 +22503,26 @@ export interface operations {
};
};
};
reset_hf_token: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HFTokenStatus"];
};
};
};
};
list_downloads: {
parameters: {
query?: never;
Expand Down
3 changes: 3 additions & 0 deletions invokeai/frontend/web/src/services/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ export type GetHFTokenStatusResponse =
export type SetHFTokenResponse = NonNullable<
paths['/api/v2/models/hf_login']['post']['responses']['200']['content']['application/json']
>;
export type ResetHFTokenResponse = NonNullable<
paths['/api/v2/models/hf_login']['delete']['responses']['200']['content']['application/json']
>;
export type SetHFTokenArg = NonNullable<
paths['/api/v2/models/hf_login']['post']['requestBody']['content']['application/json']
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { assert } from 'tsafe';
const log = logger('events');
const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select();

export const UNAUTHORIZED_TOAST_ID = getPrefixedId('unauthorized-toast');
const UNAUTHORIZED_TOAST_ID = getPrefixedId('unauthorized-toast');
const FORBIDDEN_TOAST_ID = getPrefixedId('forbidden-toast');

const getHFTokenStatusToastTitle = (token_status: S['HFTokenStatus']) => {
Expand Down