chore(container,skills): LRU eviction for owner caches; widen file_purpose Literal

Two cleanups from the /simplify pass:

* ``_CONTAINER_OWNER_CACHE`` and ``_SKILL_CACHE`` now LRU-evict via
  ``OrderedDict.popitem(last=False)`` instead of full ``clear()`` at
  capacity. Full clears converted a steady-state cached workload into a
  periodic full-DB-load oscillation as the cache repopulated from zero
  and cleared again. Reads now ``move_to_end`` so the just-touched
  entry survives the next eviction. Mirrors the pre-existing LRU
  pattern in ``_remember_container_owner``.

* ``LiteLLM_ManagedObjectTable.file_purpose`` Literal now includes
  ``"container"`` so Pydantic validation accepts rows written by the
  ownership store.
This commit is contained in:
user 2026-05-04 22:52:54 +00:00
parent 4fa577810b
commit ec9b84d38c
No known key found for this signature in database
5 changed files with 42 additions and 13 deletions

View File

@ -8,6 +8,7 @@ Used by the transformation layer and skills injection hook.
import os
import time
import uuid
from collections import OrderedDict
from typing import Any, Dict, List, Optional, Tuple
from litellm._logging import verbose_logger
@ -28,7 +29,7 @@ ALLOW_UNOWNED_SKILL_ACCESS_ENV = "LITELLM_ALLOW_UNOWNED_SKILL_ACCESS"
# round-trip per request. Same shape as `_byok_cred_cache` and the container
# ownership cache: (value, monotonic_timestamp). `None` is cached as a true
# negative ("skill does not exist") so repeated misses also avoid the DB.
_SKILL_CACHE: Dict[str, Tuple[Optional[Any], float]] = {}
_SKILL_CACHE: "OrderedDict[str, Tuple[Optional[Any], float]]" = OrderedDict()
_SKILL_CACHE_TTL = 60 # seconds
_SKILL_CACHE_MAX_SIZE = 10000
@ -42,13 +43,18 @@ def _read_skill_cache(skill_id: str) -> Tuple[bool, Optional[Any]]:
if time.monotonic() - timestamp > _SKILL_CACHE_TTL:
_SKILL_CACHE.pop(skill_id, None)
return False, None
_SKILL_CACHE.move_to_end(skill_id)
return True, value
def _write_skill_cache(skill_id: str, skill: Optional[Any]) -> None:
if len(_SKILL_CACHE) >= _SKILL_CACHE_MAX_SIZE:
_SKILL_CACHE.clear()
# LRU eviction (popitem(last=False)) instead of full ``clear()`` —
# see container ownership cache for rationale.
if skill_id in _SKILL_CACHE:
_SKILL_CACHE.move_to_end(skill_id)
_SKILL_CACHE[skill_id] = (skill, time.monotonic())
while len(_SKILL_CACHE) > _SKILL_CACHE_MAX_SIZE:
_SKILL_CACHE.popitem(last=False)
def _invalidate_skill_cache(skill_id: str) -> None:

View File

@ -4609,7 +4609,7 @@ class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
class LiteLLM_ManagedObjectTable(LiteLLMPydanticObjectBase):
unified_object_id: str
model_object_id: str
file_purpose: Literal["batch", "fine-tune", "response"]
file_purpose: Literal["batch", "fine-tune", "response", "container"]
file_object: Union[LiteLLMBatch, LiteLLMFineTuningJob, ResponsesAPIResponse]

View File

