From c69859a552b007e4d5def236e46b5a6ed3aa4403 Mon Sep 17 00:00:00 2001 From: Ngo Software <317870757+ngo-software@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:41:16 +0800 Subject: [PATCH] fix(chat): clamp negative token counts in request log row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some upstream providers return negative token counts in malformed usage data. The cost calculation already clamps via _compute_cost_microcents, but the raw input_tokens/output_tokens columns in RequestLog were unprotected — negative values persisted to the DB corrupt analytics dashboards (negative total tokens, negative spend aggregates). Clamp both fields to max(0, value) at extraction time so the log row never carries negative counts regardless of upstream data quality. --- app/routes/chat.py | 4 +- tests/unit/test_negative_token_clamping.py | 88 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_negative_token_clamping.py diff --git a/app/routes/chat.py b/app/routes/chat.py index 1779d44..26236d8 100644 --- a/app/routes/chat.py +++ b/app/routes/chat.py @@ -148,8 +148,8 @@ async def _build_log_row( meta = response.get("_orca_meta", {}) or {} usage = response.get("usage", {}) or {} resolved = actual_resolved or response.get("model") or requested_model - input_t = usage.get("prompt_tokens", 0) or 0 - output_t = usage.get("completion_tokens", 0) or 0 + input_t = max(0, usage.get("prompt_tokens", 0) or 0) + output_t = max(0, usage.get("completion_tokens", 0) or 0) return RequestLog( workspace_id=str(kc.workspace_id), api_key_id=str(kc.key_id), diff --git a/tests/unit/test_negative_token_clamping.py b/tests/unit/test_negative_token_clamping.py new file mode 100644 index 0000000..d76fc69 --- /dev/null +++ b/tests/unit/test_negative_token_clamping.py @@ -0,0 +1,88 @@ +"""Regression: _build_log_row must clamp negative token counts to zero. + +Some upstream providers return negative token counts in malformed usage +data. Without clamping, the RequestLog row persists negative values which +corrupts analytics dashboards (negative total tokens, negative spend +aggregates). The cost calculation already clamps via _compute_cost_microcents, +but the raw token count columns were unprotected.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from app.routes.chat import _build_log_row +from app.schemas import ChatCompletionRequest, ChatMessage +from packages.auth.types import KeyContext + + +def _make_body(**overrides): + defaults = { + "model": "gpt-4o", + "messages": [ChatMessage(role="user", content="hello")], + } + defaults.update(overrides) + return ChatCompletionRequest(**defaults) + + +def _make_kc(): + kc = MagicMock(spec=KeyContext) + kc.workspace_id = "ws-1" + kc.key_id = "key-1" + return kc + + +@pytest.mark.asyncio +async def test_build_log_row_clamps_negative_input_tokens(): + body = _make_body() + kc = _make_kc() + response = { + "model": "gpt-4o", + "usage": {"prompt_tokens": -5, "completion_tokens": 10}, + "_orca_meta": {"provider": "openai", "latency_ms": 100}, + } + log = await _build_log_row( + body=body, kc=kc, response=response, + status_code=200, error_type=None, + started_perf=0, strategy="balanced", + requested_model="gpt-4o", + ) + assert log.input_tokens == 0 + assert log.output_tokens == 10 + + +@pytest.mark.asyncio +async def test_build_log_row_clamps_negative_output_tokens(): + body = _make_body() + kc = _make_kc() + response = { + "model": "gpt-4o", + "usage": {"prompt_tokens": 10, "completion_tokens": -3}, + "_orca_meta": {"provider": "openai", "latency_ms": 100}, + } + log = await _build_log_row( + body=body, kc=kc, response=response, + status_code=200, error_type=None, + started_perf=0, strategy="balanced", + requested_model="gpt-4o", + ) + assert log.input_tokens == 10 + assert log.output_tokens == 0 + + +@pytest.mark.asyncio +async def test_build_log_row_clamps_both_negative(): + body = _make_body() + kc = _make_kc() + response = { + "model": "gpt-4o", + "usage": {"prompt_tokens": -1, "completion_tokens": -1}, + "_orca_meta": {"provider": "openai", "latency_ms": 100}, + } + log = await _build_log_row( + body=body, kc=kc, response=response, + status_code=200, error_type=None, + started_perf=0, strategy="balanced", + requested_model="gpt-4o", + ) + assert log.input_tokens == 0 + assert log.output_tokens == 0