3131 release, which is the deliberate state of ``latest``: a floating tag
3232 would guarantee golden rot, so it never gets one.
33333. ``test_declared_metric_names_exist`` (live, every release in the table):
34- declared names present in the real exposition, the end-invariant itself.
34+ declared names present in the real exposition under the type their query
35+ assumes, the end-invariant itself.
3536 On ``latest`` this is the early warning that an upstream rename is
3637 coming, red on live PRs before, not at, the next pin bump.
3738
5859 exposed_vllm_families ,
5960 format_golden ,
6061 golden_path ,
61- in_golden ,
6262 is_exposed ,
63+ resolves ,
6364 load_golden ,
6465)
6566from utils .net import get_free_port
8283# name declarations are read from it, never the tokenizer itself.
8384GEMMA_TARBALL = "e2e/testdata/models/google_gemma-3-270m.tar.gz"
8485
85- # Declared names that a STOCK vLLM does not expose. All five arrived in #348
86+ # Declared names that a STOCK vLLM does not expose. All four arrived in #348
8687# ("vLLM latest (0.15.0) production metrics") and are absent from a default
87- # v0.26.0 server, seemingly gated on optional features (KV offloading and
88- # similar ) whose components never register their metric families on a stock
89- # configuration. Kept out of the strict checks rather than deleted so the
88+ # v0.26.0 server, gated on optional features (VLLM_COMPUTE_NANS_IN_LOGITS and
89+ # --kv-cache-metrics ) whose components never register their metric families on
90+ # a stock configuration. Kept out of the strict checks rather than deleted so the
9091# declarations can be triaged: each is either config-gated (then this list
9192# documents the gate) or stale (then it should be removed from vllm_client).
93+ #
94+ # Guarded by test_conditionally_exposed_still_apply. A fifth entry,
95+ # vllm:prompt_tokens_recomputed, outlived its declaration and sat here shrinking
96+ # the strict checks for nothing; it is registered nowhere in vLLM v0.26.0-v0.28.0.
9297CONDITIONALLY_EXPOSED = {
9398 "vllm:corrupted_requests" ,
9499 "vllm:kv_block_idle_before_evict_seconds" ,
95100 "vllm:kv_block_lifetime_seconds" ,
96101 "vllm:kv_block_reuse_gap_seconds" ,
97- "vllm:prompt_tokens_recomputed" ,
98102}
99103
100104# Declarations whose queries select nothing, with a fix already in flight. Kept
@@ -153,7 +157,7 @@ def test_declared_names_resolve_against_goldens(golden_file: Path) -> None:
153157 declared = _declared ("http://127.0.0.1:1" , DEFAULT_MODEL )
154158 assert declared , "vLLM client declared no metric names"
155159
156- missing = sorted (name for name , metric in declared .items () if name not in SKIPPED and not in_golden (metric , golden ))
160+ missing = sorted (name for name , metric in declared .items () if name not in SKIPPED and not resolves (metric , golden ))
157161 assert not missing , (
158162 f"{ len (missing )} /{ len (declared )} declared metric names do not resolve against { golden_file .name } "
159163 f"(stale names produce silently empty report fields): { missing } "
@@ -172,13 +176,36 @@ def test_known_unresolved_still_do_not_resolve(golden_file: Path) -> None:
172176 undeclared = sorted (name for name in KNOWN_UNRESOLVED if name not in declared )
173177 assert not undeclared , f"no longer declared, drop the KNOWN_UNRESOLVED entries: { undeclared } "
174178
175- now_resolving = sorted (name for name in KNOWN_UNRESOLVED if in_golden (declared [name ], golden ))
179+ now_resolving = sorted (name for name in KNOWN_UNRESOLVED if resolves (declared [name ], golden ))
176180 assert not now_resolving , (
177181 f"{ now_resolving } now resolve against { golden_file .name } ; drop their KNOWN_UNRESOLVED "
178182 f"entries so the strict check covers them again"
179183 )
180184
181185
186+ @pytest .mark .parametrize ("golden_file" , GOLDEN_FILES , ids = lambda p : p .stem )
187+ def test_conditionally_exposed_still_apply (golden_file : Path ) -> None :
188+ # Guards the other allowlist the same way. CONDITIONALLY_EXPOSED shrinks the
189+ # strict checks, so each entry has to still be earning that: still declared by
190+ # vllm_client, and still absent from a stock server's golden. Feeding it
191+ # {"vllm:corrupted_requests"} against v0.26.0.txt passes, because the client
192+ # declares it and the golden (captured from a stock server, which does not run
193+ # with VLLM_COMPUTE_NANS_IN_LOGITS) does not list it. Dropping the declaration
194+ # or a release starting to expose it both fail here, which is what stops the
195+ # list quietly widening the hole it opens.
196+ golden = load_golden (golden_file )
197+ declared = _declared ("http://127.0.0.1:1" , DEFAULT_MODEL )
198+
199+ undeclared = sorted (name for name in CONDITIONALLY_EXPOSED if name not in declared )
200+ assert not undeclared , f"no longer declared, drop the CONDITIONALLY_EXPOSED entries: { undeclared } "
201+
202+ now_resolving = sorted (name for name in CONDITIONALLY_EXPOSED if resolves (declared [name ], golden ))
203+ assert not now_resolving , (
204+ f"{ now_resolving } resolve against { golden_file .name } , so they are not gated off on a stock "
205+ f"server; drop their CONDITIONALLY_EXPOSED entries so the strict check covers them again"
206+ )
207+
208+
182209@pytest .mark .asyncio
183210@pytest .mark .skipif (not VLLMServerRunner .is_available (), reason = "no vLLM server or executable available" )
184211async def test_exposed_families_match_golden () -> None :
@@ -211,13 +238,29 @@ async def test_exposed_families_match_golden() -> None:
211238@pytest .mark .asyncio
212239@pytest .mark .skipif (not VLLMServerRunner .is_available (), reason = "no vLLM server or executable available" )
213240async def test_declared_metric_names_exist () -> None :
241+ # Two oracles over one exposition, both driven by the metric's own
242+ # candidate_names(): every series a query selects must be present
243+ # (is_exposed, over sample and family names), and the family behind it must
244+ # carry the type the query assumes (resolves, over the `# TYPE` map).
245+ # Presence alone is not enough, and that gap is the same shape as the bug
246+ # this module exists for. Given `# TYPE vllm:prefix_cache_hits gauge` and a
247+ # sample line `vllm:prefix_cache_hits{...} 3`, a declared
248+ # CounterMetric("vllm:prefix_cache_hits") is_exposed -> True on the bare-name
249+ # candidate group, while the query it emits, increase(...[60s]), is nonsense
250+ # over a gauge and reports no error. resolves -> False catches it.
214251 async with VLLMServerRunner (port = get_free_port ()) as server :
215- names = exposed_names ( await _warmed_up_exposition (server ) )
252+ exposition = await _warmed_up_exposition (server )
216253 declared = _declared (server .base_url , server .model )
217254
218255 assert declared , "vLLM client declared no metric names"
219- missing = sorted (name for name , metric in declared .items () if name not in SKIPPED and not is_exposed (metric , names ))
220- assert not missing , (
221- f"{ len (missing )} /{ len (declared )} declared metric names absent from a real vLLM /metrics exposition "
222- f"(stale names produce silently empty report fields): { missing } "
256+ names = exposed_names (exposition )
257+ families = exposed_vllm_families (exposition )
258+ checked = {name : metric for name , metric in declared .items () if name not in SKIPPED }
259+
260+ absent = {name for name , metric in checked .items () if not is_exposed (metric , names )}
261+ mistyped = sorted (name for name , metric in checked .items () if name not in absent and not resolves (metric , families ))
262+ assert not (absent or mistyped ), (
263+ f"{ len (absent ) + len (mistyped )} /{ len (checked )} declared metric names unusable against a real vLLM "
264+ f"/metrics exposition (a stale name reports an empty field, a retyped family reports a nonsense one, "
265+ f"and neither raises): absent={ sorted (absent )} , wrong_type={ mistyped } "
223266 )
0 commit comments