diff --git a/changes/11505.fix.md b/changes/11505.fix.md new file mode 100644 index 00000000000..61cbd99c3e8 --- /dev/null +++ b/changes/11505.fix.md @@ -0,0 +1 @@ +Fix `ResourceSlot.to_humanized()` to fall back to a guessed unit per slot instead of raising on missing slot type entries, preventing 500 errors from clobbering user-facing 4xx responses during error formatting. diff --git a/src/ai/backend/common/types.py b/src/ai/backend/common/types.py index 695a3bbd0d7..2dedb80dc7c 100644 --- a/src/ai/backend/common/types.py +++ b/src/ai/backend/common/types.py @@ -1223,14 +1223,17 @@ def from_user_input( return cls(data) def to_humanized(self, slot_types: Mapping[str, Any]) -> Mapping[str, str]: - try: - return { - k: type(self)._humanize_value(Decimal(v), slot_types[k]) - for k, v in self.data.items() - if v is not None - } - except KeyError as e: - raise ValueError(f"Unknown slot type: {e.args[0]!r}") from e + def _resolve_unit(key: str) -> Any: + try: + return slot_types[key] + except KeyError: + return type(self)._guess_slot_type(key) + + return { + k: type(self)._humanize_value(Decimal(v), _resolve_unit(k)) + for k, v in self.data.items() + if v is not None + } @classmethod def from_json(cls, obj: Mapping[str, Any]) -> ResourceSlot: diff --git a/tests/unit/common/test_types.py b/tests/unit/common/test_types.py index ca583d710d2..5affea474f6 100644 --- a/tests/unit/common/test_types.py +++ b/tests/unit/common/test_types.py @@ -257,6 +257,30 @@ def test_resource_slot_serialization() -> None: assert r2["b"] == Decimal(0) +def test_resource_slot_to_humanized_falls_back_for_unknown_slot() -> None: + # Missing keys fall back to the guessed unit instead of raising. + r = ResourceSlot({ + "cpu": "1", + "mem": str(Decimal(2 * (2**30))), + "cuda.device": "1", + "cuda.mem": str(Decimal(1 * (2**30))), + }) + slot_types: dict[str, str] = {"cpu": "count", "mem": "bytes"} + humanized = r.to_humanized(slot_types) + assert humanized == { + "cpu": "1", + "mem": "2g", + "cuda.device": "1", + "cuda.mem": "1g", + } + + +def test_resource_slot_to_humanized_with_empty_slot_types() -> None: + r = ResourceSlot({"cuda.device": "2", "cuda.mem": str(Decimal(2**30))}) + humanized = r.to_humanized({}) + assert humanized == {"cuda.device": "2", "cuda.mem": "1g"} + + def test_resource_slot_serialization_prevent_scientific_notation() -> None: r1 = ResourceSlot({"a": "2E+1", "b": "200"}) assert r1.to_json()["a"] == "20"