mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-27 05:15:51 +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")
|
return _error_json(400, "Invalid JSON body")
|
||||||
|
|
||||||
messages = body.get("messages")
|
messages = body.get("messages")
|
||||||
if not messages or not isinstance(messages, list):
|
if not isinstance(messages, list) or len(messages) != 1:
|
||||||
return _error_json(400, "messages field is required and must be a non-empty array")
|
return _error_json(400, "Only a single user message is supported")
|
||||||
|
|
||||||
# Stream not yet supported
|
# Stream not yet supported
|
||||||
if body.get("stream", False):
|
if body.get("stream", False):
|
||||||
return _error_json(400, "stream=true is not supported yet. Set stream=false or omit it.")
|
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
|
message = messages[0]
|
||||||
user_content = None
|
if not isinstance(message, dict) or message.get("role") != "user":
|
||||||
for msg in reversed(messages):
|
return _error_json(400, "Only a single user message is supported")
|
||||||
if msg.get("role") == "user":
|
user_content = message.get("content", "")
|
||||||
user_content = msg.get("content", "")
|
|
||||||
break
|
|
||||||
if user_content is None:
|
|
||||||
return _error_json(400, "messages must contain at least one user message")
|
|
||||||
if isinstance(user_content, list):
|
if isinstance(user_content, list):
|
||||||
# Multi-modal content array — extract text parts
|
# Multi-modal content array — extract text parts
|
||||||
user_content = " ".join(
|
user_content = " ".join(
|
||||||
@ -92,7 +88,9 @@ async def handle_chat_completions(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
agent_loop = request.app["agent_loop"]
|
agent_loop = request.app["agent_loop"]
|
||||||
timeout_s: float = request.app.get("request_timeout", 120.0)
|
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"]
|
session_lock: asyncio.Lock = request.app["session_lock"]
|
||||||
|
|
||||||
logger.info("API request session_key={} content={}", API_SESSION_KEY, user_content[:80])
|
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("/v1/models", handle_models)
|
||||||
app.router.add_get("/health", handle_health)
|
app.router.add_get("/health", handle_health)
|
||||||
return app
|
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,
|
_chat_completion_response,
|
||||||
_error_json,
|
_error_json,
|
||||||
create_app,
|
create_app,
|
||||||
|
handle_chat_completions,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -93,6 +94,73 @@ async def test_stream_true_returns_400(aiohttp_client, app) -> None:
|
|||||||
assert "stream" in body["error"]["message"].lower()
|
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.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
|
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user