Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions python/packages/devui/agent_framework_devui/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from typing import Any, cast

from fastapi import FastAPI, HTTPException, Request

from ._conversations import ConversationItemValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
Expand Down Expand Up @@ -1084,6 +1086,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:
Expand Down
33 changes: 33 additions & 0 deletions python/packages/devui/tests/devui/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading