fix(ui): fetch version + debug flag from /health/readiness/details (#27896)

* fix(ui): fetch version + debug flag from /health/readiness/details

The proxy moved `litellm_version`, `is_detailed_debug`, and other
diagnostic fields off the public `/health/readiness` payload behind
an auth-gated `/health/readiness/details` endpoint. The navbar
version tag and the detailed-debug-mode banner stopped working
because they were still reading those fields from the unauthed
response, which no longer contains them.

Replace `useHealthReadiness` with a `useHealthReadinessDetails`
hook that takes an `accessToken` argument and sends a Bearer header
to the auth-gated endpoint. The hook stays disabled while
`accessToken` is falsy, so the navbar can keep rendering on the
public model hub (where the token is null) without triggering an
auth redirect or a 401-loop.

* fix(ui): disable retries on readiness/details + cover token forwarding

Two small follow-ups on the readiness/details migration:

- Set `retry: false` on the query. The payload feeds a passive
  navbar tag and a debug banner; a 401 from an expired token
  shouldn't fan out into three retries against the proxy.
- Add navbar specs that assert the `accessToken` prop is forwarded
  into the hook (matches the DebugWarningBanner spec). Without
  this, the navbar could silently regress to passing `undefined`
  and the existing tests wouldn't catch it.
This commit is contained in:
yuneng-jiang
2026-05-13 20:33:23 -07:00
committed by GitHub
parent 1294165768
commit 1bea77e564
7 changed files with 122 additions and 53 deletions
@@ -1,29 +0,0 @@
import { getProxyBaseUrl } from "@/components/networking";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
const healthReadinessKeys = createQueryKeys("healthReadiness");
interface HealthReadinessResponse {
litellm_version?: string;
log_level?: string;
is_detailed_debug?: boolean;
[key: string]: any;
}
const fetchHealthReadiness = async (): Promise<HealthReadinessResponse> => {
const baseUrl = getProxyBaseUrl();
const response = await fetch(`${baseUrl}/health/readiness`);
if (!response.ok) {
throw new Error(`Failed to fetch health readiness: ${response.statusText}`);
}
return response.json();
};
export const useHealthReadiness = (): UseQueryResult<HealthReadinessResponse> => {
return useQuery<HealthReadinessResponse>({
queryKey: healthReadinessKeys.detail("readiness"),
queryFn: fetchHealthReadiness,
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
@@ -0,0 +1,61 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import {
getGlobalLitellmHeaderName,
getProxyBaseUrl,
} from "@/components/networking";
import { createQueryKeys } from "../common/queryKeysFactory";
const healthReadinessDetailsKeys = createQueryKeys("healthReadinessDetails");
export interface HealthReadinessDetailsResponse {
status: string;
db?: string;
cache?: unknown;
litellm_version?: string;
success_callbacks?: string[];
use_aiohttp_transport?: boolean;
log_level?: string;
is_detailed_debug?: boolean;
}
const fetchHealthReadinessDetails = async (
accessToken: string,
): Promise<HealthReadinessDetailsResponse> => {
const baseUrl = getProxyBaseUrl();
const response = await fetch(`${baseUrl}/health/readiness/details`, {
method: "GET",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(
`Failed to fetch health readiness details: ${response.statusText}`,
);
}
return response.json();
};
/**
* Fetches the auth-gated detailed readiness payload.
*
* The caller passes its own `accessToken` so this hook stays usable in both
* authed and unauthed shells (e.g. the public model hub renders the navbar
* with a null token). When `accessToken` is falsy the query stays disabled
* and `data` is undefined — consumers should treat that as "details
* unavailable" rather than an error.
*/
export const useHealthReadinessDetails = (
accessToken: string | null | undefined,
): UseQueryResult<HealthReadinessDetailsResponse> => {
return useQuery<HealthReadinessDetailsResponse>({
queryKey: healthReadinessDetailsKeys.detail("readiness"),
queryFn: () => fetchHealthReadinessDetails(accessToken!),
enabled: Boolean(accessToken),
staleTime: 5 * 60 * 1000,
// The response feeds a passive navbar tag and a debug banner — a failed
// call (e.g. expired token → 401) shouldn't fan out into three retries.
retry: false,
});
};
@@ -80,7 +80,7 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
isDarkMode={false}
toggleDarkMode={() => { }}
/>
<DebugWarningBanner />
<DebugWarningBanner accessToken={accessToken} />
<div className="flex flex-1 overflow-auto">
<div className="mt-2">
<SidebarProvider
@@ -2,40 +2,52 @@ import { renderWithProviders, screen } from "../../tests/test-utils";
import { vi } from "vitest";
import { DebugWarningBanner } from "./DebugWarningBanner";
vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness", () => ({
useHealthReadiness: vi.fn(),
vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({
useHealthReadinessDetails: vi.fn(),
}));
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
describe("DebugWarningBanner", () => {
it("should render", () => {
vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner />);
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner accessToken="token" />);
expect(screen.getByRole("alert")).toBeInTheDocument();
});
it("should show warning when detailed debug mode is active", () => {
vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner />);
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner accessToken="token" />);
expect(screen.getByText(/Performance Warning: Detailed Debug Mode Active/i)).toBeInTheDocument();
});
it("should mention LITELLM_LOG=DEBUG in the description", () => {
vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner />);
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: { is_detailed_debug: true } } as any);
renderWithProviders(<DebugWarningBanner accessToken="token" />);
expect(screen.getByText("LITELLM_LOG=DEBUG")).toBeInTheDocument();
});
it("should render nothing when is_detailed_debug is false", () => {
vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: false } } as any);
const { container } = renderWithProviders(<DebugWarningBanner />);
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: { is_detailed_debug: false } } as any);
const { container } = renderWithProviders(<DebugWarningBanner accessToken="token" />);
expect(container).toBeEmptyDOMElement();
});
it("should render nothing when health data is undefined", () => {
vi.mocked(useHealthReadiness).mockReturnValue({ data: undefined } as any);
const { container } = renderWithProviders(<DebugWarningBanner />);
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: undefined } as any);
const { container } = renderWithProviders(<DebugWarningBanner accessToken="token" />);
expect(container).toBeEmptyDOMElement();
});
it("should pass accessToken to the readiness hook", () => {
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: undefined } as any);
renderWithProviders(<DebugWarningBanner accessToken="my-token" />);
expect(useHealthReadinessDetails).toHaveBeenCalledWith("my-token");
});
it("should pass a null accessToken through (disables the hook)", () => {
vi.mocked(useHealthReadinessDetails).mockReturnValue({ data: undefined } as any);
renderWithProviders(<DebugWarningBanner accessToken={null} />);
expect(useHealthReadinessDetails).toHaveBeenCalledWith(null);
});
});
@@ -2,10 +2,14 @@
import React from "react";
import { Alert } from "antd";
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
export const DebugWarningBanner: React.FC = () => {
const { data: healthData } = useHealthReadiness();
interface DebugWarningBannerProps {
accessToken: string | null;
}
export const DebugWarningBanner: React.FC<DebugWarningBannerProps> = ({ accessToken }) => {
const { data: healthData } = useHealthReadinessDetails(accessToken);
// Only show banner if detailed debug mode is explicitly enabled
if (!healthData?.is_detailed_debug) {
@@ -90,7 +90,7 @@ vi.mock("./Navbar/CommunityEngagementButtons/CommunityEngagementButtons", () =>
// Create mock functions that can be controlled in tests
let mockUseThemeImpl = () => ({ logoUrl: null as string | null });
let mockUseHealthReadinessImpl = () => ({ data: null as any });
let mockUseHealthReadinessDetailsImpl = () => ({ data: null as any });
let mockGetLocalStorageItemImpl = (key: string) => null as string | null;
let mockUseAuthorizedImpl = () => ({
userId: "test-user",
@@ -99,12 +99,17 @@ let mockUseAuthorizedImpl = () => ({
premiumUser: false,
});
const useHealthReadinessDetailsSpy = vi.hoisted(() => vi.fn());
vi.mock("@/contexts/ThemeContext", () => ({
useTheme: () => mockUseThemeImpl(),
}));
vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness", () => ({
useHealthReadiness: () => mockUseHealthReadinessImpl(),
vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({
useHealthReadinessDetails: (accessToken: string | null | undefined) => {
useHealthReadinessDetailsSpy(accessToken);
return mockUseHealthReadinessDetailsImpl();
},
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
@@ -204,14 +209,30 @@ describe("Navbar", () => {
});
it("should show version badge when health data contains version", () => {
mockUseHealthReadinessImpl = () => ({ data: { litellm_version: "1.0.0" } });
mockUseHealthReadinessDetailsImpl = () => ({ data: { litellm_version: "1.0.0" } });
renderWithProviders(<Navbar {...defaultProps} />);
expect(screen.getByText("v1.0.0")).toBeInTheDocument();
// Reset mock
mockUseHealthReadinessImpl = () => ({ data: null });
mockUseHealthReadinessDetailsImpl = () => ({ data: null });
});
it("should forward accessToken to the readiness hook", () => {
useHealthReadinessDetailsSpy.mockClear();
renderWithProviders(<Navbar {...defaultProps} accessToken="my-token" />);
expect(useHealthReadinessDetailsSpy).toHaveBeenCalledWith("my-token");
});
it("should forward a null accessToken to the readiness hook (disables the hook)", () => {
useHealthReadinessDetailsSpy.mockClear();
renderWithProviders(<Navbar {...defaultProps} accessToken={null} />);
expect(useHealthReadinessDetailsSpy).toHaveBeenCalledWith(null);
});
it("should use custom logo from theme context", () => {
@@ -1,4 +1,4 @@
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
import { getProxyBaseUrl } from "@/components/networking";
import { useTheme } from "@/contexts/ThemeContext";
@@ -46,7 +46,7 @@ const Navbar: React.FC<NavbarProps> = ({
const baseUrl = getProxyBaseUrl();
const [logoutUrl, setLogoutUrl] = useState("");
const { logoUrl } = useTheme();
const { data: healthData } = useHealthReadiness();
const { data: healthData } = useHealthReadinessDetails(accessToken);
const version = healthData?.litellm_version;
const disableBouncingIcon = useDisableBouncingIcon();