mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix scheduled-tasks create/update against Prisma input shape
Two bugs surfaced on first real proxy invocation:
1. owner_token: prisma-client-python rejected the bare scalar with
"owner_key: A value is required but not set". Prisma exposes the FK
via the relation field, not the scalar column. Switched create_task
to {"owner_key": {"connect": {"token": owner_token}}}.
2. action_args: prisma-client-python rejected the bare Python dict on
the Json? column ("Invalid argument type. action_args should be of
any of the following types: NullableJsonNullValueInput, Json"). Added
_serialize_json_for_prisma helper (same pattern as
memory_endpoints._serialize_metadata_for_prisma) and applied to both
create and update paths. Read path round-trips back to native Python.
Updated the in-memory fake in tests to mirror real Prisma write
semantics: owner_key.connect.token unfolds to the owner_token column,
and Json strings are deserialised back on read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0f73a091dd
commit
93f8bec80c
2 changed files with 50 additions and 3 deletions
|
|
@ -8,11 +8,26 @@ is required for multi-pod tick safety.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from litellm.proxy.scheduled_tasks.schedule import compute_next_run
|
||||
|
||||
|
||||
def _serialize_json_for_prisma(value: Any) -> Optional[str]:
|
||||
"""
|
||||
Encode a value bound for a Prisma `Json?` column. prisma-client-python
|
||||
rejects raw Python dicts/lists on Json columns
|
||||
(`MissingRequiredValueError` / `DataError`); always json.dumps so the
|
||||
driver hands a string to Postgres jsonb. Read path round-trips back
|
||||
to native Python.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
# Whitelist of fields PATCH can touch. Anything outside this set would let
|
||||
# a buggy caller rewrite scheduling state (status, fired flags, ...).
|
||||
UPDATABLE_FIELDS = frozenset(
|
||||
|
|
@ -62,13 +77,15 @@ async def create_task(
|
|||
) -> Any:
|
||||
return await prisma_client.db.litellm_scheduledtasktable.create(
|
||||
data={
|
||||
"owner_token": owner_token,
|
||||
# Prisma exposes the FK via the relation field (`owner_key`),
|
||||
# not the scalar column. Use connect to satisfy the input type.
|
||||
"owner_key": {"connect": {"token": owner_token}},
|
||||
"user_id": user_id,
|
||||
"team_id": team_id,
|
||||
"agent_id": agent_id,
|
||||
"title": title,
|
||||
"action": action,
|
||||
"action_args": action_args,
|
||||
"action_args": _serialize_json_for_prisma(action_args),
|
||||
"check_prompt": check_prompt,
|
||||
"format_prompt": format_prompt,
|
||||
"schedule_kind": schedule_kind,
|
||||
|
|
@ -133,6 +150,15 @@ async def update_task_for_owner(
|
|||
)
|
||||
if existing is None:
|
||||
return None
|
||||
|
||||
# Mirror the create path: Json columns must be json-encoded for
|
||||
# prisma-client-python.
|
||||
if "action_args" in fields:
|
||||
fields = {
|
||||
**fields,
|
||||
"action_args": _serialize_json_for_prisma(fields["action_args"]),
|
||||
}
|
||||
|
||||
return await prisma_client.db.litellm_scheduledtasktable.update(
|
||||
where={"task_id": task_id},
|
||||
data=fields,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ prisma scheduled-tasks table. The /due test path uses a fake of query_raw +
|
|||
tx() that exercises the same Python-side schedule advance logic.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
|
@ -80,8 +81,23 @@ class _FakeScheduledTaskTable:
|
|||
return [r for r in self.rows if self._matches(r, where)]
|
||||
|
||||
async def create(self, data: Dict[str, Any]) -> MagicMock:
|
||||
# Mirror real Prisma write semantics:
|
||||
# - relation field `owner_key: {connect: {token: X}}` becomes the
|
||||
# scalar `owner_token` column on the row;
|
||||
# - Json columns (`action_args`) come in as JSON strings and are
|
||||
# deserialized back to Python on read.
|
||||
normalized = dict(data)
|
||||
if "owner_key" in normalized:
|
||||
connect = normalized.pop("owner_key", {}).get("connect", {})
|
||||
if "token" in connect:
|
||||
normalized["owner_token"] = connect["token"]
|
||||
if isinstance(normalized.get("action_args"), str):
|
||||
try:
|
||||
normalized["action_args"] = json.loads(normalized["action_args"])
|
||||
except ValueError:
|
||||
pass
|
||||
self._counter += 1
|
||||
row = _make_row(task_id=f"task-{self._counter}", **data)
|
||||
row = _make_row(task_id=f"task-{self._counter}", **normalized)
|
||||
self.rows.append(row)
|
||||
return row
|
||||
|
||||
|
|
@ -106,6 +122,11 @@ class _FakeScheduledTaskTable:
|
|||
for r in self.rows:
|
||||
if r.task_id == where["task_id"]:
|
||||
for k, v in data.items():
|
||||
if k == "action_args" and isinstance(v, str):
|
||||
try:
|
||||
v = json.loads(v)
|
||||
except ValueError:
|
||||
pass
|
||||
setattr(r, k, v)
|
||||
return r
|
||||
raise Exception("Not found")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue