diff --git a/tests/test_litellm/proxy/agent_session_endpoints/conftest.py b/tests/test_litellm/proxy/agent_session_endpoints/conftest.py index dde13368a49..1fa228b38e1 100644 --- a/tests/test_litellm/proxy/agent_session_endpoints/conftest.py +++ b/tests/test_litellm/proxy/agent_session_endpoints/conftest.py @@ -247,6 +247,9 @@ def _build_test_app( run_router, session_router, ) + from litellm.proxy.agent_session_endpoints.error_envelope import ( + register_v2_exception_handlers, + ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth app = FastAPI() @@ -254,6 +257,8 @@ def _build_test_app( app.include_router(session_router) app.include_router(run_router) app.include_router(internal_router) + # Mirror the production setup so tests see the same /v2 error envelope. + register_v2_exception_handlers(app) def _fake_auth() -> UserAPIKeyAuth: return UserAPIKeyAuth( diff --git a/tests/test_litellm/proxy/agent_session_endpoints/test_error_envelope.py b/tests/test_litellm/proxy/agent_session_endpoints/test_error_envelope.py new file mode 100644 index 00000000000..b0095927906 --- /dev/null +++ b/tests/test_litellm/proxy/agent_session_endpoints/test_error_envelope.py @@ -0,0 +1,82 @@ +""" +Verify the standardized {error: {code, message, status, details?}} envelope +applies to /v2/* endpoints (HTTPException + ValidationError) and that +non-/v2 paths keep FastAPI's default detail shape. +""" + +import pytest + + +def test_404_emits_envelope_for_missing_session(client, noop_provider): + """A 404 from the v2 surface should be wrapped in the envelope.""" + res = client.get( + "/v2/sessions/does-not-exist", + headers={"Authorization": "Bearer k"}, + ) + # find_unique returns None -> ownership assert raises HTTPException(404). + assert res.status_code == 404 + body = res.json() + assert "error" in body and "detail" not in body + assert body["error"]["status"] == 404 + assert body["error"]["code"] == "not_found" + assert isinstance(body["error"]["message"], str) + + +def test_validation_error_wraps_in_envelope(client, noop_provider): + """A 422 ValidationError from a missing required body field should + arrive as ``{error: {code: validation_error, details: [...]}}``.""" + # POST /v2/agents requires `name` + `model`; omit both. + res = client.post( + "/v2/agents", + headers={"Authorization": "Bearer k"}, + json={}, + ) + assert res.status_code == 422 + body = res.json() + assert "error" in body and "detail" not in body + assert body["error"]["code"] == "validation_error" + assert body["error"]["status"] == 422 + assert isinstance(body["error"].get("details"), list) + # Validation details preserve the per-field {loc, msg, type} shape. + assert any("name" in str(d.get("loc", [])) for d in body["error"]["details"]) + + +def test_http_status_code_to_envelope_code_mapping(client, noop_provider): + """409 should map to ``conflict``, not a generic name.""" + # Create an agent + session, then try to POST with missing agent_id + # path -> simpler: trigger a 409 by creating two runs back-to-back + # on the same session. + agent = client.post( + "/v2/agents", + headers={"Authorization": "Bearer k"}, + json={"name": "t", "model": "gpt-4"}, + ).json() + sess = client.post( + f"/v2/agents/{agent['id']}/sessions", + headers={"Authorization": "Bearer k"}, + json={"repos": []}, + ).json() + sid = sess["id"] + daemon_token = sess["daemon_token"] + client.post( + f"/v2/sessions/{sid}/internal/register", + headers={"Authorization": f"Bearer {daemon_token}"}, + json={"vm_id": "i-noop"}, + ) + # First run: queued. + client.post( + f"/v2/sessions/{sid}/runs", + headers={"Authorization": "Bearer k"}, + json={"prompt": {"text": "hi"}}, + ) + # Second run on same session while first is still queued -> 409 run_busy. + res = client.post( + f"/v2/sessions/{sid}/runs", + headers={"Authorization": "Bearer k"}, + json={"prompt": {"text": "again"}}, + ) + assert res.status_code == 409 + body = res.json() + assert body["error"]["status"] == 409 + assert body["error"]["code"] == "conflict" + assert "run_busy" in body["error"]["message"] diff --git a/tests/test_litellm/proxy/agent_session_endpoints/test_followup_concurrency.py b/tests/test_litellm/proxy/agent_session_endpoints/test_followup_concurrency.py index bd7dc26dd29..b84a71cc0ee 100644 --- a/tests/test_litellm/proxy/agent_session_endpoints/test_followup_concurrency.py +++ b/tests/test_litellm/proxy/agent_session_endpoints/test_followup_concurrency.py @@ -90,7 +90,8 @@ def test_followup_returns_409_when_active_run_is_not_the_latest( json={"prompt": {"text": "should be blocked"}}, ) assert res.status_code == 409 - assert "run_busy" in res.json()["detail"] + # Error envelope shape: {"error": {"code", "message", ...}} + assert "run_busy" in res.json()["error"]["message"] def test_followup_normal_path_creates_run_when_no_active_runs(client, noop_provider): diff --git a/tests/test_litellm/proxy/agent_session_endpoints/test_view_only_admin_no_writes.py b/tests/test_litellm/proxy/agent_session_endpoints/test_view_only_admin_no_writes.py index 79b523710ea..a0224085946 100644 --- a/tests/test_litellm/proxy/agent_session_endpoints/test_view_only_admin_no_writes.py +++ b/tests/test_litellm/proxy/agent_session_endpoints/test_view_only_admin_no_writes.py @@ -44,7 +44,8 @@ def test_view_only_admin_cannot_create_agent(view_only_admin_client, noop_provid json={"name": "evil", "model": "gpt-4"}, ) assert res.status_code == 403 - assert "view-only" in res.json()["detail"].lower() + # Error envelope: {"error": {"code", "message", "status"}} + assert "view-only" in res.json()["error"]["message"].lower() def test_view_only_admin_cannot_update_other_tenant_agent(