Skip to content

Commit 67747a9

Browse files
authored
fix: unify provider retrieval into swr hook (#8609)
1 parent edfc51b commit 67747a9

File tree

6 files changed

+128
-110
lines changed

6 files changed

+128
-110
lines changed

web/src/components/chat/ProviderContext.tsx

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"use client";
2-
import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
2+
import {
3+
WellKnownLLMProviderDescriptor,
4+
LLMProviderDescriptor,
5+
} from "@/app/admin/configuration/llm/interfaces";
36
import React, {
47
createContext,
58
useContext,
@@ -8,57 +11,101 @@ import React, {
811
useCallback,
912
} from "react";
1013
import { useUser } from "@/providers/UserProvider";
11-
import { useRouter } from "next/navigation";
12-
import { checkLlmProvider } from "../initialSetup/welcome/lib";
14+
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
15+
import { useLLMProviderOptions } from "@/lib/hooks/useLLMProviderOptions";
16+
import { testDefaultProvider as testDefaultProviderSvc } from "@/lib/llm/svc";
1317

1418
interface ProviderContextType {
1519
shouldShowConfigurationNeeded: boolean;
1620
providerOptions: WellKnownLLMProviderDescriptor[];
17-
refreshProviderInfo: () => Promise<void>; // Add this line
21+
refreshProviderInfo: () => Promise<void>;
22+
// Expose configured provider instances for components that need it (e.g., onboarding)
23+
llmProviders: LLMProviderDescriptor[] | undefined;
24+
isLoadingProviders: boolean;
25+
hasProviders: boolean;
1826
}
1927

2028
const ProviderContext = createContext<ProviderContextType | undefined>(
2129
undefined
2230
);
2331

32+
const DEFAULT_LLM_PROVIDER_TEST_COMPLETE_KEY = "defaultLlmProviderTestComplete";
33+
34+
function checkDefaultLLMProviderTestComplete() {
35+
if (typeof window === "undefined") return true;
36+
return (
37+
localStorage.getItem(DEFAULT_LLM_PROVIDER_TEST_COMPLETE_KEY) === "true"
38+
);
39+
}
40+
41+
function setDefaultLLMProviderTestComplete() {
42+
if (typeof window === "undefined") return;
43+
localStorage.setItem(DEFAULT_LLM_PROVIDER_TEST_COMPLETE_KEY, "true");
44+
}
45+
2446
export function ProviderContextProvider({
2547
children,
2648
}: {
2749
children: React.ReactNode;
2850
}) {
2951
const { user } = useUser();
30-
const router = useRouter();
3152

32-
const [validProviderExists, setValidProviderExists] = useState<boolean>(true);
33-
const [providerOptions, setProviderOptions] = useState<
34-
WellKnownLLMProviderDescriptor[]
35-
>([]);
53+
// Use SWR hooks instead of raw fetch
54+
const {
55+
llmProviders,
56+
isLoading: isLoadingProviders,
57+
refetch: refetchProviders,
58+
} = useLLMProviders();
59+
const { llmProviderOptions: providerOptions, refetch: refetchOptions } =
60+
useLLMProviderOptions();
3661

37-
const fetchProviderInfo = useCallback(async () => {
38-
const { providers, options, defaultCheckSuccessful } =
39-
await checkLlmProvider(user);
62+
const [defaultCheckSuccessful, setDefaultCheckSuccessful] =
63+
useState<boolean>(true);
4064

41-
setValidProviderExists(providers.length > 0 && defaultCheckSuccessful);
42-
setProviderOptions(options);
43-
}, [user, setValidProviderExists, setProviderOptions]);
65+
// Test the default provider - only runs if test hasn't passed yet
66+
const testDefaultProvider = useCallback(async () => {
67+
const shouldCheck =
68+
!checkDefaultLLMProviderTestComplete() &&
69+
(!user || user.role === "admin");
4470

71+
if (shouldCheck) {
72+
const success = await testDefaultProviderSvc();
73+
setDefaultCheckSuccessful(success);
74+
if (success) {
75+
setDefaultLLMProviderTestComplete();
76+
}
77+
}
78+
}, [user]);
79+
80+
// Test default provider on mount
4581
useEffect(() => {
46-
fetchProviderInfo();
47-
}, [router, user, fetchProviderInfo]);
82+
testDefaultProvider();
83+
}, [testDefaultProvider]);
84+
85+
const hasProviders = (llmProviders?.length ?? 0) > 0;
86+
const validProviderExists = hasProviders && defaultCheckSuccessful;
4887

4988
const shouldShowConfigurationNeeded =
50-
!validProviderExists && providerOptions.length > 0;
89+
!validProviderExists && (providerOptions?.length ?? 0) > 0;
5190

52-
const refreshProviderInfo = async () => {
53-
await fetchProviderInfo();
54-
};
91+
const refreshProviderInfo = useCallback(async () => {
92+
// Refetch provider lists and re-test default provider if needed
93+
await Promise.all([
94+
refetchProviders(),
95+
refetchOptions(),
96+
testDefaultProvider(),
97+
]);
98+
}, [refetchProviders, refetchOptions, testDefaultProvider]);
5599

56100
return (
57101
<ProviderContext.Provider
58102
value={{
59103
shouldShowConfigurationNeeded,
60-
providerOptions,
61-
refreshProviderInfo, // Add this line
104+
providerOptions: providerOptions ?? [],
105+
refreshProviderInfo,
106+
llmProviders,
107+
isLoadingProviders,
108+
hasProviders,
62109
}}
63110
>
64111
{children}

web/src/components/initialSetup/welcome/constants.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

web/src/components/initialSetup/welcome/lib.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import useSWR from "swr";
2+
import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
3+
import { errorHandlingFetcher } from "@/lib/fetcher";
4+
5+
export function useLLMProviderOptions() {
6+
const { data, error, mutate } = useSWR<
7+
WellKnownLLMProviderDescriptor[] | undefined
8+
>("/api/admin/llm/built-in/options", errorHandlingFetcher, {
9+
revalidateOnFocus: false,
10+
dedupingInterval: 60000, // Dedupe requests within 1 minute
11+
});
12+
13+
return {
14+
llmProviderOptions: data,
15+
isLoading: !error && !data,
16+
error,
17+
refetch: mutate,
18+
};
19+
}

web/src/lib/llm/svc.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* LLM action functions for mutations.
3+
*
4+
* These are async functions for one-off actions that don't need SWR caching.
5+
*
6+
* Endpoints:
7+
* - /api/admin/llm/test/default - Test the default LLM provider connection
8+
*/
9+
10+
/**
11+
* Test the default LLM provider.
12+
* Returns true if the default provider is configured and working, false otherwise.
13+
*/
14+
export async function testDefaultProvider(): Promise<boolean> {
15+
try {
16+
const response = await fetch("/api/admin/llm/test/default", {
17+
method: "POST",
18+
});
19+
return response?.ok || false;
20+
} catch {
21+
return false;
22+
}
23+
}

web/src/refresh-components/onboarding/useOnboardingState.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useReducer, useCallback, useState, useEffect, useRef } from "react";
1+
import { useReducer, useCallback, useEffect, useRef } from "react";
22
import { onboardingReducer, initialState } from "./reducer";
33
import {
44
OnboardingActions,
@@ -12,6 +12,7 @@ import { updateUserPersonalization } from "@/lib/userSettings";
1212
import { useUser } from "@/providers/UserProvider";
1313
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
1414
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
15+
import { useProviderStatus } from "@/components/chat/ProviderContext";
1516

1617
export function useOnboardingState(liveAssistant?: MinimalPersonaSnapshot): {
1718
state: OnboardingState;
@@ -21,43 +22,28 @@ export function useOnboardingState(liveAssistant?: MinimalPersonaSnapshot): {
2122
} {
2223
const [state, dispatch] = useReducer(onboardingReducer, initialState);
2324
const { user, refreshUser } = useUser();
24-
// Use the SWR hook for LLM providers - no persona ID for the general providers list
25+
26+
// Get provider data from ProviderContext instead of duplicating the call
2527
const {
2628
llmProviders,
27-
isLoading: isLoadingProviders,
28-
refetch: refreshLlmProviders,
29-
} = useLLMProviders();
29+
isLoadingProviders,
30+
hasProviders: hasLlmProviders,
31+
providerOptions,
32+
refreshProviderInfo,
33+
} = useProviderStatus();
34+
35+
// Only fetch persona-specific providers (different endpoint)
3036
const { refetch: refreshPersonaProviders } = useLLMProviders(
3137
liveAssistant?.id
3238
);
33-
const hasLlmProviders = (llmProviders?.length ?? 0) > 0;
39+
3440
const userName = user?.personalization?.name;
35-
const [llmDescriptors, setLlmDescriptors] = useState<
36-
WellKnownLLMProviderDescriptor[]
37-
>([]);
41+
const llmDescriptors = providerOptions;
42+
3843
const nameUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
3944
null
4045
);
4146

42-
useEffect(() => {
43-
refreshLlmProviders();
44-
const fetchLlmDescriptors = async () => {
45-
try {
46-
const response = await fetch("/api/admin/llm/built-in/options");
47-
if (!response.ok) {
48-
setLlmDescriptors([]);
49-
return;
50-
}
51-
const data = await response.json();
52-
setLlmDescriptors(Array.isArray(data) ? data : []);
53-
} catch (_e) {
54-
setLlmDescriptors([]);
55-
}
56-
};
57-
58-
fetchLlmDescriptors();
59-
}, []);
60-
6147
// Navigate to the earliest incomplete step in the onboarding flow.
6248
// Step order: Welcome -> Name -> LlmSetup -> Complete
6349
// We check steps in order and stop at the first incomplete one.
@@ -134,13 +120,13 @@ export function useOnboardingState(liveAssistant?: MinimalPersonaSnapshot): {
134120
}
135121

136122
if (state.currentStep === OnboardingStep.LlmSetup) {
137-
refreshLlmProviders();
123+
refreshProviderInfo();
138124
if (liveAssistant) {
139125
refreshPersonaProviders();
140126
}
141127
}
142128
dispatch({ type: OnboardingActionType.NEXT_STEP });
143-
}, [state, refreshLlmProviders, llmProviders, refreshPersonaProviders]);
129+
}, [state, refreshProviderInfo, llmProviders, refreshPersonaProviders]);
144130

145131
const prevStep = useCallback(() => {
146132
dispatch({ type: OnboardingActionType.PREV_STEP });

0 commit comments

Comments
 (0)