mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-02 17:32:39 +00:00
refactor(api): tighten fixed-session chat input contract
Reject mismatched models and require a single user message so the OpenAI-compatible endpoint reflects the fixed-session nanobot runtime without extra compatibility noise.
This commit is contained in:
parent
5635907e33
commit
55501057ac
@ -69,21 +69,17 @@ async def handle_chat_completions(request: web.Request) -> web.Response:
|
||||
return _error_json(400, "Invalid JSON body")
|
||||
|
||||
messages = body.get("messages")
|
||||
if not messages or not isinstance(messages, list):
|
||||
return _error_json(400, "messages field is required and must be a non-empty array")
|
||||
if not isinstance(messages, list) or len(messages) != 1:
|
||||
return _error_json(400, "Only a single user message is supported")
|
||||
|
||||
# Stream not yet supported
|
||||
if body.get("stream", False):
|
||||
return _error_json(400, "stream=true is not supported yet. Set stream=false or omit it.")
|
||||
|
||||
# Extract last user message — nanobot manages its own multi-turn history
|
||||
user_content = None
|
||||
for msg in reversed(messages):
|
||||
if msg.get("role") == "user":
|
||||
user_content = msg.get("content", "")
|
||||
break
|
||||
if user_content is None:
|
||||
return _error_json(400, "messages must contain at least one user message")
|
||||
message = messages[0]
|
||||
if not isinstance(message, dict) or message.get("role") != "user":
|
||||
return _error_json(400, "Only a single user message is supported")
|
||||
user_content = message.get("content", "")
|
||||
if isinstance(user_content, list):
|
||||
# Multi-modal content array — extract text parts
|
||||
user_content = " ".join(
|
||||
@ -92,7 +88,9 @@ async def handle_chat_completions(request: web.Request) -> web.Response:
|
||||
|
||||
agent_loop = request.app["agent_loop"]
|
||||
timeout_s: float = request.app.get("request_timeout", 120.0)
|
||||
model_name: str = body.get("model") or request.app.get("model_name", "nanobot")
|
||||
model_name: str = request.app.get("model_name", "nanobot")
|
||||
if (requested_model := body.get("model")) and requested_model != model_name:
|
||||
return _error_json(400, f"Only configured model '{model_name}' is available")
|
||||
session_lock: asyncio.Lock = request.app["session_lock"]
|
||||
|
||||
logger.info("API request session_key={} content={}", API_SESSION_KEY, user_content[:80])
|
||||
@ -190,10 +188,3 @@ def create_app(agent_loop, model_name: str = "nanobot", request_timeout: float =
|
||||
app.router.add_get("/v1/models", handle_models)
|
||||
app.router.add_get("/health", handle_health)
|
||||
return app
|
||||
|
||||
|
||||
def run_server(agent_loop, host: str = "127.0.0.1", port: int = 8900,
|
||||
model_name: str = "nanobot", request_timeout: float = 120.0) -> None:
|
||||
"""Create and run the server (blocking)."""
|
||||
app = create_app(agent_loop, model_name=model_name, request_timeout=request_timeout)
|
||||
web.run_app(app, host=host, port=port, print=lambda msg: logger.info(msg))
|
||||
|
||||
@ -14,6 +14,7 @@ from nanobot.api.server import (
|
||||
_chat_completion_response,
|
||||
_error_json,
|
||||
create_app,
|
||||
handle_chat_completions,
|
||||
)
|
||||
|
||||
try:
|
||||
@ -93,6 +94,73 @@ async def test_stream_true_returns_400(aiohttp_client, app) -> None:
|
||||
assert "stream" in body["error"]["message"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_mismatch_returns_400() -> None:
|
||||
request = MagicMock()
|
||||
request.json = AsyncMock(
|
||||
return_value={
|
||||
"model": "other-model",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}
|
||||
)
|
||||
request.app = {
|
||||
"agent_loop": _make_mock_agent(),
|
||||
"model_name": "test-model",
|
||||
"request_timeout": 10.0,
|
||||
"session_lock": asyncio.Lock(),
|
||||
}
|
||||
|
||||
resp = await handle_chat_completions(request)
|
||||
assert resp.status == 400
|
||||
body = json.loads(resp.body)
|
||||
assert "test-model" in body["error"]["message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_user_message_required() -> None:
|
||||
request = MagicMock()
|
||||
request.json = AsyncMock(
|
||||
return_value={
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "previous reply"},
|
||||
],
|
||||
}
|
||||
)
|
||||
request.app = {
|
||||
"agent_loop": _make_mock_agent(),
|
||||
"model_name": "test-model",
|
||||
"request_timeout": 10.0,
|
||||
"session_lock": asyncio.Lock(),
|
||||
}
|
||||
|
||||
resp = await handle_chat_completions(request)
|
||||
assert resp.status == 400
|
||||
body = json.loads(resp.body)
|
||||
assert "single user message" in body["error"]["message"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_user_message_must_have_user_role() -> None:
|
||||
request = MagicMock()
|
||||
request.json = AsyncMock(
|
||||
return_value={
|
||||
"messages": [{"role": "system", "content": "you are a bot"}],
|
||||
}
|
||||
)
|
||||
request.app = {
|
||||
"agent_loop": _make_mock_agent(),
|
||||
"model_name": "test-model",
|
||||
"request_timeout": 10.0,
|
||||
"session_lock": asyncio.Lock(),
|
||||
}
|
||||
|
||||
resp = await handle_chat_completions(request)
|
||||
assert resp.status == 400
|
||||
body = json.loads(resp.body)
|
||||
assert "single user message" in body["error"]["message"].lower()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user