[OpenAI] Log raw request payload for --log-requests (#20605)
This commit is contained in:
@@ -84,6 +84,11 @@ class OpenAIServingBase(ABC):
|
|||||||
if error_msg:
|
if error_msg:
|
||||||
return self.create_error_response(error_msg)
|
return self.create_error_response(error_msg)
|
||||||
|
|
||||||
|
# Log the raw OpenAI request payload before conversion to tokenized form.
|
||||||
|
request_logger = self.tokenizer_manager.request_logger
|
||||||
|
if request_logger.log_requests and request_logger.log_requests_level >= 2:
|
||||||
|
request_logger.log_openai_received_request(request, request=raw_request)
|
||||||
|
|
||||||
# Convert to internal format
|
# Convert to internal format
|
||||||
adapted_request, processed_request = self._convert_to_internal_request(
|
adapted_request, processed_request = self._convert_to_internal_request(
|
||||||
request, raw_request
|
request, raw_request
|
||||||
|
|||||||
@@ -130,6 +130,34 @@ class RequestLogger:
|
|||||||
decoded = tokenizer.decode(obj.input_ids, skip_special_tokens=False)
|
decoded = tokenizer.decode(obj.input_ids, skip_special_tokens=False)
|
||||||
obj.text = decoded
|
obj.text = decoded
|
||||||
|
|
||||||
|
def log_openai_received_request(
|
||||||
|
self,
|
||||||
|
obj: Any,
|
||||||
|
request: Optional["fastapi.Request"] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log the raw OpenAI request payload before request adaptation/tokenization."""
|
||||||
|
max_length, _, _ = self.metadata
|
||||||
|
max_length = max_length if max_length is not None else 2048
|
||||||
|
headers = _extract_whitelisted_headers(request)
|
||||||
|
|
||||||
|
if hasattr(obj, "model_dump"):
|
||||||
|
obj_to_log = obj.model_dump(exclude_none=True)
|
||||||
|
else:
|
||||||
|
obj_to_log = obj
|
||||||
|
|
||||||
|
if self.log_requests_format == "json":
|
||||||
|
log_data = {
|
||||||
|
"obj": _transform_data_for_logging(obj_to_log, max_length=max_length),
|
||||||
|
}
|
||||||
|
if headers:
|
||||||
|
log_data["headers"] = headers
|
||||||
|
log_json(self.targets, "request.received.openai", log_data)
|
||||||
|
else:
|
||||||
|
headers_str = f", headers={headers}" if headers else ""
|
||||||
|
self._log(
|
||||||
|
f"Receive OpenAI: obj={_dataclass_to_string_truncated(obj_to_log, max_length)}{headers_str}"
|
||||||
|
)
|
||||||
|
|
||||||
def log_finished_request(
|
def log_finished_request(
|
||||||
self,
|
self,
|
||||||
obj: Union["GenerateReqInput", "EmbeddingReqInput"],
|
obj: Union["GenerateReqInput", "EmbeddingReqInput"],
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ register_amd_ci(est_time=120, suite="nightly-amd-1-gpu", nightly=True)
|
|||||||
TEST_ROUTING_KEY = "test-routing-key-12345"
|
TEST_ROUTING_KEY = "test-routing-key-12345"
|
||||||
TEST_CUSTOM_HEADER_NAME = "X-Test-Header"
|
TEST_CUSTOM_HEADER_NAME = "X-Test-Header"
|
||||||
TEST_CUSTOM_HEADER_VALUE = "test-header-value-67890"
|
TEST_CUSTOM_HEADER_VALUE = "test-header-value-67890"
|
||||||
|
TEST_MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||||
|
|
||||||
|
|
||||||
class BaseTestRequestLogger:
|
class BaseTestRequestLogger:
|
||||||
@@ -54,7 +55,7 @@ class BaseTestRequestLogger:
|
|||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
cls.process = popen_launch_server(
|
cls.process = popen_launch_server(
|
||||||
"Qwen/Qwen3-0.6B",
|
TEST_MODEL_NAME,
|
||||||
DEFAULT_URL_FOR_TEST,
|
DEFAULT_URL_FOR_TEST,
|
||||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
other_args=other_args,
|
other_args=other_args,
|
||||||
@@ -77,6 +78,32 @@ class BaseTestRequestLogger:
|
|||||||
def _verify_logs(self, content: str, source_name: str):
|
def _verify_logs(self, content: str, source_name: str):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _verify_openai_logs(self, content: str, source_name: str):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _wait_until_verified(
|
||||||
|
self,
|
||||||
|
verify_fn,
|
||||||
|
get_content_fn,
|
||||||
|
source_name: str,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
interval: float = 0.1,
|
||||||
|
):
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
content = get_content_fn()
|
||||||
|
try:
|
||||||
|
verify_fn(content, source_name)
|
||||||
|
return
|
||||||
|
except AssertionError as err:
|
||||||
|
last_error = err
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
if last_error is not None:
|
||||||
|
raise last_error
|
||||||
|
|
||||||
def test_logging(self):
|
def test_logging(self):
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
DEFAULT_URL_FOR_TEST + "/generate",
|
DEFAULT_URL_FOR_TEST + "/generate",
|
||||||
@@ -88,16 +115,46 @@ class BaseTestRequestLogger:
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
time.sleep(1)
|
self._wait_until_verified(
|
||||||
|
self._verify_logs,
|
||||||
stdout_content = self.stdout.getvalue() + self.stderr.getvalue()
|
lambda: self.stdout.getvalue() + self.stderr.getvalue(),
|
||||||
self._verify_logs(stdout_content, "stdout")
|
"stdout",
|
||||||
|
)
|
||||||
|
self._wait_until_verified(
|
||||||
|
self._verify_logs,
|
||||||
|
lambda: "".join(f.read_text() for f in Path(self.temp_dir).glob("*.log")),
|
||||||
|
"log files",
|
||||||
|
)
|
||||||
|
|
||||||
log_files = list(Path(self.temp_dir).glob("*.log"))
|
log_files = list(Path(self.temp_dir).glob("*.log"))
|
||||||
self.assertGreater(len(log_files), 0, "No log files found in temp directory")
|
self.assertGreater(len(log_files), 0, "No log files found in temp directory")
|
||||||
|
|
||||||
file_content = "".join(f.read_text() for f in log_files)
|
def test_openai_chat_logging(self):
|
||||||
self._verify_logs(file_content, "log files")
|
response = requests.post(
|
||||||
|
DEFAULT_URL_FOR_TEST + "/v1/chat/completions",
|
||||||
|
json={
|
||||||
|
"model": TEST_MODEL_NAME,
|
||||||
|
"messages": [{"role": "user", "content": "hello request logger"}],
|
||||||
|
"max_tokens": 8,
|
||||||
|
"temperature": 0,
|
||||||
|
},
|
||||||
|
headers=self.request_headers,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self._wait_until_verified(
|
||||||
|
self._verify_openai_logs,
|
||||||
|
lambda: self.stdout.getvalue() + self.stderr.getvalue(),
|
||||||
|
"stdout",
|
||||||
|
)
|
||||||
|
self._wait_until_verified(
|
||||||
|
self._verify_openai_logs,
|
||||||
|
lambda: "".join(f.read_text() for f in Path(self.temp_dir).glob("*.log")),
|
||||||
|
"log files",
|
||||||
|
)
|
||||||
|
|
||||||
|
log_files = list(Path(self.temp_dir).glob("*.log"))
|
||||||
|
self.assertGreater(len(log_files), 0, "No log files found in temp directory")
|
||||||
|
|
||||||
|
|
||||||
class TestRequestLoggerText(BaseTestRequestLogger, CustomTestCase):
|
class TestRequestLoggerText(BaseTestRequestLogger, CustomTestCase):
|
||||||
@@ -113,6 +170,17 @@ class TestRequestLoggerText(BaseTestRequestLogger, CustomTestCase):
|
|||||||
"x-smg-routing-key", content, f"Header name not found in {source_name}"
|
"x-smg-routing-key", content, f"Header name not found in {source_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _verify_openai_logs(self, content: str, source_name: str):
|
||||||
|
self.assertIn(
|
||||||
|
"Receive OpenAI:", content, f"OpenAI receive log not found in {source_name}"
|
||||||
|
)
|
||||||
|
self.assertIn("'messages':", content, f"Messages not found in {source_name}")
|
||||||
|
self.assertIn(
|
||||||
|
"hello request logger",
|
||||||
|
content,
|
||||||
|
f"OpenAI user prompt not found in {source_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
|
class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
|
||||||
log_requests_format = "json"
|
log_requests_format = "json"
|
||||||
@@ -123,7 +191,10 @@ class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
|
|||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
if not line.strip() or not line.startswith("{"):
|
if not line.strip() or not line.startswith("{"):
|
||||||
continue
|
continue
|
||||||
data = json.loads(line)
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
rid = data.get("rid", "")
|
rid = data.get("rid", "")
|
||||||
if rid.startswith("HEALTH_CHECK"):
|
if rid.startswith("HEALTH_CHECK"):
|
||||||
@@ -152,6 +223,34 @@ class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
|
|||||||
finished_found, f"request.finished event not found in {source_name}"
|
finished_found, f"request.finished event not found in {source_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _verify_openai_logs(self, content: str, source_name: str):
|
||||||
|
openai_received_found = False
|
||||||
|
for line in content.splitlines():
|
||||||
|
if not line.strip() or not line.startswith("{"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if data.get("event") != "request.received.openai":
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj = data.get("obj", {})
|
||||||
|
self.assertEqual(obj.get("model"), TEST_MODEL_NAME)
|
||||||
|
self.assertIsInstance(obj.get("messages"), list)
|
||||||
|
self.assertGreater(len(obj.get("messages")), 0)
|
||||||
|
self.assertEqual(obj["messages"][0].get("content"), "hello request logger")
|
||||||
|
self.assertEqual(
|
||||||
|
data.get("headers", {}).get("x-smg-routing-key"), TEST_ROUTING_KEY
|
||||||
|
)
|
||||||
|
openai_received_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
openai_received_found,
|
||||||
|
f"request.received.openai event not found in {source_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCustomHeaderViaEnvVar(BaseTestRequestLogger, CustomTestCase):
|
class TestCustomHeaderViaEnvVar(BaseTestRequestLogger, CustomTestCase):
|
||||||
"""Test that custom headers can be added via SGLANG_LOG_REQUEST_HEADERS env var."""
|
"""Test that custom headers can be added via SGLANG_LOG_REQUEST_HEADERS env var."""
|
||||||
@@ -187,6 +286,21 @@ class TestCustomHeaderViaEnvVar(BaseTestRequestLogger, CustomTestCase):
|
|||||||
f"Default header value not found in {source_name}",
|
f"Default header value not found in {source_name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _verify_openai_logs(self, content: str, source_name: str):
|
||||||
|
self.assertIn(
|
||||||
|
"Receive OpenAI:", content, f"OpenAI receive log not found in {source_name}"
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
TEST_CUSTOM_HEADER_NAME.lower(),
|
||||||
|
content,
|
||||||
|
f"Custom header name not found in {source_name}",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
TEST_CUSTOM_HEADER_VALUE,
|
||||||
|
content,
|
||||||
|
f"Custom header value not found in {source_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user