// ======================================== // useApiSettings Hook // ======================================== // TanStack Query hooks for API Settings management import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useFormatMessage } from '../hooks/useLocale'; import { useNotifications } from '../hooks/useNotifications'; import { sanitizeErrorMessage } from '../utils/errorSanitizer'; import { fetchProviders, createProvider, updateProvider, deleteProvider, testProvider, testProviderKey, getProviderHealthStatus, triggerProviderHealthCheck, fetchEndpoints, createEndpoint, updateEndpoint, deleteEndpoint, fetchCacheStats, clearCache, updateCacheSettings, fetchModelPools, fetchModelPool, createModelPool, updateModelPool, deleteModelPool, getAvailableModelsForPool, discoverModelsForPool, fetchApiConfig, syncApiConfig, previewYamlConfig, checkCcwLitellmStatus, installCcwLitellm, uninstallCcwLitellm, fetchCliSettings, createCliSettings, updateCliSettings, deleteCliSettings, toggleCliSettingsEnabled, type ProviderCredential, type CustomEndpoint, type CacheStats, type GlobalCacheSettings, type ModelPoolConfig, type ModelPoolType, type DiscoveredProvider, type CliSettingsEndpoint, type SaveCliSettingsRequest, } from '../lib/api'; // Query key factory export const apiSettingsKeys = { all: ['apiSettings'] as const, providers: () => [...apiSettingsKeys.all, 'providers'] as const, provider: (id: string) => [...apiSettingsKeys.providers(), id] as const, endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const, endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const, cache: () => [...apiSettingsKeys.all, 'cache'] as const, modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const, modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const, ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const, cliSettings: () => [...apiSettingsKeys.all, 'cliSettings'] as const, cliSetting: (id: string) => [...apiSettingsKeys.cliSettings(), id] as const, }; const STALE_TIME = 2 * 60 * 1000; // ======================================== // Provider Hooks // ======================================== export interface UseProvidersOptions { staleTime?: number; enabled?: boolean; } export interface UseProvidersReturn { providers: ProviderCredential[]; totalCount: number; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; invalidate: () => Promise; } export function useProviders(options: UseProvidersOptions = {}): UseProvidersReturn { const { staleTime = STALE_TIME, enabled = true } = options; const queryClient = useQueryClient(); const query = useQuery({ queryKey: apiSettingsKeys.providers(), queryFn: fetchProviders, staleTime, enabled, retry: 2, }); const providers = query.data?.providers ?? []; const refetch = async () => { await query.refetch(); }; const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() }); }; return { providers, totalCount: providers.length, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, invalidate, }; } export function useCreateProvider() { const queryClient = useQueryClient(); const formatMessage = useFormatMessage(); const { success, info, error: errorToast } = useNotifications(); const mutation = useMutation({ mutationFn: (provider: Omit) => createProvider(provider), onMutate: () => { info( formatMessage({ id: 'status.creating' }), formatMessage({ id: 'common.feedback.providerCreate.success' }) ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() }); success( formatMessage({ id: 'common.success' }), formatMessage({ id: 'common.feedback.providerCreate.success' }) ); }, onError: (err) => { const sanitized = sanitizeErrorMessage(err, 'providerCreate'); const message = formatMessage({ id: sanitized.messageKey }); const title = formatMessage({ id: 'common.error' }); errorToast(title, message); }, }); return { createProvider: mutation.mutateAsync, isCreating: mutation.isPending, error: mutation.error, }; } export function useUpdateProvider() { const queryClient = useQueryClient(); const formatMessage = useFormatMessage(); const { success, info, error: errorToast } = useNotifications(); const mutation = useMutation({ mutationFn: ({ providerId, updates }: { providerId: string; updates: Partial> }) => updateProvider(providerId, updates), onMutate: () => { info( formatMessage({ id: 'status.inProgress' }), formatMessage({ id: 'common.feedback.providerUpdate.success' }) ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() }); success( formatMessage({ id: 'common.success' }), formatMessage({ id: 'common.feedback.providerUpdate.success' }) ); }, onError: (err) => { const sanitized = sanitizeErrorMessage(err, 'providerUpdate'); const message = formatMessage({ id: sanitized.messageKey }); const title = formatMessage({ id: 'common.error' }); errorToast(title, message); }, }); return { updateProvider: (providerId: string, updates: Partial>) => mutation.mutateAsync({ providerId, updates }), isUpdating: mutation.isPending, error: mutation.error, }; } export function useDeleteProvider() { const queryClient = useQueryClient(); const formatMessage = useFormatMessage(); const { success, info, error: errorToast } = useNotifications(); const mutation = useMutation({ mutationFn: (providerId: string) => deleteProvider(providerId), onMutate: () => { info( formatMessage({ id: 'status.deleting' }), formatMessage({ id: 'common.feedback.providerDelete.success' }) ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() }); success( formatMessage({ id: 'common.success' }), formatMessage({ id: 'common.feedback.providerDelete.success' }) ); }, onError: (err) => { const sanitized = sanitizeErrorMessage(err, 'providerDelete'); const message = formatMessage({ id: sanitized.messageKey }); const title = formatMessage({ id: 'common.error' }); errorToast(title, message); }, }); return { deleteProvider: mutation.mutateAsync, isDeleting: mutation.isPending, error: mutation.error, }; } export function useTestProvider() { const mutation = useMutation({ mutationFn: (providerId: string) => testProvider(providerId), }); return { testProvider: mutation.mutateAsync, isTesting: mutation.isPending, error: mutation.error, }; } export function useTestProviderKey() { const mutation = useMutation({ mutationFn: ({ providerId, keyId }: { providerId: string; keyId: string }) => testProviderKey(providerId, keyId), }); return { testProviderKey: mutation.mutateAsync, isTesting: mutation.isPending, error: mutation.error, }; } export function useProviderHealthStatus(providerId: string) { return useQuery({ queryKey: [...apiSettingsKeys.provider(providerId), 'health'], queryFn: () => getProviderHealthStatus(providerId), enabled: !!providerId, staleTime: 30000, // 30 seconds refetchInterval: 60000, // Refetch every minute }); } export function useTriggerProviderHealthCheck() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (providerId: string) => triggerProviderHealthCheck(providerId), onSuccess: (_, providerId) => { queryClient.invalidateQueries({ queryKey: [...apiSettingsKeys.provider(providerId), 'health'] }); }, }); return { triggerHealthCheck: mutation.mutateAsync, isChecking: mutation.isPending, error: mutation.error, }; } // ======================================== // Endpoint Hooks // ======================================== export interface UseEndpointsOptions { staleTime?: number; enabled?: boolean; } export interface UseEndpointsReturn { endpoints: CustomEndpoint[]; totalCount: number; cachedCount: number; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; invalidate: () => Promise; } export function useEndpoints(options: UseEndpointsOptions = {}): UseEndpointsReturn { const { staleTime = STALE_TIME, enabled = true } = options; const queryClient = useQueryClient(); const query = useQuery({ queryKey: apiSettingsKeys.endpoints(), queryFn: fetchEndpoints, staleTime, enabled, retry: 2, }); const endpoints = query.data?.endpoints ?? []; const cachedEndpoints = endpoints.filter((e) => e.cacheStrategy.enabled); const refetch = async () => { await query.refetch(); }; const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() }); }; return { endpoints, totalCount: endpoints.length, cachedCount: cachedEndpoints.length, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, invalidate, }; } export function useCreateEndpoint() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (endpoint: Omit) => createEndpoint(endpoint), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() }); }, }); return { createEndpoint: mutation.mutateAsync, isCreating: mutation.isPending, error: mutation.error, }; } export function useUpdateEndpoint() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ endpointId, updates }: { endpointId: string; updates: Partial> }) => updateEndpoint(endpointId, updates), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() }); }, }); return { updateEndpoint: (endpointId: string, updates: Partial>) => mutation.mutateAsync({ endpointId, updates }), isUpdating: mutation.isPending, error: mutation.error, }; } export function useDeleteEndpoint() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (endpointId: string) => deleteEndpoint(endpointId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() }); }, }); return { deleteEndpoint: mutation.mutateAsync, isDeleting: mutation.isPending, error: mutation.error, }; } // ======================================== // Cache Hooks // ======================================== export interface UseCacheStatsOptions { staleTime?: number; enabled?: boolean; } export interface UseCacheStatsReturn { stats: CacheStats | null; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; } export function useCacheStats(options: UseCacheStatsOptions = {}): UseCacheStatsReturn { const { staleTime = 30000, enabled = true } = options; // 30 seconds stale time for cache stats const query = useQuery({ queryKey: apiSettingsKeys.cache(), queryFn: fetchCacheStats, staleTime, enabled, retry: 2, }); const refetch = async () => { await query.refetch(); }; return { stats: query.data ?? null, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, }; } export function useClearCache() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: () => clearCache(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() }); }, }); return { clearCache: mutation.mutateAsync, isClearing: mutation.isPending, error: mutation.error, }; } export function useUpdateCacheSettings() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>) => updateCacheSettings(settings), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() }); }, }); return { updateCacheSettings: mutation.mutateAsync, isUpdating: mutation.isPending, error: mutation.error, }; } // ======================================== // Model Pool Hooks // ======================================== export interface UseModelPoolsOptions { staleTime?: number; enabled?: boolean; } export interface UseModelPoolsReturn { pools: ModelPoolConfig[]; totalCount: number; enabledCount: number; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; invalidate: () => Promise; } export function useModelPools(options: UseModelPoolsOptions = {}): UseModelPoolsReturn { const { staleTime = STALE_TIME, enabled = true } = options; const queryClient = useQueryClient(); const query = useQuery({ queryKey: apiSettingsKeys.modelPools(), queryFn: fetchModelPools, staleTime, enabled, retry: 2, }); const pools = query.data?.pools ?? []; const enabledPools = pools.filter((p) => p.enabled); const refetch = async () => { await query.refetch(); }; const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() }); }; return { pools, totalCount: pools.length, enabledCount: enabledPools.length, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, invalidate, }; } export function useModelPool(poolId: string) { return useQuery({ queryKey: apiSettingsKeys.modelPool(poolId), queryFn: () => fetchModelPool(poolId), enabled: !!poolId, staleTime: STALE_TIME, retry: 2, }); } export function useCreateModelPool() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (pool: Omit) => createModelPool(pool), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() }); }, }); return { createModelPool: mutation.mutateAsync, isCreating: mutation.isPending, error: mutation.error, }; } export function useUpdateModelPool() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ poolId, updates }: { poolId: string; updates: Partial }) => updateModelPool(poolId, updates), onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() }); queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPool(variables.poolId) }); }, }); return { updateModelPool: (poolId: string, updates: Partial) => mutation.mutateAsync({ poolId, updates }), isUpdating: mutation.isPending, error: mutation.error, }; } export function useDeleteModelPool() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (poolId: string) => deleteModelPool(poolId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() }); }, }); return { deleteModelPool: mutation.mutateAsync, isDeleting: mutation.isPending, error: mutation.error, }; } export function useAvailableModelsForPool(modelType: ModelPoolType) { return useQuery({ queryKey: [...apiSettingsKeys.modelPools(), 'available', modelType], queryFn: () => getAvailableModelsForPool(modelType), enabled: !!modelType, staleTime: STALE_TIME, }); } export function useDiscoverModelsForPool() { const mutation = useMutation({ mutationFn: ({ modelType, targetModel }: { modelType: ModelPoolType; targetModel: string }) => discoverModelsForPool(modelType, targetModel), }); return { discoverModels: (modelType: ModelPoolType, targetModel: string) => mutation.mutateAsync({ modelType, targetModel }), isDiscovering: mutation.isPending, error: mutation.error, data: mutation.data, }; } // ======================================== // Config Hooks // ======================================== export function useApiConfig() { return useQuery({ queryKey: [...apiSettingsKeys.all, 'config'], queryFn: fetchApiConfig, staleTime: STALE_TIME, }); } export function useSyncApiConfig() { return useMutation({ mutationFn: () => syncApiConfig(), }); } export function usePreviewYamlConfig() { return useMutation({ mutationFn: () => previewYamlConfig(), }); } // ======================================== // CCW-LiteLLM Package Hooks // ======================================== export interface UseCcwLitellmStatusOptions { staleTime?: number; enabled?: boolean; refresh?: boolean; } export function useCcwLitellmStatus(options: UseCcwLitellmStatusOptions = {}) { const { staleTime = 5 * 60 * 1000, enabled = true, refresh = false } = options; return useQuery({ queryKey: [...apiSettingsKeys.ccwLitellm(), 'status', refresh], queryFn: () => checkCcwLitellmStatus(refresh), staleTime, enabled, }); } export function useInstallCcwLitellm() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: () => installCcwLitellm(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() }); }, }); return { install: mutation.mutateAsync, isInstalling: mutation.isPending, error: mutation.error, }; } export function useUninstallCcwLitellm() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: () => uninstallCcwLitellm(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() }); }, }); return { uninstall: mutation.mutateAsync, isUninstalling: mutation.isPending, error: mutation.error, }; } // ======================================== // CLI Settings Hooks // ======================================== export interface UseCliSettingsOptions { staleTime?: number; enabled?: boolean; } export interface UseCliSettingsReturn { cliSettings: CliSettingsEndpoint[]; totalCount: number; enabledCount: number; providerBasedCount: number; directCount: number; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise; invalidate: () => Promise; } export function useCliSettings(options: UseCliSettingsOptions = {}): UseCliSettingsReturn { const { staleTime = STALE_TIME, enabled = true } = options; const queryClient = useQueryClient(); const query = useQuery({ queryKey: apiSettingsKeys.cliSettings(), queryFn: fetchCliSettings, staleTime, enabled, retry: 2, }); const cliSettings = query.data?.endpoints ?? []; const enabledCliSettings = cliSettings.filter((s) => s.enabled); // Determine mode based on whether settings have providerId in description or env vars const providerBasedCount = cliSettings.filter((s) => { // Provider-based: has ANTHROPIC_BASE_URL set to provider's apiBase return s.settings.env.ANTHROPIC_BASE_URL && !s.settings.env.ANTHROPIC_BASE_URL.includes('api.anthropic.com'); }).length; const directCount = cliSettings.length - providerBasedCount; const refetch = async () => { await query.refetch(); }; const invalidate = async () => { await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() }); }; return { cliSettings, totalCount: cliSettings.length, enabledCount: enabledCliSettings.length, providerBasedCount, directCount, isLoading: query.isLoading, isFetching: query.isFetching, error: query.error, refetch, invalidate, }; } export function useCreateCliSettings() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (request: SaveCliSettingsRequest) => createCliSettings(request), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() }); }, }); return { createCliSettings: mutation.mutateAsync, isCreating: mutation.isPending, error: mutation.error, }; } export function useUpdateCliSettings() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ endpointId, request }: { endpointId: string; request: Partial }) => updateCliSettings(endpointId, request), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() }); }, }); return { updateCliSettings: (endpointId: string, request: Partial) => mutation.mutateAsync({ endpointId, request }), isUpdating: mutation.isPending, error: mutation.error, }; } export function useDeleteCliSettings() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (endpointId: string) => deleteCliSettings(endpointId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() }); }, }); return { deleteCliSettings: mutation.mutateAsync, isDeleting: mutation.isPending, error: mutation.error, }; } export function useToggleCliSettings() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ endpointId, enabled }: { endpointId: string; enabled: boolean }) => toggleCliSettingsEnabled(endpointId, enabled), onSuccess: () => { queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() }); }, }); return { toggleCliSettings: (endpointId: string, enabled: boolean) => mutation.mutateAsync({ endpointId, enabled }), isToggling: mutation.isPending, error: mutation.error, }; }