Coverage for orchestrant/benchmark/contract.py: 88%
412 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
1"""Does the server still behave the way the tooling assumes?
3Each check records a neutral answer (yes, no, inconclusive, error, skipped) and
4its evidence, never a pass or fail; `--diff` names what moved between runtimes.
6 orchestrant-bench contract --backend geniex-npu --overflow-tokens 6000 --output npu.json
7 orchestrant-bench contract --diff v061-npu.json v070-npu.json
8"""
10from __future__ import annotations
12import argparse
13import functools
14import json
15import math
16import random
17import re
18import sys
19import time
20import urllib.error
22from orchestrant.benchmark.answers import split_answer
23from orchestrant.benchmark.client import post_json
26# Fixed vocabulary and seed: a changed prefill time is the server, not the prompt.
27_WORDS = (
28 "lane", "server", "model", "token", "cache", "prefill", "decode", "window",
29 "budget", "request", "answer", "context", "kernel", "thread", "cluster",
30 "memory", "report", "runtime", "version", "grader", "sample", "repeat",
31 "fixture", "harness", "verdict", "interval", "baseline",
32) # fmt: skip
35def filler(approx_tokens, seed=1234):
36 """Deterministic prose of roughly `approx_tokens` tokens (usage has the real count)."""
37 rng = random.Random(seed) # nosec B311 # noqa: S311 -- filler text, not a secret
38 words, sentences = int(approx_tokens / 1.3), []
39 while words > 0:
40 n = min(words, rng.randint(8, 16))
41 sentences.append(
42 " ".join(rng.choice(_WORDS) for _ in range(n)).capitalize() + "."
43 )
44 words -= n
45 return " ".join(sentences)
48# Each request's timeout when --timeout is not given.
49DEFAULT_TIMEOUT_S = 600
50# The slowest cold prefill measured, halved because the rate falls with depth.
51SLOWEST_PREFILL_TOK_S = 6
54def prompt_timeout(tokens, timeout=None):
55 """Seconds a `tokens`-long prompt may take: --timeout or, if longer, its slowest prefill."""
56 return max(timeout or DEFAULT_TIMEOUT_S, math.ceil(tokens / SLOWEST_PREFILL_TOK_S))
59def _timeout(ctx):
60 return ctx.get("timeout") or DEFAULT_TIMEOUT_S
63def _chat(ctx, messages, timeout=None, **params):
64 """One non-streamed chat call -> (seconds, body or None, error or None)."""
65 body = {"model": ctx["model"], "messages": messages, "stream": False, **params}
66 t0 = time.monotonic()
67 try:
68 with post_json(
69 f"{ctx['base_url']}/v1/chat/completions",
70 body,
71 entry=ctx["entry"],
72 timeout=timeout or _timeout(ctx),
73 ) as r:
74 return time.monotonic() - t0, r.json(), None
75 except urllib.error.HTTPError as e:
76 detail = e.read().decode("utf-8", "replace")[:300]
77 return time.monotonic() - t0, None, f"HTTP {e.code}: {detail}"
78 except Exception as e:
79 return time.monotonic() - t0, None, f"{type(e).__name__}: {e}"[:300]
82def _content(body):
83 msg = ((body or {}).get("choices") or [{}])[0].get("message") or {}
84 return msg.get("content") or ""
87def _finish(body):
88 return ((body or {}).get("choices") or [{}])[0].get("finish_reason")
91def _usage(body):
92 return (body or {}).get("usage") or {}
95def _user(text):
96 return [{"role": "user", "content": text}]
99def check_max_tokens(ctx):
100 _, body, err = _chat(
101 ctx,
102 _user("Count from 1 to 200, separated by spaces."),
103 max_tokens=16,
104 temperature=0,
105 )
106 if err:
107 return {"answer": "error", "evidence": err}
108 used = _usage(body).get("completion_tokens")
109 honoured = used is not None and used <= 16 and _finish(body) == "length"
110 return {
111 "answer": "yes" if honoured else "no",
112 "evidence": f"max_tokens=16 -> completion_tokens={used}, finish_reason={_finish(body)!r}",
113 }
116# The budget the output-cap check asks for, and a reply that runs past it.
117_CAP_ASK = 3000
118_CAP_PROMPT = (
119 "Write every whole number from 1 to 5000 in order, one per line. "
120 "Output only the numbers, with no other text."
121)
122# A server may stop at one of these and still report a normal finish.
123_ROUND_CAPS = (256, 512, 1024, 2048)
124# Servers count the last token or two differently; this close is the budget.
125_CAP_SLACK = 16
128def check_output_cap(ctx):
129 """Does the server stop EARLIER than max_tokens asks -- and where?"""
130 seconds, body, err = _chat(
131 ctx,
132 _user(_CAP_PROMPT),
133 timeout=max(1800, _timeout(ctx)),
134 max_tokens=_CAP_ASK,
135 temperature=0,
136 )
137 if err:
138 return {"answer": "error", "evidence": err}
139 used, finish = _usage(body).get("completion_tokens"), _finish(body)
140 evidence = (
141 f"max_tokens={_CAP_ASK} -> completion_tokens={used}, "
142 f"finish_reason={finish!r}, prompt_tokens={_usage(body).get('prompt_tokens')}, "
143 f"{seconds:.0f}s"
144 )
145 if not isinstance(used, int):
146 answer = "inconclusive"
147 evidence += " (no completion_tokens: the stop point cannot be read)"
148 elif used >= _CAP_ASK - _CAP_SLACK:
149 answer = "no"
150 if used > _CAP_ASK + _CAP_SLACK:
151 evidence += " (MORE than asked: max_tokens itself is not honoured)"
152 elif finish == "length" or used in _ROUND_CAPS:
153 answer = "yes"
154 else:
155 answer = "inconclusive"
156 evidence += " (the reply finished on its own, before a cap could show)"
157 return {"answer": answer, "evidence": evidence, "stopped_at_tokens": used}
160def check_usage(ctx):
161 _, body, err = _chat(
162 ctx, _user("Reply with the single word: ok"), max_tokens=8, temperature=0
163 )
164 if err:
165 return {"answer": "error", "evidence": err}
166 u = _usage(body)
167 ok = bool(u.get("prompt_tokens")) and u.get("completion_tokens") is not None
168 return {"answer": "yes" if ok else "no", "evidence": f"usage={u}"}
171def _unique(text):
172 """The same request text with a nonce, so no cache has seen it."""
173 return f"{text} ({random.SystemRandom().randrange(10**9)})"
176def check_cached_prompt_tokens(ctx):
177 """Is usage.prompt_tokens still the prompt's size when the prompt is cached?"""
178 text, seen = _unique("Reply with the single word: ok"), []
179 for _ in range(2):
180 _, body, err = _chat(ctx, _user(text), max_tokens=4, temperature=0)
181 if err:
182 return {"answer": "error", "evidence": err}
183 usage = _usage(body)
184 cached = (usage.get("prompt_tokens_details") or {}).get("cached_tokens")
185 seen.append((usage.get("prompt_tokens"), cached))
186 (first, _), (second, cached) = seen
187 return {
188 "answer": "yes" if first and second == first else "no",
189 "evidence": f"identical prompt: prompt_tokens {first} then {second} (cached_tokens {cached})",
190 }
193def check_stream_usage(ctx):
194 body = {
195 "model": ctx["model"],
196 "messages": _user(_unique("Reply with the single word: ok")),
197 "max_tokens": 8,
198 "temperature": 0,
199 "stream": True,
200 "stream_options": {"include_usage": True},
201 }
202 usage, chunks = None, 0
203 try:
204 with post_json(
205 f"{ctx['base_url']}/v1/chat/completions",
206 body,
207 entry=ctx["entry"],
208 stream=True,
209 timeout=_timeout(ctx),
210 ) as r:
211 for line in r.lines():
212 if not line.startswith("data:"):
213 continue
214 data = line[5:].strip()
215 if data == "[DONE]":
216 break
217 try:
218 chunk = json.loads(data)
219 except json.JSONDecodeError:
220 continue
221 chunks += 1
222 usage = chunk.get("usage") or usage
223 except Exception as e:
224 return {"answer": "error", "evidence": f"{type(e).__name__}: {e}"[:300]}
225 # A unique prompt, so no cache hit zeroes prompt_tokens and nulls the prefill rate.
226 usable = (
227 bool(usage)
228 and bool(usage.get("prompt_tokens"))
229 and bool(usage.get("completion_tokens"))
230 )
231 return {
232 "answer": "yes" if usable else "no",
233 "evidence": f"{chunks} chunks, usage chunk: {usage}",
234 }
237_SEA = "Write one sentence about the sea."
238# Near-greedy anywhere; NOT temperature 0 (see check_temperature0_is_greedy).
239_GREEDY = {"temperature": 0.01, "top_k": 1}
242def _spacer(ctx):
243 """An unrelated request, so the next one is not an identical follow-up."""
244 _chat(ctx, _user("Reply with the single word: ok"), max_tokens=1)
247def _twice(ctx, spaced=True, **params):
248 """The same request twice, spaced by default -> ([text, text], [prompt_tokens, ...], error)."""
249 replies, prompt_tokens = [], []
250 for i in range(2):
251 if spaced or i == 0:
252 _spacer(ctx)
253 _, body, err = _chat(ctx, _user(_SEA), **params)
254 if err:
255 return None, None, err
256 replies.append(_content(body))
257 prompt_tokens.append(_usage(body).get("prompt_tokens"))
258 return replies, prompt_tokens, None
261def _agree(ctx, label, spaced=True, **params):
262 replies, _, err = _twice(ctx, spaced=spaced, **params)
263 if err:
264 return {"answer": "error", "evidence": err}
265 same = replies[0] == replies[1]
266 return {
267 "answer": "yes" if same else "no",
268 "evidence": f"identical ({label})"
269 if same
270 else f"{replies[0][:60]!r} vs {replies[1][:60]!r}",
271 }
274def check_temperature0(ctx):
275 return _agree(ctx, "T=0, another request between", max_tokens=48, temperature=0)
278def check_greedy(ctx):
279 return _agree(ctx, "temperature 0.01, top_k 1", max_tokens=48, **_GREEDY)
282def check_temperature0_is_greedy(ctx):
283 """Is temperature 0 greedy decoding, or read as unset?"""
284 _spacer(ctx)
285 _, t0, err = _chat(ctx, _user(_SEA), max_tokens=48, temperature=0)
286 if err:
287 return {"answer": "error", "evidence": err}
288 _spacer(ctx)
289 _, greedy, err = _chat(ctx, _user(_SEA), max_tokens=48, **_GREEDY)
290 if err:
291 return {"answer": "error", "evidence": err}
292 a, b = _content(t0), _content(greedy)
293 return {
294 "answer": "yes" if a == b else "no",
295 "evidence": "T=0 reply == temperature 0.01/top_k 1 reply"
296 if a == b
297 else f"T=0 {a[:50]!r} vs greedy {b[:50]!r}",
298 }
301def check_identical_repeat(ctx):
302 """Does an identical request sent twice IN A ROW get the same reply?"""
303 base = check_greedy(ctx)
304 if base["answer"] != "yes":
305 return {
306 "answer": "inconclusive" if base["answer"] == "no" else base["answer"],
307 "evidence": f"near-greedy replies differ even when spaced: {base['evidence']}",
308 }
309 replies, tokens, err = _twice(ctx, spaced=False, max_tokens=48, **_GREEDY)
310 if err:
311 return {"answer": "error", "evidence": err}
312 same = replies[0] == replies[1]
313 return {
314 "answer": "yes" if same else "no",
315 "evidence": f"back to back: prompt_tokens {tokens[0]} then {tokens[1]}; "
316 + ("identical" if same else f"{replies[0][:50]!r} vs {replies[1][:50]!r}"),
317 }
320def check_seed(ctx):
321 return _agree(
322 ctx,
323 "T=1.0, seed 1234, another request between",
324 max_tokens=48,
325 temperature=1.0,
326 seed=1234,
327 )
330def check_thinks(ctx):
331 _, body, err = _chat(ctx, _user("What is 2 + 3?"), max_tokens=512, temperature=0)
332 if err:
333 return {"answer": "error", "evidence": err}
334 text = _content(body)
335 msg = ((body.get("choices") or [{}])[0]).get("message") or {}
336 separate = msg.get("reasoning_content") or msg.get("reasoning")
337 counted = (_usage(body).get("completion_tokens_details") or {}).get(
338 "reasoning_tokens"
339 )
340 thinks = "<think>" in text or "</think>" in text or bool(separate) or bool(counted)
341 return {
342 "answer": "yes" if thinks else "no",
343 "evidence": f"{_usage(body).get('completion_tokens')} tokens: "
344 f"{(text or separate or '')[:80]!r}"
345 + (" (separate reasoning field)" if separate else ""),
346 }
349# A counting model reaches "7"; a thinking one writes "the user" first.
350_STOPS = ["7", "user"]
353def check_stop(ctx):
354 """Does chat honour a stop sequence? Inconclusive if none ever came up."""
355 _, body, err = _chat(
356 ctx,
357 _user("Count from 1 to 20, separated by spaces. Output only the numbers."),
358 max_tokens=256,
359 temperature=0,
360 stop=_STOPS,
361 )
362 if err:
363 return {"answer": "error", "evidence": err}
364 text = _content(body)
365 if any(s in text for s in _STOPS):
366 answer = "no"
367 elif _finish(body) == "stop":
368 answer = "yes"
369 else: # the budget ran out before any stop string was due
370 answer = "inconclusive"
371 return {
372 "answer": answer,
373 "evidence": f"stop={_STOPS} -> finish_reason={_finish(body)!r}, content={text[-60:]!r}",
374 }
377def check_completions_stop(ctx):
378 body = {
379 "model": ctx["model"],
380 "prompt": "1, 2, 3, 4,",
381 "max_tokens": 24,
382 "stop": ["7"],
383 }
384 try:
385 with post_json(
386 f"{ctx['base_url']}/v1/completions",
387 body,
388 entry=ctx["entry"],
389 timeout=_timeout(ctx),
390 ) as r:
391 reply = r.json()
392 except urllib.error.HTTPError as e:
393 return {
394 "answer": "error",
395 "evidence": f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')[:200]}",
396 }
397 except Exception as e:
398 return {"answer": "error", "evidence": f"{type(e).__name__}: {e}"[:300]}
399 text = ((reply.get("choices") or [{}])[0]).get("text") or ""
400 # "yes" needs the sequence to reach 6 and stop before 7.
401 if "7" in text:
402 answer = "no"
403 elif "6" in text:
404 answer = "yes"
405 else:
406 answer = "inconclusive"
407 return {
408 "answer": answer,
409 "evidence": f"/v1/completions stop=['7'] -> {text[:60]!r}",
410 }
413_READ_FILE = {
414 "type": "function",
415 "function": {
416 "name": "read_file",
417 "description": "Return the CONTENTS of one file.",
418 "parameters": {
419 "type": "object",
420 "properties": {"path": {"type": "string", "description": "file path"}},
421 "required": ["path"],
422 },
423 },
424}
427def check_tool_calls(ctx):
428 _, body, err = _chat(
429 ctx,
430 _user("Read the file README.md."),
431 tools=[_READ_FILE],
432 max_tokens=1024,
433 temperature=0,
434 )
435 if err:
436 return {"answer": "error", "evidence": err}
437 msg = ((body.get("choices") or [{}])[0]).get("message") or {}
438 calls = msg.get("tool_calls") or []
439 name = calls[0].get("function", {}).get("name") if calls else None
440 return {
441 "answer": "yes" if name == "read_file" else "no",
442 "evidence": f"finish_reason={_finish(body)!r}, tool_calls={[c.get('function', {}).get('name') for c in calls]}, "
443 f"content={_content(body)[:60]!r}",
444 }
447# The prompt never asks for JSON, so exactly this object means the server constrained it.
448_COLOURS = ("red", "yellow", "blue")
449_COLOUR_FORMAT = {
450 "type": "json_schema",
451 "json_schema": {
452 "name": "colour_letters",
453 "strict": True,
454 "schema": {
455 "type": "object",
456 "properties": {
457 "colour": {"type": "string", "enum": list(_COLOURS)},
458 "letters": {"type": "integer"},
459 },
460 "required": ["colour", "letters"],
461 "additionalProperties": False,
462 },
463 },
464}
465_FENCED = re.compile(r"^```[\w-]*\s*\n(.*?)\n?```$", re.DOTALL)
468def _fits_colour_schema(text):
469 try:
470 value = json.loads(text)
471 except ValueError:
472 return None
473 letters = value.get("letters") if isinstance(value, dict) else None
474 return (
475 isinstance(value, dict)
476 and set(value) == {"colour", "letters"}
477 and value["colour"] in _COLOURS
478 and isinstance(letters, int)
479 and not isinstance(letters, bool)
480 )
483def _schema_outcome(answer):
484 """'honoured', 'fenced', 'json_only' or 'ignored' for one reply's answer."""
485 fits = _fits_colour_schema(answer)
486 if fits is None:
487 fence = _FENCED.match(answer)
488 return "fenced" if fence and _fits_colour_schema(fence.group(1)) else "ignored"
489 return "honoured" if fits else "json_only"
492def check_response_format(ctx):
493 """Is a response_format json_schema honoured, or (both "no") ignored or refused?"""
494 _, body, err = _chat(
495 ctx,
496 _user("Name one primary colour and say how many letters its name has."),
497 max_tokens=1024,
498 temperature=0,
499 response_format=_COLOUR_FORMAT,
500 )
501 if err:
502 if err.startswith("HTTP 4"):
503 return {
504 "answer": "no",
505 "outcome": "refused",
506 "evidence": f"refused: {err[:200]}",
507 }
508 return {"answer": "error", "evidence": err}
509 content = _content(body)
510 answer = split_answer(content)[1].strip()
511 if not answer:
512 return {
513 "answer": "inconclusive",
514 "outcome": None,
515 "evidence": f"no answer, finish_reason={_finish(body)!r}: {content[:80]!r}",
516 }
517 outcome = _schema_outcome(answer)
518 return {
519 "answer": "yes" if outcome == "honoured" else "no",
520 "outcome": outcome,
521 "evidence": f"{outcome}: {answer[:100]!r}",
522 }
525def _prefix_timings(ctx):
526 """Cold, repeat, multi-turn extend and shared-prefix fork, measured once."""
527 if "_prefix" not in ctx:
528 nonce = f"Session {random.SystemRandom().randrange(10**9)}." # no stale cache
529 prefix = f"{nonce} {filler(ctx['prefix_tokens'])}"
530 tail = filler(300, seed=99) + "\n\nReply with the single word: ok"
531 first = _user(prefix + "\n\nReply with the single word: ok")
532 runs = (
533 ("cold", first),
534 ("repeat", first),
535 ("extend", [*first, {"role": "assistant", "content": "ok"}, *_user(tail)]),
536 ("fork", _user(f"{prefix} {tail}")),
537 )
538 timings = {}
539 # A fork re-prefills the whole prefix on GenieX: every request may be cold.
540 timeout = prompt_timeout(ctx["prefix_tokens"], ctx.get("timeout"))
541 for name, messages in runs:
542 seconds, body, err = _chat(
543 ctx, messages, timeout=timeout, max_tokens=1, temperature=0
544 )
545 if err:
546 timings = {"error": f"{name}: {err}"}
547 break
548 timings[name] = (round(seconds, 2), _usage(body).get("prompt_tokens"))
549 ctx["_prefix"] = timings
550 return ctx["_prefix"]
553def _cache_answer(ctx, name, share):
554 t = _prefix_timings(ctx)
555 if "error" in t:
556 return {"answer": "error", "evidence": t["error"]}
557 cold, cold_tok = t["cold"]
558 seconds, tokens = t[name]
559 evidence = f"cold {cold}s ({cold_tok} tok) -> {name} {seconds}s ({tokens} tok)"
560 if cold <= 0.5: # below request overhead a cache cannot show; --prefix-tokens
561 return {"answer": "inconclusive", "evidence": evidence + " (cold too fast)"}
562 return {
563 "answer": "yes" if seconds < share * cold else "no",
564 "evidence": evidence,
565 }
568def check_prefix_cache(ctx):
569 out = _cache_answer(ctx, "repeat", 0.3)
570 t = ctx["_prefix"]
571 if "error" not in t:
572 cold, cold_tok = t["cold"]
573 out["prefill_tok_per_s"] = (
574 round(cold_tok / cold, 1) if cold_tok and cold else None
575 )
576 out["timings_s"] = {k: v[0] for k, v in t.items()}
577 out["prompt_tokens"] = {k: v[1] for k, v in t.items()}
578 return out
581def check_prefix_extend(ctx):
582 return _cache_answer(ctx, "extend", 0.5)
585def check_prefix_fork(ctx):
586 return _cache_answer(ctx, "fork", 0.5)
589def check_overflow(ctx):
590 n = ctx.get("overflow_tokens") or 0
591 if not n:
592 return {
593 "answer": "skipped",
594 "evidence": "pass --overflow-tokens N (more than the lane's context)",
595 }
596 seconds, body, err = _chat(
597 ctx,
598 _user(filler(n) + "\n\nReply with the single word: ok"),
599 timeout=prompt_timeout(n, ctx.get("timeout")),
600 max_tokens=16,
601 temperature=0,
602 )
603 if err:
604 # A 4xx naming the context is a refusal; a 5xx mentioning it is the server falling over.
605 refused = err.startswith("HTTP 4")
606 clean = refused and (
607 "context_length_exceeded" in err or "context" in err.lower()
608 )
609 return {
610 "answer": "yes" if clean else "no",
611 "evidence": f"~{n} tokens -> {err[:160]} after {seconds:.1f}s",
612 }
613 return {
614 "answer": "no",
615 "evidence": f"~{n} tokens -> HTTP 200 after {seconds:.1f}s, "
616 f"{_usage(body).get('completion_tokens')} tokens: {_content(body)[:40]!r}",
617 }
620def check_power_mode(ctx):
621 """Is `power_mode` VALIDATED -- a known value accepted, nonsense refused?"""
622 answers = {}
623 # Ends plain: power_mode is in the model's cache key, so this reloads the launch state.
624 for value in ("power_saver", "turbo", None):
625 extra = {"power_mode": value} if value else {}
626 seconds, _, err = _chat(
627 ctx,
628 _user("Reply with the single word: ok"),
629 max_tokens=4,
630 temperature=0,
631 **extra,
632 )
633 answers[value or "restore (no power_mode)"] = (
634 f"accepted in {seconds:.1f}s" if err is None else err[:120]
635 )
636 understood = answers["power_saver"].startswith("accepted") and not answers[
637 "turbo"
638 ].startswith("accepted")
639 return {"answer": "yes" if understood else "no", "evidence": answers}
642# The same text twice: their difference is the text's cost, apart from a system turn's framing.
643_SYSTEM_PROBE = "Answer every question as briefly as you can, in plain English."
644_SYSTEM_PLAN = (
645 ("system", _SYSTEM_PROBE),
646 ("none", None),
647 ("system_twice", f"{_SYSTEM_PROBE} {_SYSTEM_PROBE}"),
648 ("none_again", None),
649)
650_SELF_REPORT = (
651 "Before this message, were you given a system message or any other "
652 "instructions? If so, quote them word for word; if not, reply exactly: NONE"
653)
656def _system_prompt_tokens(ctx):
657 """prompt_tokens of the four _SYSTEM_PLAN requests -> dict, or an error string."""
658 nonce = random.SystemRandom().randrange(10**9)
659 _chat(ctx, _user(f"Request {nonce}. Reply with the single word: ok"), max_tokens=1)
660 tokens = {}
661 for i, (name, system) in enumerate(_SYSTEM_PLAN):
662 messages = [{"role": "system", "content": system}] if system else []
663 messages += _user(f"Request {nonce}-{i}. Reply with the single word: ok")
664 _, body, err = _chat(ctx, messages, max_tokens=4, temperature=0)
665 if err:
666 return f"{name}: {err}"
667 tokens[name] = _usage(body).get("prompt_tokens")
668 return tokens
671def _system_verdict(tokens):
672 """(answer, note, hidden_tokens) from the four prompt_tokens counts."""
673 counts = [tokens.get(name) for name, _ in _SYSTEM_PLAN]
674 if not all(isinstance(c, int) and c > 0 for c in counts):
675 return "inconclusive", "prompt_tokens missing or zero", None
676 if abs(tokens["none"] - tokens["none_again"]) > 2:
677 return "inconclusive", "the two no-system requests disagree", None
678 text = tokens["system_twice"] - tokens["system"]
679 added = tokens["system"] - (tokens["none"] + tokens["none_again"]) / 2
680 hidden = round(text - added, 1)
681 note = f"its text costs {text}, the message added {added:g} -> {hidden:g} hidden"
682 if text <= 0:
683 return "inconclusive", note + " (a longer system message cost nothing)", None
684 if hidden >= 2:
685 return "yes", note + ": an explicit system message replaces that many", hidden
686 if hidden <= -2:
687 return "no", note + ": a system turn's framing, no default displaced", hidden
688 return "inconclusive", note, hidden
691def check_bundle_system_prompt(ctx):
692 """With no system message, is a default system prompt sent anyway?
694 Read from usage: "no" means no default that a system message displaces;
695 `self_report` never votes, as a model invents a system prompt as readily as it quotes one.
696 """
697 tokens = _system_prompt_tokens(ctx)
698 if isinstance(tokens, str):
699 return {"answer": "error", "evidence": tokens}
700 answer, note, hidden = _system_verdict(tokens)
701 _, body, err = _chat(ctx, _user(_SELF_REPORT), max_tokens=512, temperature=0)
702 if err:
703 self_report = f"error: {err[:120]}"
704 else:
705 self_report = split_answer(_content(body))[1].strip()[:200] or (
706 f"(no answer, finish_reason={_finish(body)!r})"
707 )
708 return {
709 "answer": answer,
710 "evidence": f"prompt_tokens {tokens}: {note}",
711 "prompt_tokens": tokens,
712 "hidden_tokens": hidden,
713 "self_report": self_report,
714 }
717CHECKS = (
718 (
719 "max_tokens_honoured",
720 "Does the server stop at max_tokens with finish_reason 'length'?",
721 check_max_tokens,
722 ),
723 (
724 "output_cap",
725 "Does the server stop earlier than a 3000-token max_tokens asks?",
726 check_output_cap,
727 ),
728 (
729 "usage_reported",
730 "Does a non-streamed reply carry usage token counts?",
731 check_usage,
732 ),
733 (
734 "prompt_tokens_survive_cache",
735 "Is usage.prompt_tokens still the prompt's size when the prompt is cached?",
736 check_cached_prompt_tokens,
737 ),
738 (
739 "stream_usage",
740 "Does a stream honour stream_options.include_usage?",
741 check_stream_usage,
742 ),
743 (
744 "temperature0_deterministic",
745 "Do two T=0 requests (another between them) return identical text?",
746 check_temperature0,
747 ),
748 (
749 "greedy_deterministic",
750 "Do two temperature-0.01/top_k-1 requests (another between) agree?",
751 check_greedy,
752 ),
753 (
754 "temperature0_is_greedy",
755 "Is temperature 0 greedy decoding rather than 'unset'?",
756 check_temperature0_is_greedy,
757 ),
758 (
759 "identical_repeat_intact",
760 "Does an identical request sent twice in a row get the same reply?",
761 check_identical_repeat,
762 ),
763 (
764 "seed_deterministic",
765 "Do two T=1.0 requests with the same seed (another between) agree?",
766 check_seed,
767 ),
768 (
769 "emits_think",
770 "Does a plain question come back with a <think> block?",
771 check_thinks,
772 ),
773 ("chat_stop_honoured", "Does chat honour a stop sequence?", check_stop),
774 (
775 "completions_stop_honoured",
776 "Does /v1/completions accept and honour a stop sequence?",
777 check_completions_stop,
778 ),
779 (
780 "tool_calls_parsed",
781 "Is a tool call returned as tool_calls rather than text?",
782 check_tool_calls,
783 ),
784 (
785 "response_format_json_schema",
786 "Is a response_format json_schema honoured (not ignored or refused)?",
787 check_response_format,
788 ),
789 (
790 "prefix_cache",
791 "Is an identical request reused rather than re-prefilled?",
792 check_prefix_cache,
793 ),
794 (
795 "prefix_cache_extend",
796 "Is a conversation extended by one turn served from the cache?",
797 check_prefix_extend,
798 ),
799 (
800 "prefix_cache_fork",
801 "Is a shared long prefix with a different tail served from the cache?",
802 check_prefix_fork,
803 ),
804 (
805 "overflow_is_clean_error",
806 "Is an over-long prompt refused with an error naming the context?",
807 check_overflow,
808 ),
809 (
810 "power_mode_understood",
811 "Is a per-request power_mode validated (valid accepted, invalid refused)?",
812 check_power_mode,
813 ),
814 (
815 "bundle_system_prompt",
816 "With no system message, is a default system prompt sent anyway?",
817 check_bundle_system_prompt,
818 ),
819)
822def run(
823 base_url,
824 model,
825 entry=None,
826 prefix_tokens=2000,
827 overflow_tokens=0,
828 only=None,
829 timeout=None,
830):
831 ctx = {
832 "base_url": base_url,
833 "model": model,
834 "entry": entry or {},
835 "prefix_tokens": prefix_tokens,
836 "overflow_tokens": overflow_tokens,
837 "timeout": timeout,
838 }
839 results = []
840 for check_id, question, fn in CHECKS:
841 if only and check_id not in only:
842 continue
843 t0 = time.monotonic()
844 try:
845 outcome = fn(ctx)
846 except Exception as e: # one broken check must not cost the others
847 outcome = {"answer": "error", "evidence": f"{type(e).__name__}: {e}"[:300]}
848 outcome = {"id": check_id, "question": question, **outcome}
849 outcome["seconds"] = round(time.monotonic() - t0, 2)
850 results.append(outcome)
851 print(
852 f" {check_id:<28} {outcome['answer']:<8} {outcome['seconds']:>7.1f}s {str(outcome['evidence'])[:90]}"
853 )
854 return results
857def diff(old, new):
858 """Rows (check, old answer, new answer) for every check whose answer moved."""
860 def index(report):
861 rows = (report.get("reports") or [{}])[0].get("checks") or []
862 return {r["id"]: r for r in rows}
864 a, b = index(old), index(new)
865 order = [c[0] for c in CHECKS]
866 rows = []
867 for check_id in sorted(
868 set(a) | set(b), key=lambda k: order.index(k) if k in order else len(order)
869 ):
870 before = (a.get(check_id) or {}).get("answer", "-")
871 after = (b.get(check_id) or {}).get("answer", "-")
872 rows.append((check_id, before, after, before != after))
873 return rows
876def _parser():
877 parser = argparse.ArgumentParser(
878 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
879 )
880 parser.add_argument("--backend", default=None)
881 parser.add_argument("--base-url", default=None)
882 parser.add_argument("--model", default=None)
883 parser.add_argument(
884 "--prefix-tokens",
885 type=int,
886 default=2000,
887 help="size of the prefix-cache probe (default 2000)",
888 )
889 parser.add_argument(
890 "--overflow-tokens",
891 type=int,
892 default=0,
893 help="send a prompt of about this many tokens to see how overflow is reported; "
894 "0 skips it (on a 16k GGUF lane it would prefill for minutes)",
895 )
896 parser.add_argument(
897 "--timeout",
898 type=int,
899 default=None,
900 help=f"per-request timeout in seconds (default {DEFAULT_TIMEOUT_S}); the "
901 f"prefix-cache and overflow requests get at least their prompt's tokens / "
902 f"{SLOWEST_PREFILL_TOK_S}, the output cap at least 1800",
903 )
904 parser.add_argument("--only", default=None, help="comma-separated check ids")
905 parser.add_argument("--output", default=None)
906 parser.add_argument(
907 "--diff", nargs=2, metavar=("OLD", "NEW"), help="compare two contract reports"
908 )
909 return parser
912def main():
913 from orchestrant.benchmark.client import entry_config, run_start, write_report
914 from orchestrant.benchmark.openai_api import (
915 detect_model_via_api,
916 resolve_backend,
917 resolve_backend_entry,
918 resolve_model,
919 )
920 from orchestrant.benchmark.provenance import compare as compare_provenance
922 args = _parser().parse_args()
924 if args.diff:
925 with open(args.diff[0]) as f:
926 old = json.load(f)
927 with open(args.diff[1]) as f:
928 new = json.load(f)
929 for note in compare_provenance(
930 old.get("provenance") or {}, new.get("provenance") or {}
931 ):
932 print(f" ! {note}")
933 changed = 0
934 for check_id, before, after, moved in diff(old, new):
935 changed += moved
936 print(
937 f" {'CHANGED' if moved else ' '} {check_id:<28} {before:<8} -> {after}"
938 )
939 return 1 if changed else 0
941 base_url, backend_model, _ = resolve_backend(args.backend, args.base_url)
942 entry = resolve_backend_entry(args.backend, args.base_url)
943 # Detect on the probed lane, not the module default (localhost:11434).
944 detect = functools.partial(detect_model_via_api, base_url)
945 model = resolve_model(args.model, backend_model, entry, detect)
946 print(f"\n Contract probe: {model} @ {base_url}\n")
947 tool_files = ("contract.py",)
948 started = run_start(tool_files, base_url)
949 only = set(args.only.split(",")) if args.only else None
950 checks = run(
951 base_url,
952 model,
953 entry,
954 args.prefix_tokens,
955 args.overflow_tokens,
956 only,
957 timeout=args.timeout,
958 )
959 if args.output:
960 write_report(
961 args.output,
962 "bench_contract",
963 {
964 "prefix_tokens": args.prefix_tokens,
965 "overflow_tokens": args.overflow_tokens,
966 "timeout_s": args.timeout or DEFAULT_TIMEOUT_S,
967 "prefix_timeout_s": prompt_timeout(args.prefix_tokens, args.timeout),
968 "overflow_timeout_s": prompt_timeout(
969 args.overflow_tokens, args.timeout
970 ),
971 "backend_entry": entry_config(entry),
972 },
973 [{"label": args.backend or model, "model": model, "checks": checks}],
974 base_url,
975 tool_files,
976 run_start=started,
977 )
978 print(f"\n Report written to {args.output}")
979 return 0
982if __name__ == "__main__":
983 sys.exit(main())