Skip to content

Commit b3db40e

Browse files
yzhan1Yaoming Zhan
andauthored
[Bugfix][Router] Preserve full backend model metadata in /v1/models (#927)
* Preserve full backend model metadata Signed-off-by: Yaoming Zhan <yzhan@Mac.attlocal.net> * Refactor model info known fields constant Signed-off-by: Yaoming Zhan <yzhan@Mac.attlocal.net> --------- Signed-off-by: Yaoming Zhan <yzhan@Mac.attlocal.net> Co-authored-by: Yaoming Zhan <yzhan@Mac.attlocal.net>
1 parent dfe0551 commit b3db40e

4 files changed

Lines changed: 139 additions & 12 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import json
2+
import logging
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from vllm_router.routers.main_router import show_models
8+
from vllm_router.service_discovery import EndpointInfo, ModelInfo
9+
10+
11+
def _build_request():
12+
request = MagicMock()
13+
request.app.state = MagicMock()
14+
request.app.state.external_provider_registry = None
15+
return request
16+
17+
18+
def _build_endpoint(model_payload):
19+
model_id = model_payload["id"]
20+
return EndpointInfo(
21+
url="http://engine",
22+
model_names=[model_id],
23+
Id="engine-1",
24+
added_timestamp=0.0,
25+
model_label=model_id,
26+
sleep=False,
27+
model_info={model_id: ModelInfo.from_dict(model_payload)},
28+
)
29+
30+
31+
def test_model_info_preserves_extra_fields():
32+
payload = {
33+
"id": "test-model",
34+
"object": "model",
35+
"created": 123,
36+
"owned_by": "vllm",
37+
"root": "models/test-model",
38+
"parent": None,
39+
"max_model_len": 8192,
40+
"permission": [{"id": "perm-1", "allow_sampling": True}],
41+
}
42+
43+
model_info = ModelInfo.from_dict(payload)
44+
45+
assert model_info.extra_fields == {
46+
"max_model_len": 8192,
47+
"permission": [{"id": "perm-1", "allow_sampling": True}],
48+
}
49+
assert model_info.to_dict() == payload
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_show_models_returns_full_backend_model_payload(monkeypatch):
54+
payload = {
55+
"id": "rich-model",
56+
"object": "model",
57+
"created": 1774275193,
58+
"owned_by": "vllm",
59+
"root": "models/rich-model",
60+
"parent": None,
61+
"max_model_len": 262144,
62+
"permission": [
63+
{
64+
"id": "modelperm-1",
65+
"object": "model_permission",
66+
"allow_sampling": True,
67+
"allow_logprobs": True,
68+
}
69+
],
70+
}
71+
72+
monkeypatch.setattr(
73+
"vllm_router.routers.main_router.get_service_discovery",
74+
lambda: MagicMock(get_endpoint_info=lambda: [_build_endpoint(payload)]),
75+
)
76+
77+
response = await show_models(_build_request())
78+
79+
assert response.status_code == 200
80+
body = json.loads(response.body)
81+
assert body["data"] == [payload]
82+
83+
84+
@pytest.mark.asyncio
85+
async def test_show_models_does_not_warn_for_preserved_backend_fields(
86+
monkeypatch, caplog
87+
):
88+
payload = {
89+
"id": "rich-model",
90+
"object": "model",
91+
"created": 1774275193,
92+
"owned_by": "vllm",
93+
"root": "models/rich-model",
94+
"parent": None,
95+
"max_model_len": 262144,
96+
"permission": [
97+
{
98+
"id": "modelperm-1",
99+
"object": "model_permission",
100+
"allow_sampling": True,
101+
"allow_logprobs": True,
102+
}
103+
],
104+
}
105+
106+
monkeypatch.setattr(
107+
"vllm_router.routers.main_router.get_service_discovery",
108+
lambda: MagicMock(get_endpoint_info=lambda: [_build_endpoint(payload)]),
109+
)
110+
111+
with caplog.at_level(logging.WARNING, logger="vllm_router.protocols"):
112+
response = await show_models(_build_request())
113+
114+
assert response.status_code == 200
115+
assert "ignored" not in caplog.text

src/vllm_router/protocols.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import time
2-
from typing import List, Optional
2+
from typing import ClassVar, List, Optional
33

44
from pydantic import BaseModel, ConfigDict, Field, model_validator
55

@@ -11,11 +11,12 @@
1111
class OpenAIBaseModel(BaseModel):
1212
# OpenAI API does allow extra fields
1313
model_config = ConfigDict(extra="allow")
14+
_warn_on_extra_fields: ClassVar[bool] = True
1415

1516
@model_validator(mode="before")
1617
@classmethod
1718
def __log_extra_fields__(cls, data):
18-
if isinstance(data, dict):
19+
if cls._warn_on_extra_fields and isinstance(data, dict):
1920
# Get all class field names and their potential aliases
2021
field_names = set()
2122
for field_name, field in cls.model_fields.items():
@@ -43,6 +44,7 @@ class ErrorResponse(OpenAIBaseModel):
4344

4445

4546
class ModelCard(OpenAIBaseModel):
47+
_warn_on_extra_fields: ClassVar[bool] = False
4648
id: str
4749
object: str = "model"
4850
created: int = Field(default_factory=lambda: int(time.time()))

src/vllm_router/routers/main_router.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,7 @@ async def show_models(request: Request):
152152
if model_id in existing_models:
153153
continue
154154

155-
model_card = ModelCard(
156-
id=model_id,
157-
object="model",
158-
created=model_info.created,
159-
owned_by=model_info.owned_by,
160-
parent=model_info.parent,
161-
)
155+
model_card = ModelCard(**model_info.to_dict())
162156
model_cards.append(model_card)
163157
existing_models.add(model_id)
164158

src/vllm_router/service_discovery.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import time
2121
import uuid
2222
from dataclasses import dataclass
23-
from typing import Dict, List, Optional, Set
23+
from typing import Any, Dict, List, Optional, Set
2424

2525
import aiohttp
2626
import requests
@@ -32,6 +32,16 @@
3232
logger = init_logger(__name__)
3333

3434
_global_service_discovery: "Optional[ServiceDiscovery]" = None
35+
_MODEL_INFO_KNOWN_FIELDS = frozenset(
36+
{
37+
"id",
38+
"object",
39+
"created",
40+
"owned_by",
41+
"root",
42+
"parent",
43+
}
44+
)
3545

3646

3747
class ServiceDiscoveryType(enum.Enum):
@@ -51,6 +61,7 @@ class ModelInfo:
5161
root: Optional[str] = None
5262
parent: Optional[str] = None
5363
is_adapter: bool = False
64+
extra_fields: Dict[str, Any] = None
5465

5566
@classmethod
5667
def from_dict(cls, data: Dict) -> "ModelInfo":
@@ -63,19 +74,24 @@ def from_dict(cls, data: Dict) -> "ModelInfo":
6374
root=data.get("root", None),
6475
parent=data.get("parent", None),
6576
is_adapter=data.get("parent") is not None,
77+
extra_fields={
78+
k: v for k, v in data.items() if k not in _MODEL_INFO_KNOWN_FIELDS
79+
},
6680
)
6781

6882
def to_dict(self) -> Dict:
6983
"""Convert the ModelInfo instance to a dictionary."""
70-
return {
84+
data = {
7185
"id": self.id,
7286
"object": self.object,
7387
"created": self.created,
7488
"owned_by": self.owned_by,
7589
"root": self.root,
7690
"parent": self.parent,
77-
"is_adapter": self.is_adapter,
7891
}
92+
if self.extra_fields:
93+
data.update(self.extra_fields)
94+
return data
7995

8096

8197
@dataclass

0 commit comments

Comments
 (0)