mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge pull request #21071 from atapia27/feat/healthcheck-model_id-fix
healthcheck-model_id-fix
This commit is contained in:
commit
3f1554c099
11 changed files with 542 additions and 145 deletions
|
|
@ -283,12 +283,17 @@ async def perform_health_check(
|
|||
model: Optional[str] = None,
|
||||
cli_model: Optional[str] = None,
|
||||
details: Optional[bool] = True,
|
||||
model_id: Optional[str] = None,
|
||||
max_concurrency: Optional[int] = None,
|
||||
instrumentation_context: Optional[dict] = None,
|
||||
):
|
||||
"""
|
||||
Perform a health check on the system.
|
||||
|
||||
When model_id is provided, only the deployment with that id is checked
|
||||
(so models that share the same name but have different ids are checked separately).
|
||||
When model (name) is provided, all deployments matching that name are checked.
|
||||
|
||||
Returns:
|
||||
(bool): True if the health check passes, False otherwise.
|
||||
"""
|
||||
|
|
@ -314,7 +319,12 @@ async def perform_health_check(
|
|||
cycle_start_time = time.monotonic()
|
||||
requested_model_count = len(model_list)
|
||||
|
||||
if model is not None:
|
||||
# Filter by model_id first so a single deployment is checked when id is specified
|
||||
if model_id is not None:
|
||||
_by_id = [x for x in model_list if (x.get("model_info") or {}).get("id") == model_id]
|
||||
if _by_id:
|
||||
model_list = _by_id
|
||||
elif model is not None:
|
||||
_new_model_list = [
|
||||
x for x in model_list if x["litellm_params"]["model"] == model
|
||||
]
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ async def health_services_endpoint( # noqa: PLR0915
|
|||
service_in_success_callbacks = True
|
||||
else:
|
||||
for cb in litellm.success_callback:
|
||||
if hasattr(cb, "callback_name") and cb.callback_name == service:
|
||||
if getattr(cb, "callback_name", None) == service:
|
||||
service_in_success_callbacks = True
|
||||
break
|
||||
cb_id = get_callback_identifier(cb)
|
||||
|
|
@ -746,7 +746,8 @@ async def _perform_health_check_and_save(
|
|||
cli_model=cli_model,
|
||||
model=target_model,
|
||||
details=details,
|
||||
max_concurrency=max_concurrency,
|
||||
max_concurrency=max_concurrency,,
|
||||
model_id=model_id,
|
||||
)
|
||||
|
||||
# Optionally save health check result to database (non-blocking)
|
||||
|
|
|
|||
|
|
@ -473,6 +473,50 @@ def test_update_litellm_params_for_health_check():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_perform_health_check_filters_by_model_id():
|
||||
"""
|
||||
When model_id is passed, only that deployment is checked (not all deployments
|
||||
that share the same model name).
|
||||
"""
|
||||
from litellm.proxy.health_check import perform_health_check
|
||||
|
||||
# Two deployments with same model_name but different ids
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"model_info": {"id": "deployment-id-1"},
|
||||
"litellm_params": {"model": "gpt-4", "api_key": "fake-key-1"},
|
||||
},
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"model_info": {"id": "deployment-id-2"},
|
||||
"litellm_params": {"model": "gpt-4", "api_key": "fake-key-2"},
|
||||
},
|
||||
]
|
||||
|
||||
captured_list = []
|
||||
|
||||
async def mock_perform_health_check(m_list, details=True):
|
||||
captured_list.append(m_list)
|
||||
return [{"model": "gpt-4", "api_key": m_list[0]["litellm_params"]["api_key"]}], []
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.health_check._perform_health_check",
|
||||
side_effect=mock_perform_health_check,
|
||||
):
|
||||
healthy_endpoints, unhealthy_endpoints = await perform_health_check(
|
||||
model_list=model_list, model_id="deployment-id-2", details=True
|
||||
)
|
||||
|
||||
# Only one deployment (deployment-id-2) should have been passed to _perform_health_check
|
||||
assert len(captured_list) == 1
|
||||
assert len(captured_list[0]) == 1
|
||||
assert (captured_list[0][0].get("model_info") or {}).get("id") == "deployment-id-2"
|
||||
assert len(healthy_endpoints) == 1
|
||||
assert healthy_endpoints[0]["api_key"] == "fake-key-2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_perform_health_check_with_health_check_model():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ sys.path.insert(0, os.path.abspath("../../.."))
|
|||
from litellm.proxy.health_endpoints._health_endpoints import (
|
||||
_aggregate_health_check_results,
|
||||
_build_model_param_to_info_mapping,
|
||||
_perform_health_check_and_save,
|
||||
_save_background_health_checks_to_db,
|
||||
_save_health_check_results_if_changed,
|
||||
_save_health_check_to_db,
|
||||
|
|
@ -466,5 +467,43 @@ async def test_get_all_latest_health_checks_without_model_id(mock_prisma):
|
|||
assert result[0].checked_at == mock_check2.checked_at # Latest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_perform_health_check_and_save_passes_model_id_to_perform_health_check():
|
||||
"""Test that _perform_health_check_and_save passes model_id to perform_health_check so health checks run by model id."""
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "gpt-4",
|
||||
"model_info": {"id": "deployment-abc"},
|
||||
"litellm_params": {"model": "gpt-4"},
|
||||
},
|
||||
]
|
||||
healthy = [{"model": "gpt-4"}]
|
||||
unhealthy = []
|
||||
|
||||
async def mock_perform_health_check(model_list, model=None, cli_model=None, details=True, model_id=None):
|
||||
return healthy, unhealthy
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.health_endpoints._health_endpoints.perform_health_check",
|
||||
side_effect=mock_perform_health_check,
|
||||
) as mock_perform:
|
||||
result = await _perform_health_check_and_save(
|
||||
model_list=model_list,
|
||||
target_model=None,
|
||||
cli_model=None,
|
||||
details=True,
|
||||
prisma_client=None,
|
||||
start_time=0.0,
|
||||
user_id="user-1",
|
||||
model_id="deployment-abc",
|
||||
)
|
||||
|
||||
mock_perform.assert_called_once()
|
||||
call_kwargs = mock_perform.call_args[1]
|
||||
assert call_kwargs["model_id"] == "deployment-abc"
|
||||
assert result["healthy_count"] == 1
|
||||
assert result["unhealthy_count"] == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ModelsAndEndpointsView from "./ModelsAndEndpointsView";
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ vi.mock("@/components/networking", () => ({
|
|||
getCallbacksCall: vi.fn().mockResolvedValue({ router_settings: {} }),
|
||||
setCallbacksCall: vi.fn().mockResolvedValue(undefined),
|
||||
getUiSettings: vi.fn().mockResolvedValue({ values: {} }),
|
||||
latestHealthChecksCall: vi.fn().mockResolvedValue({ latest_health_checks: {} }),
|
||||
getModelCostMapReloadStatus: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/models-and-endpoints/components/ModelAnalyticsTab/ModelAnalyticsTab", () => ({
|
||||
|
|
@ -27,6 +29,14 @@ vi.mock("@/components/add_model/AddModelForm", () => ({
|
|||
default: () => null,
|
||||
}));
|
||||
|
||||
const mockHealthCheckComponent = vi.fn((_props: { all_models_on_proxy?: string[] }) => null);
|
||||
vi.mock("@/components/model_dashboard/HealthCheckComponent", () => ({
|
||||
default: (props: { all_models_on_proxy?: string[] }) => {
|
||||
mockHealthCheckComponent(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
||||
default: () => ({
|
||||
teams: [],
|
||||
|
|
@ -104,4 +114,43 @@ describe("ModelsAndEndpointsView", () => {
|
|||
);
|
||||
expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
it("should pass model IDs (not model names) to HealthCheckComponent as all_models_on_proxy", async () => {
|
||||
mockHealthCheckComponent.mockClear();
|
||||
const modelDataWithIds = {
|
||||
data: [
|
||||
{ model_name: "gpt-4", model_info: { id: "deployment-id-1" } },
|
||||
{ model_name: "gpt-4", model_info: { id: "deployment-id-2" } },
|
||||
],
|
||||
};
|
||||
mockUseModelsInfo.mockReturnValue({
|
||||
data: { data: modelDataWithIds.data },
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const queryClient = createQueryClient();
|
||||
const { getByRole } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: modelDataWithIds.data }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const healthStatusTab = getByRole("tab", { name: "Health Status" });
|
||||
await act(async () => {
|
||||
healthStatusTab.click();
|
||||
});
|
||||
|
||||
expect(mockHealthCheckComponent).toHaveBeenCalled();
|
||||
const healthCheckProps = mockHealthCheckComponent.mock.calls[0][0];
|
||||
expect(healthCheckProps.all_models_on_proxy).toEqual(["deployment-id-1", "deployment-id-2"]);
|
||||
expect(healthCheckProps.all_models_on_proxy).not.toContain("gpt-4");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
|||
return modelDataResponse.data.map((model: any) => model.model_name);
|
||||
}, [modelDataResponse?.data]);
|
||||
|
||||
const allModelIdsOnProxy = useMemo<string[]>(() => {
|
||||
if (!modelDataResponse?.data) return [];
|
||||
return modelDataResponse.data
|
||||
.map((model: any) => model.model_info?.id)
|
||||
.filter((id: string | undefined): id is string => Boolean(id));
|
||||
}, [modelDataResponse?.data]);
|
||||
|
||||
const getProviderFromModel = (model: string) => {
|
||||
if (modelCostMapData !== null && modelCostMapData !== undefined) {
|
||||
if (typeof modelCostMapData == "object" && model in modelCostMapData) {
|
||||
|
|
@ -397,7 +404,7 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
|||
<HealthCheckComponent
|
||||
accessToken={accessToken}
|
||||
modelData={processedModelData}
|
||||
all_models_on_proxy={allModelsOnProxy}
|
||||
all_models_on_proxy={allModelIdsOnProxy}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
setSelectedModelId={setSelectedModelId}
|
||||
teams={teams}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import HealthCheckComponent from "./HealthCheckComponent";
|
||||
|
||||
const mockIndividualModelHealthCheckCall = vi.fn();
|
||||
const mockLatestHealthChecksCall = vi.fn();
|
||||
|
||||
vi.mock("../networking", () => ({
|
||||
individualModelHealthCheckCall: (...args: unknown[]) => mockIndividualModelHealthCheckCall(...args),
|
||||
latestHealthChecksCall: (...args: unknown[]) => mockLatestHealthChecksCall(...args),
|
||||
}));
|
||||
|
||||
describe("HealthCheckComponent", () => {
|
||||
const getDisplayModelName = (model: { model_name?: string }) => model.model_name ?? "";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLatestHealthChecksCall.mockResolvedValue({ latest_health_checks: {} });
|
||||
mockIndividualModelHealthCheckCall.mockResolvedValue({
|
||||
healthy_count: 1,
|
||||
unhealthy_count: 0,
|
||||
healthy_endpoints: [],
|
||||
unhealthy_endpoints: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the health check section", async () => {
|
||||
const modelData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "deployment-1" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<HealthCheckComponent
|
||||
accessToken="token"
|
||||
modelData={modelData}
|
||||
all_models_on_proxy={["deployment-1"]}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect(screen.getByText("Model Health Status")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Run health checks on individual models to verify they are working correctly"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call individualModelHealthCheckCall with model id when run health check is triggered", async () => {
|
||||
const modelData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "deployment-abc-123" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(
|
||||
<HealthCheckComponent
|
||||
accessToken="token-123"
|
||||
modelData={modelData}
|
||||
all_models_on_proxy={["deployment-abc-123"]}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
/>,
|
||||
);
|
||||
|
||||
const runButtons = screen.getAllByTestId("run-health-check-btn");
|
||||
expect(runButtons.length).toBeGreaterThanOrEqual(1);
|
||||
const runButton = runButtons[0];
|
||||
|
||||
await act(async () => {
|
||||
runButton.click();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
});
|
||||
|
||||
expect(mockIndividualModelHealthCheckCall).toHaveBeenCalledWith("token-123", "deployment-abc-123");
|
||||
expect(mockIndividualModelHealthCheckCall).not.toHaveBeenCalledWith("token-123", "gpt-4");
|
||||
});
|
||||
|
||||
describe("latest_health_checks keyed by model id", () => {
|
||||
it("should show status from latest_health_checks when keys match model ids", async () => {
|
||||
const modelData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "id-alpha" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "id-beta" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"id-alpha": {
|
||||
status: "healthy",
|
||||
checked_at: "2024-01-15T10:00:00Z",
|
||||
error_message: null,
|
||||
},
|
||||
"id-beta": {
|
||||
status: "unhealthy",
|
||||
checked_at: "2024-01-15T10:05:00Z",
|
||||
error_message: "Connection failed",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<HealthCheckComponent
|
||||
accessToken="token"
|
||||
modelData={modelData}
|
||||
all_models_on_proxy={["id-alpha", "id-beta"]}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect(mockLatestHealthChecksCall).toHaveBeenCalledWith("token");
|
||||
const healthyBadges = screen.getAllByText("healthy");
|
||||
const unhealthyBadges = screen.getAllByText("unhealthy");
|
||||
expect(healthyBadges.length).toBeGreaterThanOrEqual(1);
|
||||
expect(unhealthyBadges.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should skip latest_health_checks entries whose key is not a known model id", async () => {
|
||||
const modelData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "current-model-id" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"current-model-id": {
|
||||
status: "healthy",
|
||||
checked_at: "2024-01-15T10:00:00Z",
|
||||
error_message: null,
|
||||
},
|
||||
"deleted-or-unknown-id": {
|
||||
status: "unhealthy",
|
||||
checked_at: "2024-01-15T10:05:00Z",
|
||||
error_message: "Stale entry",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<HealthCheckComponent
|
||||
accessToken="token"
|
||||
modelData={modelData}
|
||||
all_models_on_proxy={["current-model-id"]}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect(screen.getByText("healthy")).toBeInTheDocument();
|
||||
expect(screen.queryByText("unhealthy")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not apply status when latest_health_checks key is model name not model id", async () => {
|
||||
const modelData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gpt-4",
|
||||
model_info: { id: "model-id-123" },
|
||||
litellm_model_name: "gpt-4",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockLatestHealthChecksCall.mockResolvedValue({
|
||||
latest_health_checks: {
|
||||
"gpt-4": {
|
||||
status: "healthy",
|
||||
checked_at: "2024-01-15T10:00:00Z",
|
||||
error_message: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<HealthCheckComponent
|
||||
accessToken="token"
|
||||
modelData={modelData}
|
||||
all_models_on_proxy={["model-id-123"]}
|
||||
getDisplayModelName={getDisplayModelName}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect(screen.queryByText("healthy")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("none")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -53,65 +53,48 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
|
||||
const healthTableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
// Initialize health statuses on component mount
|
||||
// Initialize health statuses on component mount (keyed by model id)
|
||||
useEffect(() => {
|
||||
if (!accessToken || !modelData?.data) return;
|
||||
|
||||
const initializeHealthStatuses = async () => {
|
||||
const healthStatusMap: { [key: string]: HealthStatus } = {};
|
||||
|
||||
// Initialize all models with default state using model names
|
||||
// Initialize all models with default state using model ids
|
||||
modelData.data.forEach((model: any) => {
|
||||
const modelName = model.model_name;
|
||||
healthStatusMap[modelName] = {
|
||||
status: "none",
|
||||
lastCheck: "None",
|
||||
lastSuccess: "None",
|
||||
loading: false,
|
||||
error: undefined,
|
||||
fullError: undefined,
|
||||
successResponse: undefined,
|
||||
};
|
||||
const modelId = model.model_info?.id;
|
||||
if (modelId) {
|
||||
healthStatusMap[modelId] = {
|
||||
status: "none",
|
||||
lastCheck: "None",
|
||||
lastSuccess: "None",
|
||||
loading: false,
|
||||
error: undefined,
|
||||
fullError: undefined,
|
||||
successResponse: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const latestHealthChecks = await latestHealthChecksCall(accessToken);
|
||||
|
||||
// Override with actual database data if it exists
|
||||
// Override with actual database data if it exists (latest_health_checks is keyed by model_id)
|
||||
if (
|
||||
latestHealthChecks &&
|
||||
latestHealthChecks.latest_health_checks &&
|
||||
typeof latestHealthChecks.latest_health_checks === "object"
|
||||
) {
|
||||
Object.entries(latestHealthChecks.latest_health_checks).forEach(([key, checkData]: [string, any]) => {
|
||||
Object.entries(latestHealthChecks.latest_health_checks).forEach(([modelId, checkData]: [string, any]) => {
|
||||
if (!checkData) return;
|
||||
|
||||
let targetModelName: string | null = null;
|
||||
// Key is model_id from the backend (guaranteed by DB schema)
|
||||
const modelExists = modelData.data.some((m: any) => m.model_info?.id === modelId);
|
||||
if (!modelExists) return;
|
||||
|
||||
// The key could be either model_id or model_name, try both approaches
|
||||
const directModelMatch = modelData.data.find((m: any) => m.model_name === key);
|
||||
if (directModelMatch) {
|
||||
targetModelName = directModelMatch.model_name;
|
||||
} else {
|
||||
// If not a direct match, treat as model_id and find the corresponding model
|
||||
const modelByIdMatch = modelData.data.find((m: any) => m.model_info && m.model_info.id === key);
|
||||
if (modelByIdMatch) {
|
||||
targetModelName = modelByIdMatch.model_name;
|
||||
} else {
|
||||
// Check if checkData contains model_name and use that
|
||||
if (checkData.model_name) {
|
||||
const modelByNameInData = modelData.data.find((m: any) => m.model_name === checkData.model_name);
|
||||
if (modelByNameInData) {
|
||||
targetModelName = modelByNameInData.model_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const fullError = checkData.error_message || undefined;
|
||||
|
||||
if (targetModelName) {
|
||||
const fullError = checkData.error_message || undefined;
|
||||
|
||||
healthStatusMap[targetModelName] = {
|
||||
healthStatusMap[modelId] = {
|
||||
status: checkData.status || "unknown",
|
||||
lastCheck: checkData.checked_at ? new Date(checkData.checked_at).toLocaleString() : "None",
|
||||
lastSuccess:
|
||||
|
|
@ -125,7 +108,6 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
fullError: fullError,
|
||||
successResponse: checkData.status === "healthy" ? checkData : undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (healthError) {
|
||||
|
|
@ -246,33 +228,31 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
return cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned;
|
||||
};
|
||||
|
||||
const runIndividualHealthCheck = async (modelName: string) => {
|
||||
const runIndividualHealthCheck = async (modelId: string) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
...prev[modelName],
|
||||
[modelId]: {
|
||||
...prev[modelId],
|
||||
loading: true,
|
||||
status: "checking",
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
// Run the health check and process the response directly
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelName);
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelId);
|
||||
const currentTime = new Date().toLocaleString();
|
||||
|
||||
// Check if there are any unhealthy endpoints (which means this specific model failed)
|
||||
if (response.unhealthy_count > 0 && response.unhealthy_endpoints && response.unhealthy_endpoints.length > 0) {
|
||||
const rawError = response.unhealthy_endpoints[0]?.error || "Health check failed";
|
||||
const errorMessage = extractMeaningfulError(rawError);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "unhealthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: prev[modelName]?.lastSuccess || "None",
|
||||
lastSuccess: prev[modelId]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
fullError: rawError,
|
||||
|
|
@ -281,7 +261,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
} else {
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "healthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: currentTime,
|
||||
|
|
@ -291,41 +271,33 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}));
|
||||
}
|
||||
|
||||
// Refresh health status from database to get the saved check data including timestamp
|
||||
try {
|
||||
const latestHealthChecks = await latestHealthChecksCall(accessToken);
|
||||
const checkData = latestHealthChecks.latest_health_checks?.[modelId];
|
||||
|
||||
// Find the model ID for this model name to look up database data
|
||||
const model = modelData.data.find((m: any) => m.model_name === modelName);
|
||||
if (model) {
|
||||
const modelId = model.model_info.id;
|
||||
const checkData = latestHealthChecks.latest_health_checks?.[modelId];
|
||||
|
||||
if (checkData) {
|
||||
const fullError = checkData.error_message || undefined;
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
status: checkData.status || prev[modelName]?.status || "unknown",
|
||||
lastCheck: checkData.checked_at
|
||||
? new Date(checkData.checked_at).toLocaleString()
|
||||
: prev[modelName]?.lastCheck || "None",
|
||||
lastSuccess:
|
||||
checkData.status === "healthy"
|
||||
? checkData.checked_at
|
||||
? new Date(checkData.checked_at).toLocaleString()
|
||||
: prev[modelName]?.lastSuccess || "None"
|
||||
: prev[modelName]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: fullError ? extractMeaningfulError(fullError) : prev[modelName]?.error,
|
||||
fullError: fullError || prev[modelName]?.fullError,
|
||||
successResponse: checkData.status === "healthy" ? checkData : prev[modelName]?.successResponse,
|
||||
},
|
||||
}));
|
||||
}
|
||||
if (checkData) {
|
||||
const fullError = checkData.error_message || undefined;
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelId]: {
|
||||
status: checkData.status || prev[modelId]?.status || "unknown",
|
||||
lastCheck: checkData.checked_at
|
||||
? new Date(checkData.checked_at).toLocaleString()
|
||||
: prev[modelId]?.lastCheck || "None",
|
||||
lastSuccess:
|
||||
checkData.status === "healthy"
|
||||
? checkData.checked_at
|
||||
? new Date(checkData.checked_at).toLocaleString()
|
||||
: prev[modelId]?.lastSuccess || "None"
|
||||
: prev[modelId]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: fullError ? extractMeaningfulError(fullError) : prev[modelId]?.error,
|
||||
fullError: fullError || prev[modelId]?.fullError,
|
||||
successResponse: checkData.status === "healthy" ? checkData : prev[modelId]?.successResponse,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (dbError) {
|
||||
// Ignore database errors - we already have the health check result from the API call
|
||||
console.debug("Could not fetch updated status from database (non-critical):", dbError);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -334,10 +306,10 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
const errorMessage = extractMeaningfulError(rawError);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "unhealthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: prev[modelName]?.lastSuccess || "None",
|
||||
lastSuccess: prev[modelId]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
fullError: rawError,
|
||||
|
|
@ -349,11 +321,10 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
const runAllHealthChecks = async () => {
|
||||
const modelsToCheck = selectedModelsForHealth.length > 0 ? selectedModelsForHealth : all_models_on_proxy;
|
||||
|
||||
// Set all models to loading state
|
||||
const loadingStatuses = modelsToCheck.reduce(
|
||||
(acc, modelName) => {
|
||||
acc[modelName] = {
|
||||
...modelHealthStatuses[modelName],
|
||||
(acc, modelId) => {
|
||||
acc[modelId] = {
|
||||
...modelHealthStatuses[modelId],
|
||||
loading: true,
|
||||
status: "checking",
|
||||
};
|
||||
|
|
@ -364,30 +335,25 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
|
||||
setModelHealthStatuses((prev) => ({ ...prev, ...loadingStatuses }));
|
||||
|
||||
// Store results from individual health checks
|
||||
const healthCheckResults: { [key: string]: any } = {};
|
||||
|
||||
// Run all health checks in parallel and collect results
|
||||
const healthCheckPromises = modelsToCheck.map(async (modelName) => {
|
||||
const healthCheckPromises = modelsToCheck.map(async (modelId) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
// Run the health check and store the result
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelName);
|
||||
healthCheckResults[modelName] = response;
|
||||
const response = await individualModelHealthCheckCall(accessToken, modelId);
|
||||
healthCheckResults[modelId] = response;
|
||||
|
||||
// Update status immediately based on response
|
||||
const currentTime = new Date().toLocaleString();
|
||||
// Check if there are any unhealthy endpoints (which means this specific model failed)
|
||||
if (response.unhealthy_count > 0 && response.unhealthy_endpoints && response.unhealthy_endpoints.length > 0) {
|
||||
const rawError = response.unhealthy_endpoints[0]?.error || "Health check failed";
|
||||
const errorMessage = extractMeaningfulError(rawError);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "unhealthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: prev[modelName]?.lastSuccess || "None",
|
||||
lastSuccess: prev[modelId]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
fullError: rawError,
|
||||
|
|
@ -396,7 +362,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
} else {
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "healthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: currentTime,
|
||||
|
|
@ -406,17 +372,16 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Health check failed for ${modelName}:`, error);
|
||||
// Set error status for failed health checks
|
||||
console.error(`Health check failed for model id ${modelId}:`, error);
|
||||
const currentTime = new Date().toLocaleString();
|
||||
const rawError = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage = extractMeaningfulError(rawError);
|
||||
setModelHealthStatuses((prev) => ({
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: "unhealthy",
|
||||
lastCheck: currentTime,
|
||||
lastSuccess: prev[modelName]?.lastSuccess || "None",
|
||||
lastSuccess: prev[modelId]?.lastSuccess || "None",
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
fullError: rawError,
|
||||
|
|
@ -425,27 +390,21 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
// Wait for all health checks to complete
|
||||
await Promise.allSettled(healthCheckPromises);
|
||||
|
||||
// Refresh health statuses from database to get the saved check data including timestamps
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
const latestHealthChecks = await latestHealthChecksCall(accessToken);
|
||||
|
||||
if (latestHealthChecks.latest_health_checks) {
|
||||
// Update health statuses from database, which should have the most accurate saved data
|
||||
Object.entries(latestHealthChecks.latest_health_checks).forEach(([modelId, checkData]: [string, any]) => {
|
||||
// Find the model name for this model ID
|
||||
const model = modelData.data.find((m: any) => m.model_info.id === modelId);
|
||||
if (model && modelsToCheck.includes(model.model_name) && checkData) {
|
||||
const modelName = model.model_name;
|
||||
if (modelsToCheck.includes(modelId) && checkData) {
|
||||
const fullError = checkData.error_message || undefined;
|
||||
setModelHealthStatuses((prev) => {
|
||||
const currentStatus = prev[modelName];
|
||||
const currentStatus = prev[modelId];
|
||||
return {
|
||||
...prev,
|
||||
[modelName]: {
|
||||
[modelId]: {
|
||||
status: checkData.status || currentStatus?.status || "unknown",
|
||||
lastCheck: checkData.checked_at
|
||||
? new Date(checkData.checked_at).toLocaleString()
|
||||
|
|
@ -468,15 +427,14 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
}
|
||||
} catch (dbError) {
|
||||
console.warn("Failed to fetch updated health statuses from database (non-critical):", dbError);
|
||||
// This is non-critical - we already have the health check results from the API calls
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelection = (modelName: string, checked: boolean) => {
|
||||
const handleModelSelection = (modelId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedModelsForHealth((prev) => [...prev, modelName]);
|
||||
setSelectedModelsForHealth((prev) => [...prev, modelId]);
|
||||
} else {
|
||||
setSelectedModelsForHealth((prev) => prev.filter((name) => name !== modelName));
|
||||
setSelectedModelsForHealth((prev) => prev.filter((id) => id !== modelId));
|
||||
setAllModelsSelected(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -580,8 +538,9 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
teams,
|
||||
)}
|
||||
data={modelData.data.map((model: any) => {
|
||||
const modelName = model.model_name;
|
||||
const healthStatus = modelHealthStatuses[modelName] || {
|
||||
const modelId = model.model_info?.id;
|
||||
const healthStatus = modelId ? modelHealthStatuses[modelId] : null;
|
||||
const status = healthStatus || {
|
||||
status: "none",
|
||||
lastCheck: "None",
|
||||
loading: false,
|
||||
|
|
@ -591,12 +550,12 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
|
|||
model_info: model.model_info,
|
||||
provider: model.provider,
|
||||
litellm_model_name: model.litellm_model_name,
|
||||
health_status: healthStatus.status,
|
||||
last_check: healthStatus.lastCheck,
|
||||
last_success: healthStatus.lastSuccess || "None",
|
||||
health_loading: healthStatus.loading,
|
||||
health_error: healthStatus.error,
|
||||
health_full_error: healthStatus.fullError,
|
||||
health_status: status.status,
|
||||
last_check: status.lastCheck,
|
||||
last_success: status.lastSuccess || "None",
|
||||
health_loading: status.loading,
|
||||
health_error: status.error,
|
||||
health_full_error: status.fullError,
|
||||
};
|
||||
})}
|
||||
isLoading={false}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export const healthCheckColumns = (
|
|||
modelHealthStatuses: { [key: string]: HealthStatus },
|
||||
selectedModelsForHealth: string[],
|
||||
allModelsSelected: boolean,
|
||||
handleModelSelection: (modelName: string, checked: boolean) => void,
|
||||
handleModelSelection: (modelId: string, checked: boolean) => void,
|
||||
handleSelectAll: (checked: boolean) => void,
|
||||
runIndividualHealthCheck: (modelName: string) => void,
|
||||
runIndividualHealthCheck: (modelId: string) => void,
|
||||
getStatusBadge: (status: string) => JSX.Element,
|
||||
getDisplayModelName: (model: any) => string,
|
||||
showErrorModal?: (modelName: string, cleanedError: string, fullError: string) => void,
|
||||
|
|
@ -62,14 +62,14 @@ export const healthCheckColumns = (
|
|||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const modelName = model.model_name;
|
||||
const isSelected = selectedModelsForHealth.includes(modelName);
|
||||
const modelId = model.model_info?.id ?? "";
|
||||
const isSelected = selectedModelsForHealth.includes(modelId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => handleModelSelection(modelName, e.target.checked)}
|
||||
onChange={(e) => handleModelSelection(modelId, e.target.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Tooltip title={model.model_info.id}>
|
||||
|
|
@ -169,8 +169,9 @@ export const healthCheckColumns = (
|
|||
);
|
||||
}
|
||||
|
||||
const modelName = model.model_name;
|
||||
const hasSuccessResponse = healthStatus.status === "healthy" && modelHealthStatuses[modelName]?.successResponse;
|
||||
const modelId = model.model_info?.id ?? "";
|
||||
const displayName = getDisplayModelName(model) || model.model_name;
|
||||
const hasSuccessResponse = healthStatus.status === "healthy" && modelHealthStatuses[modelId]?.successResponse;
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -178,7 +179,7 @@ export const healthCheckColumns = (
|
|||
{hasSuccessResponse && showSuccessModal && (
|
||||
<Tooltip title="View response details" placement="top">
|
||||
<button
|
||||
onClick={() => showSuccessModal(modelName, modelHealthStatuses[modelName]?.successResponse)}
|
||||
onClick={() => showSuccessModal(displayName, modelHealthStatuses[modelId]?.successResponse)}
|
||||
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded cursor-pointer transition-colors"
|
||||
>
|
||||
<InformationCircleIcon className="h-4 w-4" />
|
||||
|
|
@ -195,8 +196,9 @@ export const healthCheckColumns = (
|
|||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const modelName = model.model_name;
|
||||
const healthStatus = modelHealthStatuses[modelName];
|
||||
const modelId = model.model_info?.id ?? "";
|
||||
const displayName = getDisplayModelName(model) || model.model_name;
|
||||
const healthStatus = modelHealthStatuses[modelId];
|
||||
|
||||
if (!healthStatus?.error) {
|
||||
return <Text className="text-gray-400 text-sm">No errors</Text>;
|
||||
|
|
@ -215,7 +217,7 @@ export const healthCheckColumns = (
|
|||
{showErrorModal && fullError !== cleanedError && (
|
||||
<Tooltip title="View full error details" placement="top">
|
||||
<button
|
||||
onClick={() => showErrorModal(modelName, cleanedError, fullError)}
|
||||
onClick={() => showErrorModal(displayName, cleanedError, fullError)}
|
||||
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded cursor-pointer transition-colors"
|
||||
>
|
||||
<InformationCircleIcon className="h-4 w-4" />
|
||||
|
|
@ -294,8 +296,8 @@ export const healthCheckColumns = (
|
|||
},
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const modelName = model.model_name;
|
||||
const healthStatus = modelHealthStatuses[modelName];
|
||||
const modelId = model.model_info?.id ?? "";
|
||||
const healthStatus = modelHealthStatuses[modelId];
|
||||
const lastSuccess = healthStatus?.lastSuccess || "None";
|
||||
|
||||
return <Text className="text-gray-600 text-sm">{lastSuccess}</Text>;
|
||||
|
|
@ -306,7 +308,7 @@ export const healthCheckColumns = (
|
|||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const modelName = model.model_name;
|
||||
const modelId = model.model_info?.id ?? "";
|
||||
|
||||
const hasExistingStatus = model.health_status && model.health_status !== "none";
|
||||
const tooltipText = model.health_loading
|
||||
|
|
@ -318,6 +320,7 @@ export const healthCheckColumns = (
|
|||
return (
|
||||
<Tooltip title={tooltipText} placement="top">
|
||||
<button
|
||||
data-testid="run-health-check-btn"
|
||||
className={`p-2 rounded-md transition-colors ${
|
||||
model.health_loading
|
||||
? "text-gray-400 cursor-not-allowed bg-gray-100"
|
||||
|
|
@ -325,7 +328,7 @@ export const healthCheckColumns = (
|
|||
}`}
|
||||
onClick={() => {
|
||||
if (!model.health_loading) {
|
||||
runIndividualHealthCheck(modelName);
|
||||
runIndividualHealthCheck(modelId);
|
||||
}
|
||||
}}
|
||||
disabled={model.health_loading}
|
||||
|
|
|
|||
|
|
@ -317,3 +317,57 @@ describe("UI config and public endpoints", () => {
|
|||
expect(configCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("individualModelHealthCheckCall", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("should call /health with model_id query param so health checks run by deployment id", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
healthy_count: 1,
|
||||
unhealthy_count: 0,
|
||||
healthy_endpoints: [],
|
||||
unhealthy_endpoints: [],
|
||||
}),
|
||||
} as any);
|
||||
global.fetch = mockFetch as any;
|
||||
|
||||
await Networking.individualModelHealthCheckCall("token-123", "deployment-abc-456");
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
const urlStr = typeof url === "string" ? url : (url as Request).url;
|
||||
expect(urlStr).toContain("health");
|
||||
const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url);
|
||||
expect(parsed.searchParams.get("model_id")).toBe("deployment-abc-456");
|
||||
expect(parsed.searchParams.has("model")).toBe(false);
|
||||
});
|
||||
|
||||
it("should encode model_id in URL", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
healthy_count: 0,
|
||||
unhealthy_count: 0,
|
||||
healthy_endpoints: [],
|
||||
unhealthy_endpoints: [],
|
||||
}),
|
||||
} as any);
|
||||
global.fetch = mockFetch as any;
|
||||
|
||||
await Networking.individualModelHealthCheckCall("token", "id/with/slashes");
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url);
|
||||
expect(parsed.searchParams.get("model_id")).toBe("id/with/slashes");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5054,14 +5054,14 @@ export const healthCheckCall = async (accessToken: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const individualModelHealthCheckCall = async (accessToken: string, modelName: string) => {
|
||||
export const individualModelHealthCheckCall = async (accessToken: string, modelId: string) => {
|
||||
/**
|
||||
* Run health check for a specific model using model name
|
||||
* Run health check for a specific model using model ID (so each deployment is checked separately).
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/health?model=${encodeURIComponent(modelName)}`
|
||||
: `/health?model=${encodeURIComponent(modelName)}`;
|
||||
? `${proxyBaseUrl}/health?model_id=${encodeURIComponent(modelId)}`
|
||||
: `/health?model_id=${encodeURIComponent(modelId)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
|
|
@ -5081,7 +5081,7 @@ export const individualModelHealthCheckCall = async (accessToken: string, modelN
|
|||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to call /health for model ${modelName}:`, error);
|
||||
console.error(`Failed to call /health for model id ${modelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue