diff --git a/app/schemas.py b/app/schemas.py index e057e48..b0c2c20 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -16,10 +16,10 @@ class ChatMessage(BaseModel): class ChatCompletionRequest(BaseModel): model: str messages: list[ChatMessage] = Field(min_length=1) - temperature: float | None = None - max_tokens: int | None = None + temperature: float | None = Field(default=None, ge=-2.0, le=2.0) + max_tokens: int | None = Field(default=None, ge=1) top_p: float | None = None - n: int | None = None + n: int | None = Field(default=None, ge=1) stream: bool = False stop: str | list[str] | None = None presence_penalty: float | None = None diff --git a/tests/unit/test_chat_request_validation.py b/tests/unit/test_chat_request_validation.py new file mode 100644 index 0000000..22b2113 --- /dev/null +++ b/tests/unit/test_chat_request_validation.py @@ -0,0 +1,71 @@ +"""Reject non-positive max_tokens and n at the schema boundary. + +ChatCompletionRequest previously accepted max_tokens=0 and n=0 without +complaint — both values are meaningless (zero tokens to generate, zero +choices to return) and most upstream providers reject them with a +confusing 400. Pydantic now fails closed with a clear validation error +before the request reaches the routing layer. +""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from app.schemas import ChatCompletionRequest + +_BASE = {"model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}]} + + +class TestMaxTokensGuard: + def test_zero_rejected(self): + with pytest.raises(ValidationError, match="max_tokens"): + ChatCompletionRequest.model_validate({**_BASE, "max_tokens": 0}) + + def test_negative_rejected(self): + with pytest.raises(ValidationError, match="max_tokens"): + ChatCompletionRequest.model_validate({**_BASE, "max_tokens": -1}) + + def test_positive_accepted(self): + req = ChatCompletionRequest.model_validate({**_BASE, "max_tokens": 100}) + assert req.max_tokens == 100 + + def test_none_accepted(self): + req = ChatCompletionRequest.model_validate(_BASE) + assert req.max_tokens is None + + +class TestNCountGuard: + def test_zero_rejected(self): + with pytest.raises(ValidationError, match="n"): + ChatCompletionRequest.model_validate({**_BASE, "n": 0}) + + def test_negative_rejected(self): + with pytest.raises(ValidationError, match="n"): + ChatCompletionRequest.model_validate({**_BASE, "n": -5}) + + def test_positive_accepted(self): + req = ChatCompletionRequest.model_validate({**_BASE, "n": 3}) + assert req.n == 3 + + def test_none_accepted(self): + req = ChatCompletionRequest.model_validate(_BASE) + assert req.n is None + + +class TestTemperatureRange: + def test_within_range(self): + req = ChatCompletionRequest.model_validate({**_BASE, "temperature": 0.7}) + assert req.temperature == 0.7 + + def test_negative_two_accepted(self): + req = ChatCompletionRequest.model_validate({**_BASE, "temperature": -2.0}) + assert req.temperature == -2.0 + + def test_above_two_rejected(self): + with pytest.raises(ValidationError, match="temperature"): + ChatCompletionRequest.model_validate({**_BASE, "temperature": 2.1}) + + def test_below_negative_two_rejected(self): + with pytest.raises(ValidationError, match="temperature"): + ChatCompletionRequest.model_validate({**_BASE, "temperature": -2.1})