From de4ed7b3555afc9b52797758076da7c6aeab1bc1 Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:07:40 +0530 Subject: [PATCH] Python: answer unsupported DevUI conversation items with a client error Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> --- .../agent_framework_devui/_conversations.py | 10 +++++- .../devui/agent_framework_devui/_server.py | 3 ++ .../packages/devui/tests/devui/test_server.py | 33 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/python/packages/devui/agent_framework_devui/_conversations.py b/python/packages/devui/agent_framework_devui/_conversations.py index 1c8b908b104..e164bc37867 100644 --- a/python/packages/devui/agent_framework_devui/_conversations.py +++ b/python/packages/devui/agent_framework_devui/_conversations.py @@ -35,6 +35,14 @@ _MODEL_OUTPUT_REFUSAL = "refusal" +class ConversationItemValidationError(ValueError): + """An item carried no content that maps to agent framework content. + + Raised instead of a plain ValueError so routes can answer with a client + error for a malformed item while a missing conversation stays 404. + """ + + def _refusal_additional_properties() -> dict[str, Any]: return { "core": {_MODEL_OUTPUT_KIND_KEY: _MODEL_OUTPUT_REFUSAL}, @@ -332,7 +340,7 @@ async def add_items(self, conversation_id: str, items: list[dict[str, Any]]) -> contents.append(agent_content) if not contents: - raise ValueError("Conversation message did not contain any supported content") + raise ConversationItemValidationError("Conversation message did not contain any supported content") chat_msg = Message(role=role, contents=contents) chat_messages.append(chat_msg) diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index bc94dee9a21..30d8ab48318 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -21,6 +21,7 @@ from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles +from ._conversations import ConversationItemValidationError from ._deployment import DeploymentManager from ._discovery import EntityDiscovery from ._executor import AgentFrameworkExecutor @@ -1084,6 +1085,8 @@ async def create_conversation_items(conversation_id: str, request_data: dict[str items = request_data.get("items", []) conv_items = await executor.conversation_store.add_items(conversation_id, items=items) return {"object": "list", "data": [item.model_dump() for item in conv_items]} + except ConversationItemValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e except HTTPException: diff --git a/python/packages/devui/tests/devui/test_server.py b/python/packages/devui/tests/devui/test_server.py index 56055fd137a..1b75e96a376 100644 --- a/python/packages/devui/tests/devui/test_server.py +++ b/python/packages/devui/tests/devui/test_server.py @@ -635,6 +635,39 @@ def test_streaming_response_does_not_hardcode_acao_header(): ) +def test_unsupported_conversation_item_content_returns_client_error(): + """Posting an item whose content parts are all unsupported is a client error on an + existing conversation; 404 stays reserved for a missing conversation.""" + server = _server_with_mock_agent(auth_enabled=False) + app = server.get_app() + + with TestClient(app, base_url="http://127.0.0.1") as client: + created = client.post("/v1/conversations", json={}) + assert created.status_code == 200 + conversation_id = created.json()["id"] + + unsupported = client.post( + f"/v1/conversations/{conversation_id}/items", + json={ + "items": [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_video", "input_video": "data:video/mp4;base64,AAAA"}], + } + ] + }, + ) + assert unsupported.status_code == 400 + assert "supported content" in unsupported.json()["detail"] + + missing = client.post( + "/v1/conversations/does-not-exist/items", + json={"items": [{"type": "message", "role": "user", "content": "hello"}]}, + ) + assert missing.status_code == 404 + + def test_cors_default_does_not_allow_arbitrary_origin_even_on_localhost(): """Default CORS must not echo Access-Control-Allow-Origin to arbitrary origins.