Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm/proxy/health_endpoints/_health_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,7 @@ async def test_model_connection(
"responses",
"anthropic_messages",
"ocr",
"evaluation",
]
| None = fastapi.Body(
None,
Expand Down
17 changes: 17 additions & 0 deletions tests/integration/providers/test_decisions_wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,23 @@ def test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cos
assert math.isclose(_number(row["spend"]), expected_spend, rel_tol=1e-9), row


def test_test_connection_evaluation_mode_uses_typesafe_decisions_path(gateway: Gateway) -> None:
provider: Final = _PROVIDERS[1]
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _answer_body(provider))
model: Final = _deployment(scenario, handle, provider)
response: Final = gateway.request(
"POST",
"/health/test_connection",
{"litellm_params": {"model": model}, "mode": "evaluation"},
)

assert response.status_code == 200, response.text
assert response.json()["status"] == "success", response.text
(call,) = _upstream_calls(gateway, handle)
assert call["path"] == f"/{handle.scenario_id}{provider.path}"


def test_repeated_identical_requests_each_reach_the_upstream_and_are_each_billed(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _answer_body(_PERPLEXITY))
Expand Down
65 changes: 63 additions & 2 deletions tests/unit/proxy/health_endpoints/test_health_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import copy
import json
import time
from collections.abc import Iterator, Mapping, Sequence
from collections.abc import Awaitable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from datetime import datetime, timedelta
from types import MappingProxyType, SimpleNamespace
Expand Down Expand Up @@ -705,6 +705,10 @@ def _test_connection_probe(
) -> Iterator[AsyncMock]:
from litellm.types.router import Deployment, LiteLLM_Params

async def run_health_check(awaitable: Awaitable[object], _timeout: float) -> dict[str, str]:
await awaitable
return {"status": "healthy"}

router: Final = MagicMock()
router.get_deployment.side_effect = lambda model_id: (
Deployment(
Expand All @@ -727,7 +731,7 @@ def _test_connection_probe(
patch("litellm.proxy.health_endpoints._health_endpoints.litellm.ahealth_check", ahealth_check),
patch(
"litellm.proxy.health_endpoints._health_endpoints.run_with_timeout",
AsyncMock(return_value={"status": "healthy"}),
AsyncMock(side_effect=run_health_check),
),
):
yield ahealth_check
Expand Down Expand Up @@ -874,6 +878,28 @@ async def test_test_model_connection_request_mode_wins_over_resolved_mode():
assert ahealth_check.call_args.kwargs["mode"] == "chat"


@pytest.mark.asyncio
async def test_test_model_connection_evaluation_mode_uses_decisions_handler():
deployment: Final = MappingProxyType(
{
"model_name": "typesafe/jev-latest",
"litellm_params": {"model": "typesafe/jev-latest", "api_key": "fake-typesafe-key"},
"model_info": {"id": "typesafe-jev-id"},
}
)
with _test_connection_probe(deployment) as ahealth_check:
result: Final = await health_test_model_connection(
request=MagicMock(),
mode="evaluation",
litellm_params={"model": "typesafe/jev-latest"},
model_info={"id": "typesafe-jev-id"},
user_api_key_dict=UserAPIKeyAuth(user_id="test-user", token="test-token"),
)

assert result["status"] == "success"
assert ahealth_check.call_args.kwargs["mode"] == "evaluation"


@pytest.mark.asyncio
async def test_test_model_connection_uses_loaded_deployment_team_id():
"""
Expand Down Expand Up @@ -4665,6 +4691,41 @@ def test_test_model_connection_accepts_image_edit_mode(monkeypatch):
assert response.json()["status"] == "success"


def test_test_model_connection_accepts_evaluation_mode(monkeypatch):
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()

app = FastAPI()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Required Final annotations missing

The new test leaves app, client, upstream, and response without Final. The repository guide requires every variable to use Final; add these annotations before merging

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

app.include_router(_health_endpoints_module.router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
client = TestClient(app)

with (
patch( # test-quality-ok: endpoint reads the proxy-global DB client and 500s when it is None; it has no injection seam
"litellm.proxy.proxy_server.prisma_client", MagicMock()
),
respx.mock(assert_all_called=True) as respx_mock,
):
upstream = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(
json={
"model": "jev-latest",
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
)
response = client.post(
"/health/test_connection",
json={
"mode": "evaluation",
"litellm_params": {"model": "typesafe/jev-latest", "api_key": "sk-test"},
},
)

assert response.status_code == 200, response.text
assert response.json()["status"] == "success"
assert upstream.called


def _pointfive_admin() -> UserAPIKeyAuth:
return UserAPIKeyAuth(token="admin-token", user_id="admin-user", user_role=LitellmUserRoles.PROXY_ADMIN)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@
expect(await screen.findByRole("button", { name: "Add Model" })).toBeInTheDocument();
});

it("offers the Evaluation decisions mode", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("proxy_admin", "user-1", true));

renderWithProviders(<AddModelForm {...createTestProps()} />);

await screen.findByText("Provider");
await userEvent.click(screen.getByRole("combobox", { name: "Mode" }));

expect(await screen.findByRole("option", { name: "Evaluation - /v1/decisions", exact: true })).toBeInTheDocument();
});

it("shows only the Close button in the connection test dialog footer", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("proxy_admin", "user-1", true));
Expand All @@ -321,7 +333,7 @@
await userEvent.click(await screen.findByTestId("test-connect-btn"));

const dialog = await screen.findByRole("dialog");
const footer = dialog.querySelector('[data-slot="dialog-footer"]');

Check warning on line 336 in ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Avoid direct Node access. Prefer using the methods from Testing Library

Check warning on line 336 in ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Avoid direct Node access. Prefer using the methods from Testing Library
expect(footer).not.toBeNull();
expect(footer!.textContent?.trim()).toBe("Close");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const TEST_MODES = [
{ value: "batch", label: "Batch - /batch" },
{ value: "anthropic_messages", label: "Anthropic Messages - /v1/messages" },
{ value: "ocr", label: "OCR - /ocr" },
{ value: "evaluation", label: "Evaluation - /v1/decisions" },
];

// Define the available auto router routing strategies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ describe("prepareModelAddRequest", () => {
expect(deployment.litellmParamsObj.timeout).toBe(5);
});

it("saves the selected mode under model_info", async () => {
const formValues = {
model_mappings: [{ public_name: "Jev", litellm_model: "typesafe/jev-latest" }],
mode: "evaluation",
};

const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);

expect(deployments).toHaveLength(1);
const [deployment] = deployments!;
expect(deployment.modelInfoObj.mode).toBe("evaluation");
expect(deployment.litellmParamsObj).not.toHaveProperty("mode");
});

it.each([
["OpenAI", "openai/*"],
["Azure_AI_Studio", "azure_ai/*"],
Expand Down
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/src/lib/http/schema.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading