From 58ff2075689b9a3c57bad4a5c45aec1ea72ffe75 Mon Sep 17 00:00:00 2001 From: vaibhavsrv Date: Sat, 19 Sep 2026 19:32:59 +0530 Subject: [PATCH] fix(pixel-chat-context): coerce float and string token limits in context trimmer --- .github/workflows/test-linux.yml | 1 + .../dashboard-api/pixel_chat_context.py | 10 +++ .../test_pixel_chat_context_token_limits.py | 64 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 ods/tests/test_pixel_chat_context_token_limits.py diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index d016236565..7ad1b744df 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -251,6 +251,7 @@ jobs: - name: Service Registry Tests run: | python3 tests/test_pixel_agent_teams_roles_count.py + python3 tests/test_pixel_chat_context_token_limits.py bash tests/test-service-registry.sh - name: Switchboard Bypass Inventory diff --git a/ods/extensions/services/dashboard-api/pixel_chat_context.py b/ods/extensions/services/dashboard-api/pixel_chat_context.py index 0c6e299f5f..75b6dd446c 100644 --- a/ods/extensions/services/dashboard-api/pixel_chat_context.py +++ b/ods/extensions/services/dashboard-api/pixel_chat_context.py @@ -46,6 +46,11 @@ class ContextUsage(_Projection): window: int = Field(ge=1, le=10_000_000) measuredAt: str = Field(min_length=1, max_length=64, pattern=r"^[0-9TZ: .+\-]+$") + @field_validator("used", "window", mode="before") + @classmethod + def _coerce_integer(cls, value): + return int(value) if isinstance(value, float) and value.is_integer() else value + class ContextModel(_Projection): id: str = Field(min_length=1, max_length=512, pattern=r"^[^\x00-\x1f\x7f]+$") @@ -53,6 +58,11 @@ class ContextModel(_Projection): contextWindow: int = Field(ge=1, le=10_000_000) routeFingerprint: str | None = Field(default=None, min_length=64, max_length=64, pattern=r"^[a-f0-9]{64}$") + @field_validator("contextWindow", mode="before") + @classmethod + def _coerce_window(cls, value): + return int(value) if isinstance(value, float) and value.is_integer() else value + class CompactionState(_Projection): status: Literal["idle", "running", "completed", "skipped", "failed", "unknown"] diff --git a/ods/tests/test_pixel_chat_context_token_limits.py b/ods/tests/test_pixel_chat_context_token_limits.py new file mode 100644 index 0000000000..76b6047550 --- /dev/null +++ b/ods/tests/test_pixel_chat_context_token_limits.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Regression test: pixel_chat_context coerces integer float token measurements. + +The baseline configured _Projection with strict=True, causing Pydantic to reject +exact integer floats (such as 100.0 or 2048.0) commonly produced by JSON deserializers +or arithmetic token counting operations with ValidationError. +""" +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "extensions/services/dashboard-api")) + +from pixel_chat_context import public_context + + +class PixelChatContextTokenLimitsTests(unittest.TestCase): + def setUp(self): + self.base_context = { + "schemaVersion": 1, + "status": "ready", + "sessionRevision": "rev-1", + "context": { + "used": 100, + "window": 2048, + "measuredAt": "2026-09-19T10:00:00Z", + }, + "model": { + "id": "model-1", + "provider": "prov-1", + "contextWindow": 2048, + }, + "compaction": {"status": "idle", "count": 0}, + "history": { + "status": "ready", + "revision": "a" * 64, + "acknowledgedMessages": 5, + }, + } + + def test_exact_integer_floats_coerced(self): + # Pass float values for used, window, and contextWindow + data = dict(self.base_context) + data["context"] = dict(data["context"], used=100.0, window=2048.0) + data["model"] = dict(data["model"], contextWindow=2048.0) + + result = public_context(data) + self.assertEqual(result["context"]["used"], 100) + self.assertIsInstance(result["context"]["used"], int) + self.assertEqual(result["context"]["window"], 2048) + self.assertIsInstance(result["context"]["window"], int) + self.assertEqual(result["model"]["contextWindow"], 2048) + self.assertIsInstance(result["model"]["contextWindow"], int) + + def test_non_integer_floats_still_rejected(self): + data = dict(self.base_context) + data["context"] = dict(data["context"], used=100.5) + with self.assertRaises(Exception): + public_context(data) + + +if __name__ == "__main__": + unittest.main()