fix(proxy): serialize model block responses

Signed-off-by: samzong <samzong.lu@gmail.com>
This commit is contained in:
samzong 2026-08-02 10:06:15 -04:00 committed by mateo-berri
parent d44d281d1d
commit 26e71ddc54
3 changed files with 55 additions and 26 deletions

View file

@ -29,6 +29,8 @@ class LiteLLM_ProxyModelTable(LiteLLMPydanticObjectBase):
@model_validator(mode="before")
@classmethod
def check_potential_json_str(cls, values):
if not isinstance(values, dict):
return values
if isinstance(values.get("litellm_params"), str):
try:
values["litellm_params"] = json.loads(values["litellm_params"])

View file

@ -180,6 +180,12 @@ class ModelBlockBody(BaseModel):
model_id: str
class ModelBlockResponse(BaseModel):
model_config = ConfigDict(protected_namespaces=())
model_id: str
blocked: bool
class ModelInfoBlockDetail(BaseModel):
id: str | None = None
blocked: bool | None = None
@ -245,11 +251,6 @@ class TestModelRoutes:
def test_block_then_unblock_persists_to_model_info(
self, client: ManagementClient, resources: ResourceManager
) -> None:
"""The blocked flag's persistence is read back from /model/info, not from the
/model/block response: that route currently returns a non-2xx serialization
envelope even though the DB write lands, so the /model/info read-back is the
authoritative persistence contract and keeps this test valid once the
response shape is fixed."""
model_name = f"e2e-mgmt-model-block-{unique_marker()}"
model_id = _create_db_model(client, resources, model_name)
@ -257,27 +258,25 @@ class TestModelRoutes:
f"{model_name!r} already reports blocked in /model/info before /model/block ran"
)
_ = client.proxy.transport.send(
"/model/block",
headers=client.proxy.transport.master,
json=ModelBlockBody(model_id=model_id),
)
_ = _poll(
client.proxy,
lambda: True if _model_blocked_flag(client, model_id) is True else None,
f"/model/info never reported {model_name!r} blocked after /model/block",
)
_ = client.proxy.transport.send(
"/model/unblock",
headers=client.proxy.transport.master,
json=ModelBlockBody(model_id=model_id),
)
_ = _poll(
client.proxy,
lambda: True if _model_blocked_flag(client, model_id) is not True else None,
f"/model/info never cleared blocked for {model_name!r} after /model/unblock",
)
for action, expected in (("block", True), ("unblock", False)):
response = unwrap(
client.proxy.transport.post(
f"/model/{action}",
headers=client.proxy.transport.master,
json=ModelBlockBody(model_id=model_id),
response_type=ModelBlockResponse,
)
)
assert response.model_id == model_id
assert response.blocked is expected
_ = _poll(
client.proxy,
lambda: True
if _model_blocked_flag(client, model_id) is expected
else None,
f"/model/info never reported blocked={expected} for {model_name!r} "
f"after /model/{action}",
)
class TestTagRoutes:

View file

@ -5,6 +5,7 @@ Tests for backend domain models.
from datetime import datetime
import pytest
from pydantic import BaseModel, TypeAdapter
from litellm.models.access_group import LiteLLM_AccessGroupTable
from litellm.models.budget import (
@ -130,6 +131,33 @@ class TestModel:
assert model.litellm_params == {"model": "gpt-4"}
assert model.model_info == {"team_id": "t1"}
def test_response_type_adapter_accepts_pydantic_row(self):
class PrismaModelRow(BaseModel):
model_id: str
model_name: str
litellm_params: dict[str, str]
model_info: dict[str, str] | None = None
blocked: bool = False
row = PrismaModelRow(
model_id="m1",
model_name="gpt-4",
litellm_params={"model": "gpt-4"},
model_info={"team_id": "t1"},
blocked=True,
)
model = TypeAdapter(LiteLLM_ProxyModelTable | None).validate_python(
row,
from_attributes=True,
)
assert model is not None
assert model.model_id == "m1"
assert model.litellm_params == {"model": "gpt-4"}
assert model.model_info == {"team_id": "t1"}
assert model.blocked is True
def test_team_helpers_none_when_no_model_info(self):
model = LiteLLM_ProxyModelTable(
model_id="m1", model_name="gpt-4", litellm_params={}, model_info=None