agentic-bench 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
abench/__init__.py ADDED
@@ -0,0 +1,107 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026, Alibaba Group and its affiliates
3
+
4
+ """Abench — agentic inference benchmark.
5
+
6
+ Closed-loop replay of recorded agent trajectories (DeepSWE v1.1, and an
7
+ LMCache-derived Otel tier) against any OpenAI-compatible inference
8
+ server, plus a synthetic random workload for engines and context bands
9
+ the traces do not cover. System-prompt length, session selection and
10
+ the steady-state measurement window are all parameterized.
11
+
12
+ Decode lengths are pinned rather than left to the model: a replayed turn
13
+ generates its recorded ``completion_tokens``, a random turn generates
14
+ ``--output-len``. Pinning is what makes two engines comparable on the
15
+ same workload; the report checks the achieved mean output length against
16
+ the recorded one so a server that ignores the pin is visible.
17
+
18
+ Datasets are distributed via the ABench ModelScope mirror
19
+ (Apsara-Stack/ABench).
20
+
21
+ Layering: ``cli`` (arguments) -> ``runner`` (orchestration) ->
22
+ ``replay`` (closed-loop engine) -> ``serve`` (server discovery,
23
+ telemetry, reporting) -> ``core`` (data model, planning, metrics).
24
+ """
25
+
26
+ from abench.core.dataset import (
27
+ TIERS,
28
+ NormSession,
29
+ NormTurn,
30
+ download_tier,
31
+ iter_jsonl_sessions,
32
+ load_sessions,
33
+ resolve_url,
34
+ write_tier_manifest,
35
+ )
36
+ from ._version import __version__
37
+ from abench.core.metrics import (
38
+ AgenticMetrics,
39
+ BucketStats,
40
+ SessionRecord,
41
+ TurnRecord,
42
+ compute_metrics,
43
+ summarize_server_metrics,
44
+ )
45
+ from abench.core.metrics import ServerMetricsSnapshot
46
+ from abench.replay.engine import run_closed_loop, warmup
47
+ from abench.serve.endpoints import (
48
+ fetch_server_info,
49
+ flush_cache,
50
+ get_auth_headers,
51
+ server_root,
52
+ wait_for_server_ready,
53
+ )
54
+ from abench.serve.report import metrics_to_dict, print_report, save_results
55
+ from abench.core.workload import (
56
+ PROFILES,
57
+ PlannedSession,
58
+ PlannedTurn,
59
+ Profile,
60
+ build_system_prompt,
61
+ make_gap_policy,
62
+ make_output_policy,
63
+ measure_text_tokens,
64
+ plan_workload,
65
+ select_sessions,
66
+ synthesize_sessions,
67
+ )
68
+
69
+ __all__ = [
70
+ "__version__",
71
+ "TIERS",
72
+ "NormSession",
73
+ "NormTurn",
74
+ "download_tier",
75
+ "iter_jsonl_sessions",
76
+ "load_sessions",
77
+ "resolve_url",
78
+ "write_tier_manifest",
79
+ "AgenticMetrics",
80
+ "BucketStats",
81
+ "SessionRecord",
82
+ "TurnRecord",
83
+ "compute_metrics",
84
+ "summarize_server_metrics",
85
+ "ServerMetricsSnapshot",
86
+ "fetch_server_info",
87
+ "flush_cache",
88
+ "get_auth_headers",
89
+ "run_closed_loop",
90
+ "server_root",
91
+ "wait_for_server_ready",
92
+ "warmup",
93
+ "metrics_to_dict",
94
+ "print_report",
95
+ "save_results",
96
+ "PROFILES",
97
+ "PlannedSession",
98
+ "PlannedTurn",
99
+ "Profile",
100
+ "build_system_prompt",
101
+ "measure_text_tokens",
102
+ "make_output_policy",
103
+ "make_gap_policy",
104
+ "plan_workload",
105
+ "select_sessions",
106
+ "synthesize_sessions",
107
+ ]
abench/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026, Alibaba Group and its affiliates
3
+
4
+ from abench.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
abench/_version.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026, Alibaba Group and its affiliates
3
+
4
+ __version__ = "0.2.0"
abench/cli.py ADDED
@@ -0,0 +1,389 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026, Alibaba Group and its affiliates
3
+
4
+ """Command-line interface: argument parsing and dispatch.
5
+
6
+ Usage examples
7
+ --------------
8
+
9
+ Prepare the default dataset once (deepswe/mixed, ~42 MB; lands under
10
+ ``~/abench_data/<name>/<tier>/``)::
11
+
12
+ python3 -m abench data deepswe/mixed
13
+
14
+ Run a single fixed-concurrency point against a served model::
15
+
16
+ python3 -m abench run --model default \
17
+ --dataset-name deepswe/mixed --max-concurrency 16
18
+ # auto-detects localhost :30000 (sglang) / :8000 (vllm); pass
19
+ # --port or --base-url only for non-default deployments
20
+
21
+ Smoke test with no download (locally generated random workload)::
22
+
23
+ python3 -m abench run --dataset-name random \
24
+ --turns 3 --output-len 64 --max-concurrency 4
25
+
26
+ What each command *does* lives in ``abench.runner``; this module only
27
+ builds the parser (layered help: ``--help`` core flags, ``--help-advanced``
28
+ everything) and dispatches.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import os
35
+ import sys
36
+
37
+ from abench.runner import cmd_data_entry, cmd_run
38
+
39
+ # ----------------------------------------------------------------------
40
+ # argument parsing
41
+ # ----------------------------------------------------------------------
42
+ # Options hidden from the default --help; shown via `<cmd> --help-advanced`.
43
+ # Layering only -- every hidden flag still works exactly as before.
44
+ _ADVANCED_DESTS = {
45
+ "run": {
46
+ "turns", "input_increment", "output_len", "gap_policy", "gap_cap_ms",
47
+ "min_turns", "max_turns", "min_context", "max_context",
48
+ "system_prompt_len", "top_k", "presence_penalty",
49
+ "steady_frac", "warmup_requests",
50
+ "duration", "session_timeout", "routing_key", "worker_header",
51
+ "ready_check_timeout_sec", "max_duration",
52
+ "disable_tqdm", "host", "port",
53
+ },
54
+ "data": set(),
55
+ }
56
+
57
+
58
+ # (cmd, dest) -> original help text, used by --help-advanced to un-hide.
59
+ _ADVANCED_ORIG: dict = {}
60
+
61
+
62
+ class _HelpAdvancedAction(argparse.Action):
63
+ """Print the full help including advanced (hidden) options."""
64
+
65
+ def __init__(self, option_strings, dest, cmd_name=None, **kwargs):
66
+ kwargs["nargs"] = 0
67
+ super().__init__(option_strings, dest, **kwargs)
68
+ self._cmd_name = cmd_name
69
+
70
+ def __call__(self, parser, namespace, values, option_string=None):
71
+ restored = []
72
+ for act in parser._actions:
73
+ key = (self._cmd_name, act.dest)
74
+ if key in _ADVANCED_ORIG:
75
+ # the stored help may itself be None (flag had no help text)
76
+ act.help = _ADVANCED_ORIG[key]
77
+ restored.append(act)
78
+ print(parser.format_help())
79
+ for act in restored:
80
+ act.help = argparse.SUPPRESS
81
+ parser.exit()
82
+
83
+
84
+ def _layer_help(subparsers) -> None:
85
+ """Suppress advanced options' help text on every subcommand."""
86
+ for name, sp in subparsers.choices.items():
87
+ for act in sp._actions:
88
+ if act.dest in _ADVANCED_DESTS.get(name, set()):
89
+ _ADVANCED_ORIG[(name, act.dest)] = act.help
90
+ act.help = argparse.SUPPRESS
91
+ sp.add_argument(
92
+ "--help-advanced",
93
+ action=_HelpAdvancedAction,
94
+ cmd_name=name,
95
+ help="Show ALL options, including advanced/internal ones.",
96
+ )
97
+
98
+
99
+ class _Parser(argparse.ArgumentParser):
100
+ """No prefix matching: `--mode` must not silently resolve to `--model`."""
101
+
102
+ def __init__(self, *a, **kw):
103
+ kw.setdefault("allow_abbrev", False)
104
+ super().__init__(*a, **kw)
105
+
106
+
107
+ def build_parser() -> argparse.ArgumentParser:
108
+ from abench._version import __version__
109
+
110
+ # `python3 -m abench` leaves __main__.py in argv[0]; without an explicit
111
+ # prog every usage line reads "usage: __main__.py run [-h]", which is not
112
+ # something a reader can copy back into a shell.
113
+ prog = (
114
+ "python3 -m abench"
115
+ if os.path.basename(sys.argv[0]) == "__main__.py"
116
+ else None
117
+ )
118
+
119
+ parser = _Parser(
120
+ prog=prog,
121
+ description="ABench — closed-loop agentic inference benchmark "
122
+ "(replays recorded agent trajectories, or a synthetic "
123
+ "workload, against any OpenAI-compatible server)",
124
+ epilog="Most users only need a few options per command; run "
125
+ "`python3 -m abench <command> --help-advanced` to see everything.",
126
+ )
127
+ parser.add_argument("--version", action="version",
128
+ version=f"abench {__version__}")
129
+ sub = parser.add_subparsers(
130
+ dest="command", required=True, parser_class=_Parser
131
+ )
132
+
133
+ # data
134
+ d = sub.add_parser(
135
+ "data",
136
+ help="Data preparation: download a preset dataset tier to the "
137
+ "standard location <data-path>/<dataset>/<tier>/ "
138
+ "(usage: data <DATASET>[/<TIER>], e.g. data deepswe/mixed).",
139
+ )
140
+ d.add_argument("dataset", nargs="?", default=None,
141
+ help="DATASET[/TIER] to download, e.g. deepswe/mixed. "
142
+ "A bare dataset name is rejected -- tiers are peers, no default. "
143
+ "See --list for the available combinations.")
144
+ d.add_argument("--data-path", default=os.path.expanduser("~/abench_data"),
145
+ help="Download root; data lands under `<data-path>/<dataset>/<tier>/` "
146
+ "-- the same layout `run --dataset-path` understands. "
147
+ "Non-default roots: pass the printed --dataset-path back to run.")
148
+ d.add_argument("--list", action="store_true",
149
+ help="List the available dataset x tier combinations.")
150
+ d.set_defaults(func=cmd_data_entry)
151
+
152
+ # run
153
+ r = sub.add_parser("run", help="Run the closed-loop replay benchmark.")
154
+ r.add_argument("--base-url", default=None,
155
+ help="Full server base URL, e.g. http://localhost:30000. When set it "
156
+ "overrides --host/--port entirely.")
157
+ r.add_argument("--host", default="0.0.0.0",
158
+ help="Server host when --base-url is not given.")
159
+ r.add_argument("--port", type=int, default=None,
160
+ help="Server port. Unset: derived from --backend (sglang/openai "
161
+ "30000, vllm 8000; auto probes 30000 then 8000). Pass only "
162
+ "for non-default deployments.")
163
+ r.add_argument("--model", default="default",
164
+ help="Model name in the request body; a local directory is also "
165
+ "auto-used as the tokenizer.")
166
+ r.add_argument("--tokenizer", default=None,
167
+ help="Explicit tokenizer path; auto-resolved when unset.")
168
+ r.add_argument("--backend", choices=["auto", "sglang", "vllm", "openai"],
169
+ default="auto",
170
+ help="Serving backend. Recommended to leave unset: 'auto' probes "
171
+ "the server (/server_info, then /metrics content) and gates "
172
+ "the observation layer accordingly. A wrong explicit "
173
+ "declaration does not error out, it silently drops that "
174
+ "backend's observation metrics (cache breakdown / KV pool / "
175
+ "server_info snapshot).")
176
+ r.add_argument("--dataset-name", dest="dataset_name", default="deepswe/mixed",
177
+ help="Dataset identity: '<name>/<tier>' (e.g. deepswe/mixed, the "
178
+ "default) looked up under ~/abench_data, or 'random' for a "
179
+ "locally generated workload. With --dataset-path it labels the "
180
+ "run (the path supplies the location).")
181
+ r.add_argument("--dataset-path", default=None,
182
+ help="Data location: a .jsonl/.jsonl.zst file, a tier directory "
183
+ "(holds manifest.json), or a root containing <name>/<tier>/. A "
184
+ "manifest found at the target sets band/turn/system-prompt "
185
+ "defaults like --dataset-name; without one, raw mode replays the "
186
+ "files as-is (no band, min_turns 1, system_prompt_len 0).")
187
+
188
+ # --- random workload knobs (only with --dataset-name random) ---
189
+ r.add_argument("--turns", type=int, default=20,
190
+ help="Turns per random session.")
191
+ r.add_argument("--input-increment", type=int, default=2048,
192
+ help="New context tokens added per turn (random mode).")
193
+ r.add_argument("--output-len", type=int, default=256,
194
+ help="Output tokens per turn (random mode).")
195
+ r.add_argument("--gap-policy", default="exponential:3000",
196
+ help="Inter-turn gap distribution: zero | fixed:Ms | uniform:lo,hi | exponential:mean.")
197
+ r.add_argument("--gap-cap-ms", type=float, default=30_000.0,
198
+ help="Cap on any single inter-turn gap (ms).")
199
+
200
+ r.add_argument("--min-turns", type=int, default=None,
201
+ help="Drop sessions with fewer turns than this (dataset manifest "
202
+ "default applies when unset).")
203
+ r.add_argument("--max-turns", type=int, default=None,
204
+ help="Cap a session at this many turns; never pads short ones.")
205
+ r.add_argument(
206
+ "--min-context",
207
+ type=int,
208
+ default=None,
209
+ help="Keep sessions whose replayed peak context (recorded peak + "
210
+ "injected system prompt) is >= this many tokens. Same axis as "
211
+ "--max-context.",
212
+ )
213
+ r.add_argument(
214
+ "--max-context",
215
+ type=int,
216
+ default=None,
217
+ help="The MODEL context window the workload must fit: internally "
218
+ "the recorded-peak cap becomes max_context minus the measured "
219
+ "injected system prompt (the derivation is printed on the "
220
+ "[plan] line).",
221
+ )
222
+ r.add_argument("--temperature", type=float, default=0.0,
223
+ help="Sampling temperature (greedy by default, as in bench_serving).")
224
+ r.add_argument("--top-p", type=float, default=1.0,
225
+ help="Top-p; only sent when < 1.0.")
226
+ r.add_argument(
227
+ "--chat-template-kwargs",
228
+ default=None,
229
+ help='JSON forwarded as chat_template_kwargs, e.g. \'{"thinking": true}\' '
230
+ "for reasoning models. Left unset the server template default applies.",
231
+ )
232
+ r.add_argument("--top-k", type=int, default=None,
233
+ help="Top-k sampling; only sent when set.")
234
+ r.add_argument("--presence-penalty", type=float, default=None,
235
+ help="Presence penalty; only sent when set.")
236
+ r.add_argument(
237
+ "--header",
238
+ action="append",
239
+ default=None,
240
+ metavar="KEY=VALUE",
241
+ help="Extra HTTP header on every request, repeatable "
242
+ "(e.g. --header X-Tenant=abc). Merged after the auth header, "
243
+ "so it can override it.",
244
+ )
245
+ r.add_argument(
246
+ "--extra-request-body",
247
+ default=None,
248
+ help="JSON object merged into the top level of every request body, "
249
+ "overriding the built-in fields (e.g. '{\"seed\": 123}'). The "
250
+ "escape hatch for engine-specific request parameters.",
251
+ )
252
+ r.add_argument("--system-prompt-len", type=int, default=None,
253
+ help="Target token length of the shared synthetic system prompt "
254
+ "(0 = none; dataset manifest default applies when unset). "
255
+ "The actual injected length is measured and shown in the "
256
+ "[plan] line, and added to the context band accounting.")
257
+ r.add_argument("--num-sessions", type=int, default=None,
258
+ help="Session pool size; defaults to 2 x --max-concurrency (the "
259
+ "validated default sizing). Pass a larger count (or the "
260
+ "tier size) for a full-tier pass.")
261
+ r.add_argument("--seed", type=int, default=42,
262
+ help="Sampling seed for session selection and random mode.")
263
+ r.add_argument("--tag", default=None,
264
+ help="Free-form label written into the result record; pure "
265
+ "annotation, affects nothing (as in bench_serving).")
266
+
267
+ r.add_argument("--max-concurrency", type=int, default=16,
268
+ help="Simultaneously active agent sessions (closed loop: a finished "
269
+ "session's slot is immediately refilled).")
270
+ r.add_argument(
271
+ "--duration",
272
+ type=float,
273
+ default=None,
274
+ help="Seconds; enables looping pool + drain-free window.",
275
+ )
276
+ r.add_argument("--steady-frac", type=float, default=0.9,
277
+ help="Steady window = turns dispatched at >= frac*C active "
278
+ "sessions; excludes ramp/drain from metrics.")
279
+ r.add_argument("--no-gaps", action="store_true", help="Ignore tool-wait gaps.")
280
+ r.add_argument("--disable-tqdm", action="store_true",
281
+ help="Disable the tqdm turn-progress bar (falls back to the "
282
+ "periodic [progress] heartbeat; tqdm also absent from the "
283
+ "environment does the same).")
284
+ r.add_argument("--gap-scale", type=float, default=1.0,
285
+ help="Scale factor applied to each tool-wait gap (pre_gap_ms) when "
286
+ "gaps are respected: 0.5 halves every wait, 1.0 reproduces the "
287
+ "recorded timing (default), 2.0 doubles it. Ignored with "
288
+ "--no-gaps.")
289
+ r.add_argument("--disable-stream", action="store_true",
290
+ help="Non-streaming requests (no per-token ITL, whole-response "
291
+ "E2E only).")
292
+ r.add_argument("--flush-cache", action="store_true",
293
+ help="POST /flush_cache (sglang) or /reset_prefix_cache (vllm; "
294
+ "needs the server launched with VLLM_SERVER_DEV_MODE=1) "
295
+ "before the run.")
296
+ r.add_argument(
297
+ "--warmup-requests",
298
+ type=int,
299
+ default=1,
300
+ help="Sessions replayed once before measurement to populate the "
301
+ "prefix cache and surface config errors early (default 1). "
302
+ "Each warmup session is capped to its first 3 turns and the "
303
+ "phase shares the --max-duration fuse.",
304
+ )
305
+ r.add_argument(
306
+ "--ready-check-timeout-sec",
307
+ type=int,
308
+ default=60,
309
+ help="Seconds to wait for GET /v1/models to answer before the run "
310
+ "starts (0 = skip). Fails fast instead of timing out every "
311
+ "request against a still-loading server.",
312
+ )
313
+ r.add_argument(
314
+ "--max-duration",
315
+ type=float,
316
+ default=None,
317
+ help="Runaway fuse: hard cap (s) on total run wall clock. Default "
318
+ "no cap; a healthy run should never reach it.",
319
+ )
320
+ r.add_argument(
321
+ "--routing-key",
322
+ action="store_true",
323
+ help="Attach a stable X-SMG-Routing-Key per session.",
324
+ )
325
+ r.add_argument(
326
+ "--worker-header",
327
+ default=None,
328
+ help=(
329
+ "Response header carrying the serving worker's identity, used "
330
+ "for the routing-stickiness metric (e.g. X-Worker-Id). Off by "
331
+ "default: standard gateways do not return one."
332
+ ),
333
+ )
334
+ r.add_argument(
335
+ "--turn-timeout",
336
+ type=float,
337
+ default=3600.0,
338
+ help="Per-request timeout in seconds; a hung turn frees its slot "
339
+ "after this long (MLPerf's qwen config uses 14400).",
340
+ )
341
+ r.add_argument(
342
+ "--progress-interval",
343
+ type=float,
344
+ default=30.0,
345
+ help="Seconds between [progress] heartbeat lines (0 disables them).",
346
+ )
347
+ r.add_argument(
348
+ "--metrics-interval",
349
+ type=float,
350
+ default=5.0,
351
+ help="Seconds between /metrics scrapes for server-side KV pool stats "
352
+ "(0 disables; silently skipped if server lacks --enable-metrics).",
353
+ )
354
+ r.add_argument(
355
+ "--session-timeout",
356
+ type=float,
357
+ default=0.0,
358
+ help="Per-session wall-clock timeout in seconds (0 = disabled). "
359
+ "A session exceeding this is marked truncated.",
360
+ )
361
+ r.add_argument(
362
+ "--output-details",
363
+ action=argparse.BooleanOptionalAction,
364
+ default=True,
365
+ help="Include the per-turn records (TTFTs, ITLs, errors) in the "
366
+ "--output-file JSON. On by default -- a run you cannot "
367
+ "re-examine afterwards is a run you have to repeat; pass "
368
+ "--no-output-details to keep each line to config + metrics "
369
+ "when the full per-turn record is not wanted.",
370
+ )
371
+ r.add_argument("--output-file", default=None,
372
+ help="Append one JSON line per run (JSONL); per-turn records are "
373
+ "included by default (--no-output-details opts out).")
374
+ r.add_argument("--verbose", action="store_true",
375
+ help="Verbose per-turn console output.")
376
+ r.set_defaults(func=cmd_run)
377
+
378
+ _layer_help(sub)
379
+ return parser
380
+
381
+
382
+ def main() -> None:
383
+ parser = build_parser()
384
+ args = parser.parse_args(sys.argv[1:])
385
+ args.func(args)
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()
@@ -0,0 +1,72 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026, Alibaba Group and its affiliates
3
+
4
+ """Domain layer: session data, workload planning, result records.
5
+
6
+ The data model and the pure functions over it: no inference servers, no
7
+ telemetry, no command line. One exception — ``dataset`` fetches tier
8
+ files from the distribution mirror over plain ``urllib``, so that a bare
9
+ install can still read downloaded data.
10
+ """
11
+
12
+ from abench.core.dataset import (
13
+ TIERS,
14
+ NormSession,
15
+ NormTurn,
16
+ TierSpec,
17
+ download_tier,
18
+ iter_jsonl_sessions,
19
+ load_sessions,
20
+ resolve_url,
21
+ write_tier_manifest,
22
+ )
23
+ from abench.core.metrics import (
24
+ AgenticMetrics,
25
+ BucketStats,
26
+ SessionRecord,
27
+ TurnRecord,
28
+ compute_metrics,
29
+ summarize_server_metrics,
30
+ )
31
+ from abench.core.workload import (
32
+ PROFILES,
33
+ PlannedSession,
34
+ PlannedTurn,
35
+ Profile,
36
+ build_system_prompt,
37
+ make_gap_policy,
38
+ make_output_policy,
39
+ measure_text_tokens,
40
+ plan_workload,
41
+ select_sessions,
42
+ synthesize_sessions,
43
+ )
44
+
45
+ __all__ = [
46
+ "TIERS",
47
+ "TierSpec",
48
+ "NormSession",
49
+ "NormTurn",
50
+ "download_tier",
51
+ "iter_jsonl_sessions",
52
+ "load_sessions",
53
+ "resolve_url",
54
+ "write_tier_manifest",
55
+ "AgenticMetrics",
56
+ "BucketStats",
57
+ "SessionRecord",
58
+ "TurnRecord",
59
+ "compute_metrics",
60
+ "summarize_server_metrics",
61
+ "PROFILES",
62
+ "PlannedSession",
63
+ "PlannedTurn",
64
+ "Profile",
65
+ "build_system_prompt",
66
+ "make_gap_policy",
67
+ "make_output_policy",
68
+ "measure_text_tokens",
69
+ "plan_workload",
70
+ "select_sessions",
71
+ "synthesize_sessions",
72
+ ]