Skip to content

Commit ee2b965

Browse files
authored
adjust config info (#7054)
1 parent a3cc3aa commit ee2b965

2 files changed

Lines changed: 207 additions & 4 deletions

File tree

fastdeploy/entrypoints/openai/api_server.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
StatefulSemaphore,
8181
api_server_logger,
8282
console_logger,
83+
get_version_info,
8384
is_port_available,
8485
retrive_model_from_server,
8586
)
@@ -784,11 +785,60 @@ def config_info() -> Response:
784785

785786
def process_object(obj):
786787
if hasattr(obj, "__dict__"):
787-
# 处理有__dict__属性的对象
788788
return obj.__dict__
789-
return None # 或其他默认处理
789+
if isinstance(obj, (set, frozenset)):
790+
return list(obj)
791+
return str(obj)
790792

791793
cfg_dict = {k: v for k, v in cfg.__dict__.items()}
794+
795+
# Version info
796+
cfg_dict["version_info"] = get_version_info()
797+
798+
# Chat template
799+
cfg_dict["chat_template"] = chat_template
800+
801+
# Server config from args
802+
cfg_dict["server_config"] = {
803+
"host": args.host,
804+
"port": args.port,
805+
"workers": args.workers,
806+
"metrics_port": args.metrics_port,
807+
"controller_port": args.controller_port,
808+
"max_concurrency": args.max_concurrency,
809+
"max_waiting_time": args.max_waiting_time,
810+
"timeout": args.timeout,
811+
"timeout_graceful_shutdown": args.timeout_graceful_shutdown,
812+
"served_model_name": args.served_model_name,
813+
"task": args.task,
814+
"model_config_name": args.model_config_name,
815+
"tokenizer_base_url": args.tokenizer_base_url,
816+
"enable_mm_output": args.enable_mm_output,
817+
"tool_call_parser": args.tool_call_parser,
818+
"tool_parser_plugin": args.tool_parser_plugin,
819+
}
820+
821+
# GPU info
822+
try:
823+
import paddle
824+
825+
from fastdeploy.platforms import current_platform
826+
827+
device_info = {}
828+
device_info["device_type"] = current_platform.device_name
829+
device_info["device_count"] = paddle.device.cuda.device_count()
830+
device_ids = str(cfg.parallel_config.device_ids).split(",") if cfg.parallel_config else ["0"]
831+
first_device = int(device_ids[0].strip()) - 1
832+
props = paddle.device.cuda.get_device_properties(first_device)
833+
device_info["device_name"] = props.name
834+
device_info["device_total_memory"] = props.total_memory
835+
device_info["device_multi_processor_count"] = props.multi_processor_count
836+
device_info["device_major"] = props.major
837+
device_info["device_minor"] = props.minor
838+
cfg_dict["device_info"] = device_info
839+
except Exception:
840+
cfg_dict["device_info"] = None
841+
792842
env_dict = {k: v() for k, v in environment_variables.items()}
793843
cfg_dict["env_config"] = env_dict
794844
result_content = json.dumps(cfg_dict, default=process_object, ensure_ascii=False)

tests/entrypoints/openai/test_metrics_routes.py

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def _build_mock_args():
6666
tokenizer_base_url=None,
6767
dynamic_load_weight=False,
6868
reasoning_parser=None,
69+
task=None,
70+
model_config_name=None,
71+
tool_call_parser=None,
6972
)
7073

7174

@@ -197,7 +200,7 @@ class WithDict:
197200
data = json.loads(resp.body.decode("utf-8"))
198201
# The object with __dict__ becomes its dict; the one without becomes null
199202
assert data.get("with_dict") == {"a": 1}
200-
assert "without_dict" in data and data["without_dict"] is None
203+
assert "without_dict" in data and isinstance(data["without_dict"], str)
201204

202205

203206
def test_metrics_app_routes_when_metrics_port_diff():
@@ -264,5 +267,155 @@ class WithDict:
264267
assert resp2.status_code == 200
265268
data = json.loads(resp2.body.decode("utf-8"))
266269
assert data.get("with_dict") == {"x": 42}
267-
assert "without_dict" in data and data["without_dict"] is None
270+
assert "without_dict" in data and isinstance(data["without_dict"], str)
268271
assert "env_config" in data
272+
273+
274+
def _reload_api_server():
275+
"""Helper: reload api_server with standard mocks, return the module."""
276+
with (
277+
patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args,
278+
patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model,
279+
patch("fastdeploy.entrypoints.chat_utils.load_chat_template") as mock_load_template,
280+
):
281+
mock_parse_args.return_value = _build_mock_args()
282+
mock_retrive_model.return_value = "test-model"
283+
mock_load_template.return_value = None
284+
285+
from fastdeploy.entrypoints.openai import api_server as api_server_mod
286+
287+
api_server = importlib.reload(api_server_mod)
288+
return api_server
289+
290+
291+
def test_config_info_server_config_matches_args():
292+
"""Verify server_config values are populated from args."""
293+
api_server = _reload_api_server()
294+
from types import SimpleNamespace as NS
295+
296+
api_server.llm_engine = NS(cfg=NS())
297+
298+
resp = _get_route(api_server.app, "/config-info").endpoint()
299+
assert resp.status_code == 200
300+
data = json.loads(resp.body.decode("utf-8"))
301+
302+
sc = data["server_config"]
303+
assert sc["host"] == "0.0.0.0"
304+
assert sc["port"] == 8000
305+
assert sc["workers"] == 1
306+
assert sc["metrics_port"] is None
307+
assert sc["controller_port"] == -1
308+
assert sc["max_concurrency"] == 16
309+
assert sc["max_waiting_time"] == -1
310+
assert sc["timeout"] == 0
311+
assert sc["timeout_graceful_shutdown"] == 0
312+
assert sc["served_model_name"] is None
313+
assert sc["task"] is None
314+
assert sc["model_config_name"] is None
315+
assert sc["tokenizer_base_url"] is None
316+
assert sc["enable_mm_output"] is False
317+
assert sc["tool_call_parser"] is None
318+
assert sc["tool_parser_plugin"] is None
319+
320+
321+
def test_config_info_top_level_fields():
322+
"""Verify version_info, chat_template, device_info, env_config all present."""
323+
api_server = _reload_api_server()
324+
from types import SimpleNamespace as NS
325+
326+
api_server.llm_engine = NS(cfg=NS(key="val"))
327+
328+
resp = _get_route(api_server.app, "/config-info").endpoint()
329+
data = json.loads(resp.body.decode("utf-8"))
330+
331+
assert "version_info" in data
332+
assert "chat_template" in data
333+
assert "device_info" in data
334+
assert "env_config" in data
335+
assert isinstance(data["env_config"], dict)
336+
# cfg field should propagate
337+
assert data["key"] == "val"
338+
339+
340+
def test_config_info_process_object_set_and_frozenset():
341+
"""Cover process_object branch for set/frozenset -> list."""
342+
api_server = _reload_api_server()
343+
from types import SimpleNamespace as NS
344+
345+
api_server.llm_engine = NS(
346+
cfg=NS(
347+
my_set={3, 1, 2},
348+
my_frozenset=frozenset(["b", "a"]),
349+
)
350+
)
351+
352+
resp = _get_route(api_server.app, "/config-info").endpoint()
353+
assert resp.status_code == 200
354+
data = json.loads(resp.body.decode("utf-8"))
355+
356+
assert isinstance(data["my_set"], list)
357+
assert sorted(data["my_set"]) == [1, 2, 3]
358+
assert isinstance(data["my_frozenset"], list)
359+
assert sorted(data["my_frozenset"]) == ["a", "b"]
360+
361+
362+
def test_config_info_non_ascii_content():
363+
"""Cover ensure_ascii=False path with unicode in cfg."""
364+
api_server = _reload_api_server()
365+
from types import SimpleNamespace as NS
366+
367+
api_server.llm_engine = NS(cfg=NS(desc="中文描述", emoji="🚀"))
368+
369+
resp = _get_route(api_server.app, "/config-info").endpoint()
370+
assert resp.status_code == 200
371+
raw = resp.body.decode("utf-8")
372+
# Non-ASCII chars should appear directly, not as \uXXXX escapes
373+
assert "中文描述" in raw
374+
assert "🚀" in raw
375+
data = json.loads(raw)
376+
assert data["desc"] == "中文描述"
377+
assert data["emoji"] == "🚀"
378+
379+
380+
def test_config_info_cfg_fields_propagated():
381+
"""Verify that all cfg.__dict__ entries end up in the response."""
382+
api_server = _reload_api_server()
383+
from types import SimpleNamespace as NS
384+
385+
api_server.llm_engine = NS(
386+
cfg=NS(
387+
model_name="Qwen-7B",
388+
max_seq_len=4096,
389+
use_fp16=True,
390+
parallel_config=None,
391+
)
392+
)
393+
394+
resp = _get_route(api_server.app, "/config-info").endpoint()
395+
data = json.loads(resp.body.decode("utf-8"))
396+
397+
assert data["model_name"] == "Qwen-7B"
398+
assert data["max_seq_len"] == 4096
399+
assert data["use_fp16"] is True
400+
assert data["parallel_config"] is None
401+
402+
403+
def test_config_info_nested_objects():
404+
"""Cover process_object with nested custom objects."""
405+
api_server = _reload_api_server()
406+
from types import SimpleNamespace as NS
407+
408+
class Inner:
409+
pass
410+
411+
inner = Inner()
412+
inner.lr = 0.01
413+
inner.steps = 100
414+
415+
api_server.llm_engine = NS(cfg=NS(train_config=inner))
416+
417+
resp = _get_route(api_server.app, "/config-info").endpoint()
418+
assert resp.status_code == 200
419+
data = json.loads(resp.body.decode("utf-8"))
420+
421+
assert data["train_config"] == {"lr": 0.01, "steps": 100}

0 commit comments

Comments
 (0)