@ -26,9 +26,10 @@ _IN_MEMORY_CONTAINER_OWNERS: "OrderedDict[str, str]" = OrderedDict()
# Short-lived cache keeps every container access check from hitting the DB
# (`_get_container_owner` is invoked on retrieve / delete / list / file-content
# paths). Mirrors the `_byok_cred_cache` pattern in mcp_server/server.py:
# (value, monotonic_timestamp) tuples, TTL'd, capped, invalidated by writes.
# A `None` value caches "untracked" so repeated negative lookups also avoid DB.
_CONTAINER_OWNER_CACHE: Dict[str, Tuple[Optional[str], float]] = {}
# (value, monotonic_timestamp) tuples, TTL'd, LRU-evicted at capacity,
# invalidated by writes. A ``None`` value caches "untracked" so repeated
# negative lookups also avoid DB.
_CONTAINER_OWNER_CACHE: "OrderedDict[str, Tuple[Optional[str], float]]" = OrderedDict()
_CONTAINER_OWNER_CACHE_TTL = 60 # seconds
_CONTAINER_OWNER_CACHE_MAX_SIZE = 10000
@ -42,13 +43,20 @@ def _read_container_owner_cache(model_object_id: str) -> Tuple[bool, Optional[st
if time.monotonic() - timestamp > _CONTAINER_OWNER_CACHE_TTL:
_CONTAINER_OWNER_CACHE.pop(model_object_id, None)
return False, None
_CONTAINER_OWNER_CACHE.move_to_end(model_object_id)
return True, value
def _write_container_owner_cache(model_object_id: str, owner: Optional[str]) -> None:
if len(_CONTAINER_OWNER_CACHE) >= _CONTAINER_OWNER_CACHE_MAX_SIZE:
_CONTAINER_OWNER_CACHE.clear()
# LRU eviction (popitem(last=False)) instead of full ``clear()`` — a
# full clear at capacity converts a steady-state cached workload into
# a periodic full-DB-load oscillation as the cache repopulates from
# zero and clears again.
if model_object_id in _CONTAINER_OWNER_CACHE:
_CONTAINER_OWNER_CACHE.move_to_end(model_object_id)
_CONTAINER_OWNER_CACHE[model_object_id] = (owner, time.monotonic())
while len(_CONTAINER_OWNER_CACHE) > _CONTAINER_OWNER_CACHE_MAX_SIZE:
_CONTAINER_OWNER_CACHE.popitem(last=False)
def _invalidate_container_owner_cache(model_object_id: str) -> None:

View File

@ -1228,10 +1228,24 @@ def test_container_owner_cache_expires_after_ttl(monkeypatch):
def test_container_owner_cache_evicts_when_at_capacity(monkeypatch):
"""The cache must not grow unbounded; reaching capacity clears all entries."""
"""The cache must not grow unbounded; reaching capacity LRU-evicts the
oldest entry, not the entire cache."""
monkeypatch.setattr(ownership, "_CONTAINER_OWNER_CACHE_MAX_SIZE", 2)
ownership._write_container_owner_cache("a", "user-a")
ownership._write_container_owner_cache("b", "user-b")
ownership._write_container_owner_cache("c", "user-c")
# Reaching the cap clears everything — the new write is the only survivor.
assert ownership._CONTAINER_OWNER_CACHE.keys() == {"c"}
# ``a`` was the oldest and is dropped; ``b`` and ``c`` survive.
assert list(ownership._CONTAINER_OWNER_CACHE.keys()) == ["b", "c"]
def test_container_owner_cache_read_marks_as_recently_used(monkeypatch):
"""Reading an entry should reset its position so a subsequent eviction
drops a less-recently-used entry instead of the just-touched one."""
monkeypatch.setattr(ownership, "_CONTAINER_OWNER_CACHE_MAX_SIZE", 2)
ownership._write_container_owner_cache("a", "user-a")
ownership._write_container_owner_cache("b", "user-b")
# Touch ``a`` so it becomes the most-recently-used.
ownership._read_container_owner_cache("a")
ownership._write_container_owner_cache("c", "user-c")
# ``b`` is the LRU at this point; ``a`` and ``c`` survive.
assert list(ownership._CONTAINER_OWNER_CACHE.keys()) == ["a", "c"]

View File

@ -537,4 +537,5 @@ def test_skill_cache_evicts_when_at_capacity(monkeypatch):
skills_handler._write_skill_cache("a", Mock())
skills_handler._write_skill_cache("b", Mock())
skills_handler._write_skill_cache("c", Mock())
assert skills_handler._SKILL_CACHE.keys() == {"c"}
# ``a`` was the oldest and is LRU-evicted; ``b`` and ``c`` survive.
assert list(skills_handler._SKILL_CACHE.keys()) == ["b", "c"]