litellm/tests/unit/proxy/proxy_server/test_routes_assistants.py
devin-ai-integration[bot] 141dba2372
fix(proxy): keep the mapped status on assistants and threads route errors (#44994)
* fix(proxy): keep the mapped status on assistants and threads route errors

* test(proxy): type and annotate the assistants and threads route error tests

* fix(proxy): redact internal details from assistants and threads route error messages

* test(proxy): integration cells for assistants route error mapping

* test(proxy): drop malformed JSON cases that never reach the assistants route handler

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-09 16:55:15 -07:00

218 lines
6.9 KiB
Python

"""Behavior pins for ``proxy_server.py`` assistants routes.
Pins (PR2):
- GET /v1/assistants
- GET /assistants
- POST /v1/assistants
- POST /assistants
- DELETE /v1/assistants/{assistant_id:path}
- DELETE /assistants/{assistant_id:path}
"""
from __future__ import annotations
from contextlib import AbstractContextManager
from typing import Callable, Final
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
import litellm
from litellm.proxy import proxy_server
from .conftest import normalize # type: ignore[import-not-found]
GET_RESPONSE = {
"object": "list",
"data": [
{
"id": "asst_1",
"object": "assistant",
"name": "Test Assistant",
"model": "gpt-4",
}
],
"first_id": "asst_1",
"last_id": "asst_1",
"has_more": False,
}
CREATE_RESPONSE = {
"id": "asst_new",
"object": "assistant",
"name": "New",
"model": "gpt-4",
"created_at": 0,
}
DELETE_RESPONSE = {"id": "asst_1", "object": "assistant.deleted", "deleted": True}
@pytest.fixture
def patched_assistants(monkeypatch):
router = MagicMock()
router.aget_assistants = AsyncMock(return_value=dict(GET_RESPONSE))
router.acreate_assistants = AsyncMock(return_value=dict(CREATE_RESPONSE))
router.adelete_assistant = AsyncMock(return_value=dict(DELETE_RESPONSE))
monkeypatch.setattr(proxy_server, "llm_router", router)
monkeypatch.setattr(
proxy_server,
"proxy_logging_obj",
MagicMock(
post_call_failure_hook=AsyncMock(), update_request_status=AsyncMock()
),
)
async def _add_data(data, **kwargs):
return data
monkeypatch.setattr(proxy_server, "add_litellm_data_to_request", _add_data)
return router
@pytest.fixture
def no_router(monkeypatch):
monkeypatch.setattr(proxy_server, "llm_router", None)
monkeypatch.setattr(
proxy_server,
"proxy_logging_obj",
MagicMock(
post_call_failure_hook=AsyncMock(), update_request_status=AsyncMock()
),
)
async def _add_data(data, **kwargs):
return data
monkeypatch.setattr(proxy_server, "add_litellm_data_to_request", _add_data)
yield
# ---------------------------------------------------------------------------
# GET /v1/assistants, GET /assistants
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", ["/v1/assistants", "/assistants"])
def test_get_assistants_happy_path(client, auth_as, patched_assistants, path):
"""Pins ``GET /v1/assistants`` and ``GET /assistants``."""
with auth_as():
response = client.get(path)
assert response.status_code == 200
assert normalize(response.json()) == {
"object": "list",
"data": [
{
"id": "<VOLATILE>",
"object": "assistant",
"name": "Test Assistant",
"model": "gpt-4",
}
],
"first_id": "asst_1",
"last_id": "asst_1",
"has_more": False,
}
@pytest.mark.parametrize("path", ["/v1/assistants", "/assistants"])
def test_get_assistants_no_router_error(client, auth_as, no_router, path):
"""Pins ``GET /v1/assistants`` and ``GET /assistants`` (error: no llm_router)."""
with auth_as():
response = client.get(path)
assert response.status_code == 500
assert len(response.content) > 0
# ---------------------------------------------------------------------------
# POST /v1/assistants, POST /assistants
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", ["/v1/assistants", "/assistants"])
def test_create_assistant_happy_path(client, auth_as, patched_assistants, path):
"""Pins ``POST /v1/assistants`` and ``POST /assistants``."""
payload = {"model": "gpt-4", "name": "New"}
with auth_as():
response = client.post(path, json=payload)
assert response.status_code == 200
assert normalize(response.json()) == {
"id": "<VOLATILE>",
"object": "assistant",
"name": "New",
"model": "gpt-4",
"created_at": "<VOLATILE>",
}
@pytest.mark.parametrize("path", ["/v1/assistants", "/assistants"])
def test_create_assistant_no_router_error(client, auth_as, no_router, path):
"""Pins ``POST /v1/assistants`` and ``POST /assistants`` (error: no llm_router)."""
with auth_as():
response = client.post(path, json={"model": "gpt-4"})
assert response.status_code == 500
assert len(response.content) > 0
# ---------------------------------------------------------------------------
# DELETE /v1/assistants/{assistant_id:path}, DELETE /assistants/{assistant_id:path}
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", ["/v1/assistants/asst_1", "/assistants/asst_1"])
def test_delete_assistant_happy_path(client, auth_as, patched_assistants, path):
"""Pins ``DELETE /v1/assistants/{assistant_id:path}`` and ``DELETE /assistants/{assistant_id:path}``."""
with auth_as():
response = client.delete(path)
assert response.status_code == 200
assert normalize(response.json()) == {
"id": "<VOLATILE>",
"object": "assistant.deleted",
"deleted": True,
}
@pytest.mark.parametrize("path", ["/v1/assistants/asst_1", "/assistants/asst_1"])
def test_delete_assistant_no_router_error(client, auth_as, no_router, path):
"""Pins ``DELETE /v1/assistants/{assistant_id:path}`` / ``DELETE /assistants/{assistant_id:path}`` (error)."""
with auth_as():
response = client.delete(path)
assert response.status_code == 500
assert len(response.content) > 0
@pytest.mark.parametrize(
("method", "path", "router_method"),
[
("GET", "/v1/assistants", "aget_assistants"),
("POST", "/v1/assistants", "acreate_assistants"),
("DELETE", "/v1/assistants/asst_1", "adelete_assistant"),
],
)
def test_assistants_routes_keep_the_mapped_provider_status(
client: TestClient,
auth_as: Callable[..., AbstractContextManager[None]],
patched_assistants: MagicMock,
method: str,
path: str,
router_method: str,
) -> None:
"""A litellm NotFoundError carries status 404 and the SDK's ``code=None``; the route answers 404 with its message."""
upstream_error: Final = litellm.NotFoundError(
message="NotFoundError: OpenAIException - Error code: 404",
model="gpt-5.4-mini",
llm_provider="openai",
)
getattr(patched_assistants, router_method).side_effect = upstream_error
with auth_as():
response: Final = client.request(method, path, json={"model": "gpt-5.4-mini"} if method == "POST" else None)
assert response.status_code == 404
assert response.json()["error"] == {
"message": upstream_error.message,
"type": "invalid_request_error",
"param": None,
"code": "404",
}