mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(memory): make key globally unique, 409 on any duplicate
Some checks failed
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Some checks failed
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Switches from the compound unique `(key, user_id, team_id)` to a simple
`key @unique`. The compound form silently allowed duplicates when
user_id or team_id was NULL (Postgres treats each NULL as distinct), so
callers could POST the same key repeatedly. Globally-unique key means
one row per key, period — any duplicate create → 409.
- schema.prisma (×3): `key String @unique`, drop `@@unique(...)`.
- initial add_memory_table migration: unique index on (key) only.
- Remove the now-unused follow-up NULLS NOT DISTINCT migration.
- Endpoint error message simplified ("already exists" — no "for this scope").
- Test fake's create() now enforces global key uniqueness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bad0d0cd45
commit
3b9445eeb2
7 changed files with 10 additions and 49 deletions
|
|
@ -14,8 +14,9 @@ CREATE TABLE IF NOT EXISTS "LiteLLM_MemoryTable" (
|
|||
CONSTRAINT "LiteLLM_MemoryTable_pkey" PRIMARY KEY ("memory_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_MemoryTable_key_user_id_team_id_key" ON "LiteLLM_MemoryTable"("key", "user_id", "team_id");
|
||||
-- CreateIndex (key is globally unique — one row per key, period)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_MemoryTable_key_key"
|
||||
ON "LiteLLM_MemoryTable"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "LiteLLM_MemoryTable_user_id_idx" ON "LiteLLM_MemoryTable"("user_id");
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
-- Fix duplicate-key issue on LiteLLM_MemoryTable.
|
||||
--
|
||||
-- Problem: the unique constraint `(key, user_id, team_id)` does not block
|
||||
-- duplicates when `user_id` or `team_id` is NULL, because by default
|
||||
-- Postgres treats each NULL as distinct (ANSI SQL semantics). Callers with
|
||||
-- no team_id could POST the same key repeatedly and get multiple rows.
|
||||
--
|
||||
-- Fix (Postgres 15+): recreate the unique index with NULLS NOT DISTINCT so
|
||||
-- NULL values are treated as equal for uniqueness checks.
|
||||
--
|
||||
-- Note: Prisma has no schema syntax for NULLS NOT DISTINCT (as of writing),
|
||||
-- so this lives as a raw SQL migration. Prisma introspection will still see
|
||||
-- the index as uniquely covering `(key, user_id, team_id)`, matching the
|
||||
-- `@@unique` in schema.prisma.
|
||||
|
||||
-- 1. Deduplicate existing rows: keep only the most-recently-updated row per
|
||||
-- (key, user_id, team_id), using IS NOT DISTINCT FROM so NULL == NULL.
|
||||
DELETE FROM "LiteLLM_MemoryTable" a
|
||||
USING "LiteLLM_MemoryTable" b
|
||||
WHERE a.memory_id <> b.memory_id
|
||||
AND a.key = b.key
|
||||
AND a.user_id IS NOT DISTINCT FROM b.user_id
|
||||
AND a.team_id IS NOT DISTINCT FROM b.team_id
|
||||
AND (
|
||||
a.updated_at < b.updated_at
|
||||
OR (a.updated_at = b.updated_at AND a.memory_id < b.memory_id)
|
||||
);
|
||||
|
||||
-- 2. Drop the old (NULL-distinct) unique index.
|
||||
DROP INDEX IF EXISTS "LiteLLM_MemoryTable_key_user_id_team_id_key";
|
||||
|
||||
-- 3. Recreate with NULLS NOT DISTINCT.
|
||||
CREATE UNIQUE INDEX "LiteLLM_MemoryTable_key_user_id_team_id_key"
|
||||
ON "LiteLLM_MemoryTable"("key", "user_id", "team_id") NULLS NOT DISTINCT;
|
||||
|
|
@ -1229,7 +1229,7 @@ model LiteLLM_ClaudeCodePluginTable {
|
|||
// `metadata` is an optional JSON envelope for structured tags without schema changes.
|
||||
model LiteLLM_MemoryTable {
|
||||
memory_id String @id @default(uuid())
|
||||
key String
|
||||
key String @unique
|
||||
value String
|
||||
metadata Json?
|
||||
user_id String?
|
||||
|
|
@ -1239,7 +1239,6 @@ model LiteLLM_MemoryTable {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@unique([key, user_id, team_id])
|
||||
@@index([user_id])
|
||||
@@index([team_id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,12 +138,12 @@ async def create_memory(
|
|||
try:
|
||||
row = await prisma_client.db.litellm_memorytable.create(data=create_data)
|
||||
except Exception as e:
|
||||
# Unique constraint (key, user_id, team_id) → 409.
|
||||
# Key is globally unique. Any duplicate → 409.
|
||||
msg = str(e)
|
||||
if "Unique" in msg or "unique" in msg or "UniqueViolation" in msg:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Memory with key '{body.key}' already exists for this scope.",
|
||||
detail=f"Memory with key '{body.key}' already exists.",
|
||||
)
|
||||
verbose_proxy_logger.exception("Error creating memory: %s", e)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
|
|||
|
|
@ -1229,7 +1229,7 @@ model LiteLLM_ClaudeCodePluginTable {
|
|||
// `metadata` is an optional JSON envelope for structured tags without schema changes.
|
||||
model LiteLLM_MemoryTable {
|
||||
memory_id String @id @default(uuid())
|
||||
key String
|
||||
key String @unique
|
||||
value String
|
||||
metadata Json?
|
||||
user_id String?
|
||||
|
|
@ -1239,7 +1239,6 @@ model LiteLLM_MemoryTable {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@unique([key, user_id, team_id])
|
||||
@@index([user_id])
|
||||
@@index([team_id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1229,7 +1229,7 @@ model LiteLLM_ClaudeCodePluginTable {
|
|||
// `metadata` is an optional JSON envelope for structured tags without schema changes.
|
||||
model LiteLLM_MemoryTable {
|
||||
memory_id String @id @default(uuid())
|
||||
key String
|
||||
key String @unique
|
||||
value String
|
||||
metadata Json?
|
||||
user_id String?
|
||||
|
|
@ -1239,7 +1239,6 @@ model LiteLLM_MemoryTable {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
|
||||
@@unique([key, user_id, team_id])
|
||||
@@index([user_id])
|
||||
@@index([team_id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,12 +84,9 @@ class _InMemoryMemoryTable:
|
|||
return [r for r in self.rows if self._matches(r, where)]
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> MagicMock:
|
||||
# Key is globally unique.
|
||||
for r in self.rows:
|
||||
if (
|
||||
r.key == data["key"]
|
||||
and r.user_id == data.get("user_id")
|
||||
and r.team_id == data.get("team_id")
|
||||
):
|
||||
if r.key == data["key"]:
|
||||
raise Exception("UniqueViolation: duplicate key")
|
||||
self._counter += 1
|
||||
row = _make_row(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue