From c2c06d60cedbb73bdf7497588ca6605ac582a505 Mon Sep 17 00:00:00 2001 From: jiangjiajun Date: Thu, 24 Sep 2026 20:23:41 +0800 Subject: [PATCH] [Serving] Remove config-info endpoint from metrics server The /config-info endpoint on the metrics server dumps the full engine config (cfg.__dict__) and all environment variables as JSON, leaking sensitive configuration and secrets. Remove the endpoint and its tests. The cache_config_info Prometheus metric is unchanged. --- fastdeploy/entrypoints/openai/api_server.py | 75 ----- .../test_ernie_03b_golang_router.py | 7 - ..._ernie_03b_pd_golang_router_v1_rdma_tp1.py | 7 - tests/e2e/test_ernie_03b_pd_router_v1_ipc.py | 7 - ...rnie_03b_pd_router_v1_rdma_global_cache.py | 7 - .../test_ernie_03b_pd_router_v1_rdma_tp1.py | 7 - .../test_ernie_03b_pd_router_v1_rdma_tp2.py | 7 - tests/e2e/test_ernie_03b_router.py | 7 - tests/entrypoints/openai/test_api_server.py | 8 - .../entrypoints/openai/test_metrics_routes.py | 279 +----------------- 10 files changed, 1 insertion(+), 410 deletions(-) diff --git a/fastdeploy/entrypoints/openai/api_server.py b/fastdeploy/entrypoints/openai/api_server.py index 217fcfaae8e..8683b28803e 100644 --- a/fastdeploy/entrypoints/openai/api_server.py +++ b/fastdeploy/entrypoints/openai/api_server.py @@ -14,7 +14,6 @@ """ import asyncio -import json import os import signal import threading @@ -80,7 +79,6 @@ StatefulSemaphore, api_server_logger, console_logger, - get_version_info, is_port_available, retrive_model_from_server, ) @@ -783,79 +781,6 @@ async def metrics(): return Response(metrics_text, media_type="text/plain") -@metrics_app.get("/config-info") -@tracing.trace_span("config-info") -def config_info() -> Response: - """ - Get the current configuration of the API server. - """ - global llm_engine - if llm_engine is None: - return Response("Engine not loaded", status_code=500) - cfg = llm_engine.cfg - - def process_object(obj): - if hasattr(obj, "__dict__"): - return obj.__dict__ - if isinstance(obj, (set, frozenset)): - return list(obj) - return str(obj) - - cfg_dict = {k: v for k, v in cfg.__dict__.items()} - - # Version info - cfg_dict["version_info"] = get_version_info() - - # Chat template - cfg_dict["chat_template"] = chat_template - - # Server config from args - cfg_dict["server_config"] = { - "host": args.host, - "port": args.port, - "workers": args.workers, - "metrics_port": args.metrics_port, - "controller_port": args.controller_port, - "max_concurrency": args.max_concurrency, - "max_waiting_time": args.max_waiting_time, - "timeout": args.timeout, - "timeout_graceful_shutdown": args.timeout_graceful_shutdown, - "served_model_name": args.served_model_name, - "task": args.task, - "model_config_name": args.model_config_name, - "tokenizer_base_url": args.tokenizer_base_url, - "enable_mm_output": args.enable_mm_output, - "tool_call_parser": args.tool_call_parser, - "tool_parser_plugin": args.tool_parser_plugin, - } - - # GPU info - try: - import paddle - - from fastdeploy.platforms import current_platform - - device_info = {} - device_info["device_type"] = current_platform.device_name - device_info["device_count"] = paddle.device.cuda.device_count() - device_ids = str(cfg.parallel_config.device_ids).split(",") if cfg.parallel_config else ["0"] - first_device = int(device_ids[0].strip()) - 1 - props = paddle.device.cuda.get_device_properties(first_device) - device_info["device_name"] = props.name - device_info["device_total_memory"] = props.total_memory - device_info["device_multi_processor_count"] = props.multi_processor_count - device_info["device_major"] = props.major - device_info["device_minor"] = props.minor - cfg_dict["device_info"] = device_info - except Exception: - cfg_dict["device_info"] = None - - env_dict = {k: v() for k, v in environment_variables.items()} - cfg_dict["env_config"] = env_dict - result_content = json.dumps(cfg_dict, default=process_object, ensure_ascii=False) - return Response(result_content, media_type="application/json") - - def run_metrics_server(): """ run metrics server diff --git a/tests/e2e/golang_router/test_ernie_03b_golang_router.py b/tests/e2e/golang_router/test_ernie_03b_golang_router.py index 6a6f8e4d7d6..bce792ade72 100644 --- a/tests/e2e/golang_router/test_ernie_03b_golang_router.py +++ b/tests/e2e/golang_router/test_ernie_03b_golang_router.py @@ -257,13 +257,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/golang_router/test_ernie_03b_pd_golang_router_v1_rdma_tp1.py b/tests/e2e/golang_router/test_ernie_03b_pd_golang_router_v1_rdma_tp1.py index 5e130c519bd..4fa71792820 100644 --- a/tests/e2e/golang_router/test_ernie_03b_pd_golang_router_v1_rdma_tp1.py +++ b/tests/e2e/golang_router/test_ernie_03b_pd_golang_router_v1_rdma_tp1.py @@ -262,13 +262,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/test_ernie_03b_pd_router_v1_ipc.py b/tests/e2e/test_ernie_03b_pd_router_v1_ipc.py index 55074c97481..646115e5cc2 100644 --- a/tests/e2e/test_ernie_03b_pd_router_v1_ipc.py +++ b/tests/e2e/test_ernie_03b_pd_router_v1_ipc.py @@ -262,13 +262,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_global_cache.py b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_global_cache.py index 71ee1607a21..00f6989a031 100644 --- a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_global_cache.py +++ b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_global_cache.py @@ -373,13 +373,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp1.py b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp1.py index dd4aa5722b4..1f5ae85356a 100644 --- a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp1.py +++ b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp1.py @@ -266,13 +266,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp2.py b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp2.py index 0bbc8186a54..f2e91b002d1 100644 --- a/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp2.py +++ b/tests/e2e/test_ernie_03b_pd_router_v1_rdma_tp2.py @@ -270,13 +270,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/e2e/test_ernie_03b_router.py b/tests/e2e/test_ernie_03b_router.py index 17d9b05dafe..53b096abe28 100644 --- a/tests/e2e/test_ernie_03b_router.py +++ b/tests/e2e/test_ernie_03b_router.py @@ -265,13 +265,6 @@ def headers(): return {"Content-Type": "application/json"} -def test_metrics_config(metrics_url): - timeout = 600 - url = metrics_url.replace("metrics", "config-info") - res = requests.get(url, timeout=timeout) - assert res.status_code == 200 - - def send_request(url, payload, timeout=60): """ 发送请求到指定的URL,并返回响应结果。 diff --git a/tests/entrypoints/openai/test_api_server.py b/tests/entrypoints/openai/test_api_server.py index 871b7c3f5b6..7cb64e3443f 100644 --- a/tests/entrypoints/openai/test_api_server.py +++ b/tests/entrypoints/openai/test_api_server.py @@ -819,14 +819,6 @@ def test_control_scheduler(): scheduler2.update_config.assert_not_called() -def test_config_info(): - args = _build_args() - with _patch_common_imports(args): - api_server = _reload_api_server(args) - api_server.llm_engine = None - assert api_server.config_info().status_code == 500 - - # ── /v1/abort_requests ────────────────────────────────────────────── diff --git a/tests/entrypoints/openai/test_metrics_routes.py b/tests/entrypoints/openai/test_metrics_routes.py index eecdef22188..a9f16d586f4 100644 --- a/tests/entrypoints/openai/test_metrics_routes.py +++ b/tests/entrypoints/openai/test_metrics_routes.py @@ -21,7 +21,6 @@ import asyncio import importlib -import json import os import tempfile from types import SimpleNamespace @@ -86,7 +85,7 @@ def _get_route(app, path: str): return None -def test_metrics_and_config_routes(): +def test_metrics_route(): with ( patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args, patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model, @@ -103,7 +102,6 @@ def test_metrics_and_config_routes(): api_server = importlib.reload(api_server_mod) - # 1) /metrics from fastdeploy.metrics import metrics as metrics_mod if not hasattr(metrics_mod.main_process_metrics, "cache_config_info"): @@ -119,89 +117,6 @@ def test_metrics_and_config_routes(): ) assert "fastdeploy:" in metrics_text - # 2) /config-info - # Inject a fake engine so /config-info returns 200 - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS(dummy="value")) - - cfg_route = _get_route(api_server.app, "/config-info") - assert cfg_route is not None - - cfg_resp = cfg_route.endpoint() - assert cfg_resp.status_code == 200 - assert getattr(cfg_resp, "media_type", "").startswith("application/json") - cfg_text = ( - cfg_resp.body.decode("utf-8") if isinstance(cfg_resp.body, (bytes, bytearray)) else str(cfg_resp.body) - ) - data = json.loads(cfg_text) - assert isinstance(data, dict) - assert "env_config" in data - - -def test_config_info_engine_not_loaded_returns_500(): - # Ensure we take the branch where llm_engine is None - with ( - patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args, - patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model, - patch("fastdeploy.entrypoints.chat_utils.load_chat_template") as mock_load_template, - ): - mock_parse_args.return_value = _build_mock_args() - mock_retrive_model.return_value = "test-model" - mock_load_template.return_value = None - - from fastdeploy.entrypoints.openai import api_server as api_server_mod - - api_server = importlib.reload(api_server_mod) - - # Fresh import sets llm_engine to None - cfg_route = _get_route(api_server.app, "/config-info") - assert cfg_route is not None - - resp = cfg_route.endpoint() - assert resp.status_code == 500 - # message body is simple text - assert b"Engine not loaded" in getattr(resp, "body", b"") - - -def test_config_info_process_object_branches(): - # Cover forcing json default() to handle - # both an object with __dict__ and one without. - with ( - patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args, - patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model, - patch("fastdeploy.entrypoints.chat_utils.load_chat_template") as mock_load_template, - ): - mock_parse_args.return_value = _build_mock_args() - mock_retrive_model.return_value = "test-model" - mock_load_template.return_value = None - - from fastdeploy.entrypoints.openai import api_server as api_server_mod - - api_server = importlib.reload(api_server_mod) - - # Build a cfg with values that exercise both branches of process_object() - class WithDict: - pass - - has_dict = WithDict() - has_dict.a = 1 - no_dict = object() - - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS(with_dict=has_dict, without_dict=no_dict)) - - cfg_route = _get_route(api_server.app, "/config-info") - assert cfg_route is not None - - resp = cfg_route.endpoint() - assert resp.status_code == 200 - data = json.loads(resp.body.decode("utf-8")) - # The object with __dict__ becomes its dict; the one without becomes null - assert data.get("with_dict") == {"a": 1} - assert "without_dict" in data and isinstance(data["without_dict"], str) - def test_metrics_app_routes_when_metrics_port_diff(): # Cover metrics_app '/metrics' @@ -227,195 +142,3 @@ def test_metrics_app_routes_when_metrics_port_diff(): assert getattr(resp, "media_type", "").startswith("text/plain") text = resp.body.decode("utf-8") if isinstance(resp.body, (bytes, bytearray)) else str(resp.body) assert "fastdeploy:" in text - - -def test_metrics_app_config_info_branches(): - # Cover metrics_app '/config-info' 500 branch and success path - # including process_object branches and response - with ( - patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args, - patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model, - patch("fastdeploy.entrypoints.chat_utils.load_chat_template") as mock_load_template, - ): - mock_parse_args.return_value = _build_mock_args_with_side_metrics() - mock_retrive_model.return_value = "test-model" - mock_load_template.return_value = None - - from fastdeploy.entrypoints.openai import api_server as api_server_mod - - api_server = importlib.reload(api_server_mod) - - # First, llm_engine is None -> 500 - cfg_route = _get_route(api_server.metrics_app, "/config-info") - assert cfg_route is not None - resp = cfg_route.endpoint() - assert resp.status_code == 500 - - # Then set a fake engine with cfg carrying both serializable and non-serializable objects - class WithDict: - pass - - has_dict = WithDict() - has_dict.x = 42 - no_dict = object() - - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS(with_dict=has_dict, without_dict=no_dict)) - - resp2 = cfg_route.endpoint() - assert resp2.status_code == 200 - data = json.loads(resp2.body.decode("utf-8")) - assert data.get("with_dict") == {"x": 42} - assert "without_dict" in data and isinstance(data["without_dict"], str) - assert "env_config" in data - - -def _reload_api_server(): - """Helper: reload api_server with standard mocks, return the module.""" - with ( - patch("fastdeploy.utils.FlexibleArgumentParser.parse_args") as mock_parse_args, - patch("fastdeploy.utils.retrive_model_from_server") as mock_retrive_model, - patch("fastdeploy.entrypoints.chat_utils.load_chat_template") as mock_load_template, - ): - mock_parse_args.return_value = _build_mock_args() - mock_retrive_model.return_value = "test-model" - mock_load_template.return_value = None - - from fastdeploy.entrypoints.openai import api_server as api_server_mod - - api_server = importlib.reload(api_server_mod) - return api_server - - -def test_config_info_server_config_matches_args(): - """Verify server_config values are populated from args.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS()) - - resp = _get_route(api_server.app, "/config-info").endpoint() - assert resp.status_code == 200 - data = json.loads(resp.body.decode("utf-8")) - - sc = data["server_config"] - assert sc["host"] == "0.0.0.0" - assert sc["port"] == 8000 - assert sc["workers"] == 1 - assert sc["metrics_port"] is None - assert sc["controller_port"] == -1 - assert sc["max_concurrency"] == 16 - assert sc["max_waiting_time"] == -1 - assert sc["timeout"] == 0 - assert sc["timeout_graceful_shutdown"] == 0 - assert sc["served_model_name"] is None - assert sc["task"] is None - assert sc["model_config_name"] is None - assert sc["tokenizer_base_url"] is None - assert sc["enable_mm_output"] is False - assert sc["tool_call_parser"] is None - assert sc["tool_parser_plugin"] is None - - -def test_config_info_top_level_fields(): - """Verify version_info, chat_template, device_info, env_config all present.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS(key="val")) - - resp = _get_route(api_server.app, "/config-info").endpoint() - data = json.loads(resp.body.decode("utf-8")) - - assert "version_info" in data - assert "chat_template" in data - assert "device_info" in data - assert "env_config" in data - assert isinstance(data["env_config"], dict) - # cfg field should propagate - assert data["key"] == "val" - - -def test_config_info_process_object_set_and_frozenset(): - """Cover process_object branch for set/frozenset -> list.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - api_server.llm_engine = NS( - cfg=NS( - my_set={3, 1, 2}, - my_frozenset=frozenset(["b", "a"]), - ) - ) - - resp = _get_route(api_server.app, "/config-info").endpoint() - assert resp.status_code == 200 - data = json.loads(resp.body.decode("utf-8")) - - assert isinstance(data["my_set"], list) - assert sorted(data["my_set"]) == [1, 2, 3] - assert isinstance(data["my_frozenset"], list) - assert sorted(data["my_frozenset"]) == ["a", "b"] - - -def test_config_info_non_ascii_content(): - """Cover ensure_ascii=False path with unicode in cfg.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - api_server.llm_engine = NS(cfg=NS(desc="中文描述", emoji="🚀")) - - resp = _get_route(api_server.app, "/config-info").endpoint() - assert resp.status_code == 200 - raw = resp.body.decode("utf-8") - # Non-ASCII chars should appear directly, not as \uXXXX escapes - assert "中文描述" in raw - assert "🚀" in raw - data = json.loads(raw) - assert data["desc"] == "中文描述" - assert data["emoji"] == "🚀" - - -def test_config_info_cfg_fields_propagated(): - """Verify that all cfg.__dict__ entries end up in the response.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - api_server.llm_engine = NS( - cfg=NS( - model_name="Qwen-7B", - max_seq_len=4096, - use_fp16=True, - parallel_config=None, - ) - ) - - resp = _get_route(api_server.app, "/config-info").endpoint() - data = json.loads(resp.body.decode("utf-8")) - - assert data["model_name"] == "Qwen-7B" - assert data["max_seq_len"] == 4096 - assert data["use_fp16"] is True - assert data["parallel_config"] is None - - -def test_config_info_nested_objects(): - """Cover process_object with nested custom objects.""" - api_server = _reload_api_server() - from types import SimpleNamespace as NS - - class Inner: - pass - - inner = Inner() - inner.lr = 0.01 - inner.steps = 100 - - api_server.llm_engine = NS(cfg=NS(train_config=inner)) - - resp = _get_route(api_server.app, "/config-info").endpoint() - assert resp.status_code == 200 - data = json.loads(resp.body.decode("utf-8")) - - assert data["train_config"] == {"lr": 0.01, "steps": 100}