mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
fix(memory): typed Prisma error + explicit-null metadata on PUT
Two more greptile threads from the last review: - Unique-violation detection was string-matching "Unique"/"UniqueViolation" in the exception message, fragile across Prisma/driver versions. Now check the typed error `code == "P2002"` first, with string fallback. - PUT could not distinguish "metadata omitted" from "metadata: null" — both parsed as `None`, so callers had no way to clear stored metadata. Switch to Pydantic v2's `model_fields_set` to tell which fields the caller actually sent; explicit null now clears the column. New tests: - explicit null clears metadata - omitted metadata preserves existing value Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55f7655c2a
commit
be7fe9b5f5
2 changed files with 65 additions and 3 deletions
|
|
@ -87,9 +87,20 @@ def _require_prisma():
|
|||
|
||||
|
||||
def _is_unique_violation(exc: Exception) -> bool:
|
||||
"""Best-effort detection of a Prisma unique-constraint violation."""
|
||||
"""
|
||||
Detect a Prisma unique-constraint violation.
|
||||
|
||||
Prefer the typed error code `P2002` from `PrismaClientKnownRequestError`;
|
||||
fall back to string matching so we stay robust across Prisma versions
|
||||
where the typed class may be unavailable or differently named.
|
||||
"""
|
||||
code = getattr(exc, "code", None)
|
||||
if code == "P2002":
|
||||
return True
|
||||
msg = str(exc)
|
||||
return "Unique" in msg or "unique" in msg or "UniqueViolation" in msg
|
||||
return (
|
||||
"P2002" in msg or "Unique" in msg or "unique" in msg or "UniqueViolation" in msg
|
||||
)
|
||||
|
||||
|
||||
def _resolve_scope(
|
||||
|
|
@ -292,10 +303,19 @@ async def upsert_memory(
|
|||
"""
|
||||
prisma_client = _require_prisma()
|
||||
|
||||
# Distinguish "metadata omitted from request" from "metadata: null".
|
||||
# Omitted → don't touch the existing field. Explicit null → clear to SQL NULL.
|
||||
# `model_fields_set` (Pydantic v2) only contains field names the caller
|
||||
# actually sent in the payload.
|
||||
fields_sent = body.model_fields_set
|
||||
metadata_explicit = "metadata" in fields_sent
|
||||
|
||||
data: dict = {}
|
||||
if body.value is not None:
|
||||
data["value"] = body.value
|
||||
if body.metadata is not None:
|
||||
if metadata_explicit:
|
||||
# body.metadata may be None here — Prisma update accepts None on a
|
||||
# nullable Json? field and sets the column to SQL NULL.
|
||||
data["metadata"] = body.metadata
|
||||
if not data:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -427,6 +427,48 @@ class TestMemoryEndpoints:
|
|||
assert resp.json()["value"] == "new"
|
||||
assert len(table.rows) == 1
|
||||
|
||||
def test_put_memory_explicit_null_metadata_clears_field(self):
|
||||
"""PUT with `metadata: null` should clear the metadata column (not silently drop the field)."""
|
||||
table = self.prisma.db.litellm_memorytable
|
||||
table.rows.append(
|
||||
_make_row(
|
||||
memory_id="m1",
|
||||
key="notes",
|
||||
value="v",
|
||||
user_id="user-a",
|
||||
team_id="team-a",
|
||||
metadata={"tag": "old"},
|
||||
)
|
||||
)
|
||||
client = _make_client(_user_auth("user-a", "team-a"))
|
||||
with _patch_prisma(self.prisma):
|
||||
resp = client.put("/v1/memory/notes", json={"metadata": None})
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["metadata"] is None
|
||||
assert table.rows[0].metadata is None
|
||||
|
||||
def test_put_memory_omitted_metadata_preserves_field(self):
|
||||
"""PUT without a metadata field should NOT touch the stored metadata."""
|
||||
table = self.prisma.db.litellm_memorytable
|
||||
table.rows.append(
|
||||
_make_row(
|
||||
memory_id="m1",
|
||||
key="notes",
|
||||
value="old",
|
||||
user_id="user-a",
|
||||
team_id="team-a",
|
||||
metadata={"tag": "keep"},
|
||||
)
|
||||
)
|
||||
client = _make_client(_user_auth("user-a", "team-a"))
|
||||
with _patch_prisma(self.prisma):
|
||||
resp = client.put("/v1/memory/notes", json={"value": "new"})
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["value"] == "new"
|
||||
assert body["metadata"] == {"tag": "keep"}
|
||||
|
||||
def test_put_memory_empty_body_returns_400(self):
|
||||
client = _make_client(_user_auth("user-a", "team-a"))
|
||||
with _patch_prisma(self.prisma):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue