From d5000de5341eede9b5a5162074e951f8881d2c98 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 6 Aug 2026 09:41:26 +0800 Subject: [PATCH] fix(models-section): defer auto-fetch catalog until base_url is set for self-hosted providers (#17804) --- .../instance-card/models-section/hooks.ts | 59 +++++++++++++------ .../instance-card/models-section/index.tsx | 3 + 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts b/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts index 242d571a1e..62069d6bea 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts @@ -86,8 +86,12 @@ export const buildModelInfo = (items: IProviderModelItem[]): IModelInfo[] => extra: { is_tools: hasToolFeature(m.features), ...(m.extra ?? {}) }, })); -/** Resolved credentials for catalog / verify / batch calls. */ -export type ResolvedCreds = { apiKey: string; baseUrl: string }; +/** Resolved credentials for catalog / verify / batch calls. + * `baseUrl` is `undefined` when the provider's form has no `base_url` + * field (e.g. VolcEngine, Google Cloud) so the auto-fetch gate can + * distinguish "no base_url field" from "base_url field exists but is + * empty". */ +export type ResolvedCreds = { apiKey: string; baseUrl: string | undefined }; // --------------------------------------------------------------------------- // 1. useResolveCreds — resolve api_key / base_url from host form or instance @@ -104,7 +108,7 @@ export function useResolveCreds( const fv = getFormValues?.() ?? {}; return { apiKey: (fv.api_key as string) ?? instance?.api_key ?? '', - baseUrl: (fv.base_url as string) ?? instance?.base_url ?? '', + baseUrl: (fv.base_url as string) ?? instance?.base_url, }; }, [getFormValues, instance]); @@ -121,13 +125,12 @@ interface UseModelsCatalogArgs { hideActions: boolean; resolveCreds: () => ResolvedCreds; instanceModels: IInstanceModel[] | undefined; - /** - * Current api_key value (read from the host form / instance). Used to - * gate the auto-fetch for providers that require an api_key to list - * models (currently only VolcEngine). For other providers the value - * is ignored and the catalog is fetched on mount regardless. - */ + apiKeyValue: string; + + baseUrlValue: string | undefined; + + instanceDetailsLoaded?: boolean; } export function useModelsCatalog({ @@ -137,6 +140,8 @@ export function useModelsCatalog({ resolveCreds, instanceModels, apiKeyValue, + baseUrlValue, + instanceDetailsLoaded, }: UseModelsCatalogArgs) { const { listProviderModels } = useListProviderModels(); const [catalog, setCatalog] = useState([]); @@ -206,7 +211,7 @@ export function useModelsCatalog({ try { const ret = await listProviderModels({ provider_name: providerName, - api_key: apiKey, + api_key: apiKey as any, base_url: baseUrl, }); if (ret?.code === 0) { @@ -225,24 +230,44 @@ export function useModelsCatalog({ // Auto-fetch the provider's available-models catalog when this section // mounts (effectively "when the card is expanded"). For VolcEngine we // wait until an api_key is available (typed in the draft form or loaded - // from instance details); for every other provider we fetch on mount - // regardless of draft / saved state - the catalog endpoint does not - // require credentials and the user expects to see the model list as - // soon as they open the "Add instance" page. + // from instance details). For providers whose form includes a + // `base_url` field (e.g. Ollama, Xinference, LocalAI) we defer until a + // non-empty URL is entered - their list-models endpoint needs the URL + // to know which server to query. For every other provider we fetch on + // mount regardless. + // + // The credential check is performed INSIDE the effect (not in the deps) + // because for saved cards the form is reset by + // `useFormResetOnDetailsLoad` in a parent effect that runs BEFORE this + // one. Reading `resolveCreds()` at effect time picks up the freshly + // reset `base_url` / `api_key` values, whereas reading them during + // render would see the stale (pre-reset) values and defer forever. const requiresApiKey = providerName === LLMFactory.VolcEngine; - const credsReady = !requiresApiKey || !!apiKeyValue; const hasAutoFetchedRef = useRef(false); useEffect(() => { if (hasAutoFetchedRef.current) return; if (hideActions) return; if (!providerName) return; - if (!credsReady) return; + + const creds = resolveCreds(); + const hasBaseUrlField = creds.baseUrl !== undefined; + const ready = + (!requiresApiKey || !!creds.apiKey) && + (!hasBaseUrlField || !!creds.baseUrl); + if (!ready) return; hasAutoFetchedRef.current = true; handleListModels(); // oxlint-disable-next-line react/exhaustive-deps - }, [providerName, instanceName, hideActions, credsReady]); + }, [ + providerName, + instanceName, + hideActions, + apiKeyValue, + baseUrlValue, + instanceDetailsLoaded, + ]); // Mark `hasFetched` true once the per-instance query resolves — even if // it returned an empty array — so `hideIfEmpty` can safely take effect. diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx index 98bcec3a4b..e1e59d2b11 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx @@ -59,6 +59,7 @@ export function ModelsSection(props: ModelsSectionProps) { hideIfEmpty = false, getFormValues, verifyTransform, + instanceDetailsLoaded, onBlurSuppressChange, onInstanceModelsChange, onInstanceModelsEdited, @@ -98,6 +99,8 @@ export function ModelsSection(props: ModelsSectionProps) { resolveCreds, instanceModels, apiKeyValue: currentCreds.apiKey, + baseUrlValue: currentCreds.baseUrl, + instanceDetailsLoaded, }); // 3a. Draft-only: locally-tracked "added models" list.