tuieval 0.2.0.dev87__tar.gz → 0.2.0.dev89__tar.gz
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.
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/CHANGELOG.md +1 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/writing-packs.md +3 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/engine.py +12 -5
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/packs.py +14 -7
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/answer/pack.toml +1 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/tui.py +4 -1
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/tests/mock_server.py +12 -2
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/tests/test_tuieval.py +106 -2
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/.gitignore +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/LICENSE +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/README.md +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/docs/models.md +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/compare.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/pta.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/yamlout.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- `max_tokens` in pack.toml sets one pack's output budget; changing it reruns only that pack. Models whose context or endpoint can't fit it are skipped for that pack.
|
|
5
6
|
- The run screen's queue keeps the running model in view, unless you scroll it yourself (scrolling back to it follows again).
|
|
6
7
|
- Start while a run is going skips models and packs already running or queued (it says which); picking only those adds nothing. Retest and picked questions still go through.
|
|
7
8
|
- Fix: something queued just as a run ended (while its server was stopping) now starts instead of waiting forever.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev89
|
|
4
4
|
Summary: Evaluate local and frontier LLMs on your own questions: accuracy, speed, tokens and PASS/FAIL verdicts, in the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ashe-wb/tuieval
|
|
6
6
|
Project-URL: Issues, https://github.com/ashe-wb/tuieval/issues
|
|
@@ -35,6 +35,7 @@ order = 30 # position in the list
|
|
|
35
35
|
tests = ["tests.yaml"] # optional; default: tests.* first, then every other .yaml/.csv file
|
|
36
36
|
screen = 20 # tests in a Screen run (spread across categories, one variant per group)
|
|
37
37
|
stop_early = true # stop a run of this pack once its FAIL is certain (default false; see below)
|
|
38
|
+
max_tokens = 32768 # optional: this pack's output budget, instead of models.toml [sampling] max_tokens
|
|
38
39
|
|
|
39
40
|
[certify]
|
|
40
41
|
repeat = 3 # repeats in a Certify run (default: models.toml [defaults] repeat)
|
|
@@ -53,6 +54,8 @@ min_consistency = 0.95 # share of variant groups where every variant p
|
|
|
53
54
|
|
|
54
55
|
`needs` entries other than `vision`, `tools` and `long_context` name Python modules the pack's grading needs (for example `pandas`, when your hidden tests use it). The pack is skipped, with a note, where that module isn't installed in tuieval's Python environment.
|
|
55
56
|
|
|
57
|
+
`max_tokens` suits packs whose answers are much longer (or shorter) than the rest. Thinking counts toward it. It's part of the pack's fingerprint, so changing it reruns only this pack. A model is skipped for the pack, with a note, when its context can't hold the longest prompt plus `max_tokens`, or when its pinned provider endpoint allows fewer output tokens.
|
|
58
|
+
|
|
56
59
|
## Tests (YAML)
|
|
57
60
|
|
|
58
61
|
```yaml
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0.dev89'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev89')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -646,8 +646,9 @@ def serves(ids, name):
|
|
|
646
646
|
|
|
647
647
|
|
|
648
648
|
def pack_min_context(pack):
|
|
649
|
-
"""Rough context a pack needs: its longest prompt (~4 chars/token) plus room to answer
|
|
650
|
-
|
|
649
|
+
"""Rough context a pack needs: its longest prompt (~4 chars/token) plus room to answer (the
|
|
650
|
+
pack's own max_tokens when it sets one)."""
|
|
651
|
+
return max(len(t["input"]) for t in pack.tests) // 4 + (pack.max_tokens or 4096)
|
|
651
652
|
|
|
652
653
|
|
|
653
654
|
class Cancelled(Exception):
|
|
@@ -872,7 +873,8 @@ class Engine:
|
|
|
872
873
|
needs_tools = m.get("tools") is not False and any("tools" in p.needs for p in self.packs.values())
|
|
873
874
|
ep = choose_endpoint(eps, m["model"], effective_sampling(self.cfg, m), needs_tools, previous)
|
|
874
875
|
ep = ep and {"tag": ep["tag"], "provider": ep.get("provider_name"),
|
|
875
|
-
"quantization": ep.get("quantization") or "unknown"
|
|
876
|
+
"quantization": ep.get("quantization") or "unknown",
|
|
877
|
+
"max_output": ep.get("max_completion_tokens")}
|
|
876
878
|
except (OSError, ValueError):
|
|
877
879
|
# offline: assume the endpoint earlier results used (the run itself will need the network)
|
|
878
880
|
ep = {"tag": previous, "provider": None, "quantization": previous_quant} if previous else None
|
|
@@ -1196,6 +1198,9 @@ class Engine:
|
|
|
1196
1198
|
elif (sv.ctx or 10**9) < pack_min_context(pack):
|
|
1197
1199
|
job.status, job.note = "skipped", \
|
|
1198
1200
|
f"needs ~{pack_min_context(pack) // 1024}k context, {sv.ctx // 1024}k fits on {sv.machine.id}"
|
|
1201
|
+
elif pack.max_tokens and pack.max_tokens > ((self.endpoint(m) or {}).get("max_output") or 10**9):
|
|
1202
|
+
job.status, job.note = "skipped", \
|
|
1203
|
+
f"needs {pack.max_tokens:,} output tokens, endpoint allows {self.endpoint(m)['max_output']:,}"
|
|
1199
1204
|
elif missing:
|
|
1200
1205
|
job.status, job.note = "skipped", f"{', '.join(missing)} not installed in tuieval's Python"
|
|
1201
1206
|
elif status and status.startswith("outdated"):
|
|
@@ -2037,7 +2042,8 @@ class Engine:
|
|
|
2037
2042
|
def _body(self, job, test, rep=0):
|
|
2038
2043
|
s = job.sampling
|
|
2039
2044
|
body = {"model": job.model["served_name"], "messages": self._messages(job, test),
|
|
2040
|
-
"temperature": s["temperature"], "top_p": s["top_p"],
|
|
2045
|
+
"temperature": s["temperature"], "top_p": s["top_p"],
|
|
2046
|
+
"max_tokens": job.pack.max_tokens or s["max_tokens"]}
|
|
2041
2047
|
for k in ("top_k", "min_p", "presence_penalty", "repeat_penalty", "seed", "reasoning_effort"):
|
|
2042
2048
|
if k in s:
|
|
2043
2049
|
body[k] = s[k]
|
|
@@ -2380,7 +2386,8 @@ class Engine:
|
|
|
2380
2386
|
"model": {**{k: m.get(k) for k in ("label", "served_name", "server", "model", "tags", "vision", "mmproj")},
|
|
2381
2387
|
**{k: m[k] for k in ("tools", "max_context", "thinking") if k in m}},
|
|
2382
2388
|
"pack": {"name": job.pack.name, "label": job.pack.label, "group": job.pack.group,
|
|
2383
|
-
"fingerprint": job.pack.fingerprint, "tests": len(job.pack.tests)
|
|
2389
|
+
"fingerprint": job.pack.fingerprint, "tests": len(job.pack.tests),
|
|
2390
|
+
**({"max_tokens": job.pack.max_tokens} if job.pack.max_tokens else {})},
|
|
2384
2391
|
"settings": job.sampling,
|
|
2385
2392
|
"serving": job.settings.get("serving"),
|
|
2386
2393
|
"settings_fingerprint": settings_fingerprint(job.settings),
|
|
@@ -50,6 +50,7 @@ class Pack:
|
|
|
50
50
|
screen: int # tests in a screening run
|
|
51
51
|
certify_repeat: int # repeats in a certification run (0 = models.toml default)
|
|
52
52
|
stop_early: bool = False # stop the pack once its FAIL is certain (pack.toml stop_early)
|
|
53
|
+
max_tokens: int = 0 # output budget for this pack's answers (0 = [sampling] max_tokens)
|
|
53
54
|
|
|
54
55
|
@property
|
|
55
56
|
def modules(self):
|
|
@@ -135,12 +136,15 @@ NOT_FINGERPRINTED = {"reference", "wrong", "description", "category", "critical"
|
|
|
135
136
|
DIFFICULTIES = ("easy", "medium", "hard")
|
|
136
137
|
|
|
137
138
|
|
|
138
|
-
def _fingerprint(system, tools, grader, tests, images):
|
|
139
|
-
"""Hash of what the model sees and what decides pass/fail.
|
|
139
|
+
def _fingerprint(system, tools, grader, tests, images, max_tokens=0):
|
|
140
|
+
"""Hash of what the model sees and what decides pass/fail. A pack's own max_tokens counts
|
|
141
|
+
(it decides what gets cut off); packs without one keep the fingerprint they always had."""
|
|
140
142
|
h = hashlib.sha256()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
what = {"system": system, "tools": tools, "grader": grader,
|
|
144
|
+
"tests": [{k: v for k, v in t.items() if k not in NOT_FINGERPRINTED} for t in tests]}
|
|
145
|
+
if max_tokens:
|
|
146
|
+
what["max_tokens"] = max_tokens
|
|
147
|
+
h.update(json.dumps(what, sort_keys=True, default=str).encode())
|
|
144
148
|
for path in sorted(images):
|
|
145
149
|
with open(path, "rb") as f:
|
|
146
150
|
h.update(f.read())
|
|
@@ -217,13 +221,16 @@ def load_pack(path):
|
|
|
217
221
|
if tools and "tools" not in needs:
|
|
218
222
|
needs = list(needs) + ["tools"]
|
|
219
223
|
gate = {"min_accuracy": 0.8, **meta.get("gate", {})}
|
|
224
|
+
max_tokens = meta.get("max_tokens", 0)
|
|
225
|
+
if "max_tokens" in meta and (isinstance(max_tokens, bool) or not isinstance(max_tokens, int) or max_tokens < 1):
|
|
226
|
+
raise PackError(f"{meta_path}: max_tokens must be a whole number above 0, not {max_tokens!r}")
|
|
220
227
|
return Pack(name=name, path=path, label=meta.get("label", name), group=meta.get("group", "Other"),
|
|
221
228
|
description=meta.get("description", ""), grader=meta.get("grader", "answer"),
|
|
222
229
|
system=system, needs=list(needs), tools=tools, tests=tests,
|
|
223
|
-
fingerprint=_fingerprint(system, tools, meta.get("grader", "answer"), tests, images),
|
|
230
|
+
fingerprint=_fingerprint(system, tools, meta.get("grader", "answer"), tests, images, max_tokens),
|
|
224
231
|
gate=gate, screen=int(meta.get("screen", 10)),
|
|
225
232
|
certify_repeat=int(meta.get("certify", {}).get("repeat", 0)),
|
|
226
|
-
stop_early=bool(meta.get("stop_early", False)))
|
|
233
|
+
stop_early=bool(meta.get("stop_early", False)), max_tokens=max_tokens)
|
|
227
234
|
|
|
228
235
|
|
|
229
236
|
def load_packs(packs_dir=None, errors=None):
|
|
@@ -6,6 +6,7 @@ grader = "answer" # how answers are scored (answer, rag, reply, t
|
|
|
6
6
|
system = "system.txt" # the system prompt sent with every question
|
|
7
7
|
order = 100 # position in the list
|
|
8
8
|
screen = 6 # tests in a Screen run
|
|
9
|
+
# max_tokens = 32768 # this pack's output budget, instead of models.toml [sampling] max_tokens
|
|
9
10
|
|
|
10
11
|
[certify]
|
|
11
12
|
repeat = 3 # times each question is asked in a Certify run
|
|
@@ -531,7 +531,8 @@ class AnswerScreen(ModalScreen):
|
|
|
531
531
|
partial = " [cyan](pack still running or unfinished: not in scores yet)[/cyan]" if entry.get("partial") else ""
|
|
532
532
|
quant = f" [magenta]{rec['quantization']}[/magenta]" if rec.get("quantization") else ""
|
|
533
533
|
finish = rec.get("finish") or "?"
|
|
534
|
-
|
|
534
|
+
limit = f"this pack's max_tokens, {pack.max_tokens:,}" if pack and pack.max_tokens else "max_tokens"
|
|
535
|
+
finish = f"[bold red]{finish} (cut off at {limit})[/]" if finish == "length" else finish
|
|
535
536
|
toks = rec.get("completion_tokens")
|
|
536
537
|
split = ", ".join(f"{k} {rec[k + '_tokens']:,}" for k in ("reasoning", "answer")
|
|
537
538
|
if rec.get(k + "_tokens") is not None)
|
|
@@ -709,6 +710,8 @@ class SetupScreen(Screen):
|
|
|
709
710
|
count = (f"[b cyan]{len(picked)}/{len(pk.tests)} tests picked[/]" if picked
|
|
710
711
|
else f"[dim]{len(pk.tests)} tests[/dim]")
|
|
711
712
|
needs = f" [magenta]{', '.join(pk.needs)}[/magenta]" if pk.needs else ""
|
|
713
|
+
if pk.max_tokens:
|
|
714
|
+
needs += f" [dim]max_tokens {pk.max_tokens:,}[/dim]"
|
|
712
715
|
mix = {lvl: sum(t.get("difficulty") == lvl for t in pk.tests) for lvl in ("easy", "medium", "hard")}
|
|
713
716
|
mixed = " ".join(f"[{DIFF_STYLE[l]}]{n}{l[0].upper()}[/]" for l, n in mix.items() if n)
|
|
714
717
|
packs.add_option(Selection(f"[dim]{pk.group} ·[/dim] {pk.label} {count} {mixed}{needs}",
|
|
@@ -9,6 +9,8 @@ fixed always "ANSWER: 42"
|
|
|
9
9
|
unavailable HTTP 404 "No endpoints found" for every chat request
|
|
10
10
|
|
|
11
11
|
Streams like llama.cpp: reasoning_content then content deltas, usage and timings at the end.
|
|
12
|
+
An answer longer than the request's max_tokens (one token per word) is cut off with finish
|
|
13
|
+
"length". --max-output N also serves an OpenRouter-style endpoints list with that output limit.
|
|
12
14
|
"""
|
|
13
15
|
import argparse
|
|
14
16
|
import json
|
|
@@ -47,7 +49,7 @@ def reply_for(test, mode):
|
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class Handler(BaseHTTPRequestHandler):
|
|
50
|
-
answers, mode, model, delay = {}, "oracle", "mock", 0.0
|
|
52
|
+
answers, mode, model, delay, max_output = {}, "oracle", "mock", 0.0, None
|
|
51
53
|
|
|
52
54
|
def log_message(self, *a):
|
|
53
55
|
pass
|
|
@@ -63,6 +65,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
63
65
|
def do_GET(self):
|
|
64
66
|
if self.path.rstrip("/") in ("/v1/models", "/models"):
|
|
65
67
|
return self._json({"data": [{"id": self.model}]})
|
|
68
|
+
if self.max_output and self.path.rstrip("/").endswith("/endpoints"):
|
|
69
|
+
return self._json({"data": {"endpoints": [{"tag": "mock/fp16", "quantization": "fp16", "status": 0,
|
|
70
|
+
"max_completion_tokens": self.max_output}]}})
|
|
66
71
|
if self.path == "/health":
|
|
67
72
|
return self._json({"status": "ok", "model": self.model})
|
|
68
73
|
self._json({"error": "not found"}, 404)
|
|
@@ -82,6 +87,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
82
87
|
if self.mode == "unavailable": # like OpenRouter with no provider serving the model
|
|
83
88
|
return self._json({"error": {"message": f"No endpoints found for {self.model}.", "code": 404}}, 404)
|
|
84
89
|
text, calls = reply_for(test, self.mode)
|
|
90
|
+
finish = "tool_calls" if calls else "stop"
|
|
91
|
+
if req.get("max_tokens") and len(text.split()) > req["max_tokens"]:
|
|
92
|
+
text, finish = " ".join(text.split()[:req["max_tokens"]]), "length"
|
|
85
93
|
self.send_response(200)
|
|
86
94
|
self.send_header("Content-Type", "text/event-stream")
|
|
87
95
|
self.end_headers()
|
|
@@ -98,7 +106,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
98
106
|
send({"choices": [{"delta": {"tool_calls": [{"index": i, "function": {
|
|
99
107
|
"name": c["name"], "arguments": json.dumps(c.get("arguments") or {})}}]}}]})
|
|
100
108
|
words = max(1, len(text.split()) + 3)
|
|
101
|
-
send({"choices": [{"delta": {}, "finish_reason":
|
|
109
|
+
send({"choices": [{"delta": {}, "finish_reason": finish}],
|
|
102
110
|
"usage": {"prompt_tokens": len(str(content).split()), "completion_tokens": words},
|
|
103
111
|
"timings": {"predicted_n": words, "predicted_per_second": 50.0, "prompt_n": 10,
|
|
104
112
|
"prompt_per_second": 500.0, "cache_n": 0}})
|
|
@@ -119,8 +127,10 @@ def main(argv=None):
|
|
|
119
127
|
p.add_argument("--mode", choices=("oracle", "wrong", "fixed", "unavailable"), default="oracle")
|
|
120
128
|
p.add_argument("--model", default="mock")
|
|
121
129
|
p.add_argument("--delay", type=float, default=0.0, help="seconds each answer takes")
|
|
130
|
+
p.add_argument("--max-output", type=int)
|
|
122
131
|
a, _ = p.parse_known_args(argv) # other flags (e.g. speed knobs under test) are accepted and ignored
|
|
123
132
|
Handler.answers, Handler.mode, Handler.model, Handler.delay = load_answers(a.packs), a.mode, a.model, a.delay
|
|
133
|
+
Handler.max_output = a.max_output
|
|
124
134
|
srv = Server(("127.0.0.1", a.port), Handler)
|
|
125
135
|
print(f"mock server on 127.0.0.1:{a.port} ({a.mode}, {len(Handler.answers)} known questions)", flush=True)
|
|
126
136
|
try:
|
|
@@ -49,10 +49,11 @@ def free_port():
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class Mock:
|
|
52
|
-
def __init__(self, packs, mode, port=None, delay=0.0):
|
|
52
|
+
def __init__(self, packs, mode, port=None, delay=0.0, *args):
|
|
53
53
|
self.port = port or free_port()
|
|
54
54
|
self.proc = subprocess.Popen([sys.executable, os.path.join(HERE, "mock_server.py"), "--port", str(self.port),
|
|
55
|
-
"--packs", packs, "--mode", mode, "--delay", str(delay)],
|
|
55
|
+
"--packs", packs, "--mode", mode, "--delay", str(delay), *args],
|
|
56
|
+
stdout=subprocess.DEVNULL)
|
|
56
57
|
for _ in range(100):
|
|
57
58
|
try:
|
|
58
59
|
urllib.request.urlopen(f"http://127.0.0.1:{self.port}/v1/models", timeout=1)
|
|
@@ -1815,6 +1816,109 @@ class FirstPack(unittest.TestCase):
|
|
|
1815
1816
|
self.assertIn("→ fix the expected answer", out)
|
|
1816
1817
|
|
|
1817
1818
|
|
|
1819
|
+
class PackMaxTokens(unittest.TestCase):
|
|
1820
|
+
"""A pack's own max_tokens: sent with its requests, part of its fingerprint only, and packs that
|
|
1821
|
+
can't fit it (context, a hosted endpoint's output limit) are skipped."""
|
|
1822
|
+
LONG = " ".join(["word"] * 40) + "\nANSWER: 7" # ~42 tokens at the mock's one token per word
|
|
1823
|
+
|
|
1824
|
+
def setUp(self):
|
|
1825
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
1826
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
1827
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
1828
|
+
for name, extra in (("short", "max_tokens = 20\n"), ("roomy", "max_tokens = 100\n"), ("plain", "")):
|
|
1829
|
+
self.pack(name, extra)
|
|
1830
|
+
|
|
1831
|
+
def tearDown(self):
|
|
1832
|
+
self.tmp.cleanup()
|
|
1833
|
+
|
|
1834
|
+
def pack(self, name, extra):
|
|
1835
|
+
d = os.path.join(self.ws, "packs", name)
|
|
1836
|
+
os.makedirs(d, exist_ok=True)
|
|
1837
|
+
write(os.path.join(d, "pack.toml"), f'label = "{name}"\n{extra}[gate]\nmin_accuracy = 0.1\n')
|
|
1838
|
+
write(os.path.join(d, "tests.yaml"), f"- id: q\n input: {name} question\n expected: 7\n"
|
|
1839
|
+
f" difficulty: easy\n reference: {json.dumps(self.LONG)}\n")
|
|
1840
|
+
|
|
1841
|
+
def engine(self, code):
|
|
1842
|
+
return subprocess.run([sys.executable, "-c", "from tuieval import engine, packs; e = engine.Engine(); " + code],
|
|
1843
|
+
capture_output=True, text=True, env=dict(os.environ, TUIEVAL_HOME=self.ws))
|
|
1844
|
+
|
|
1845
|
+
def result(self, label, pack):
|
|
1846
|
+
path = os.path.join(self.ws, "results", label, f"{pack}.json")
|
|
1847
|
+
return json.loads(read(path)) if os.path.isfile(path) else None
|
|
1848
|
+
|
|
1849
|
+
def test_load_and_fingerprint(self):
|
|
1850
|
+
from tuieval import packs
|
|
1851
|
+
d = os.path.join(self.ws, "packs", "plain")
|
|
1852
|
+
before = packs.load_pack(d)
|
|
1853
|
+
self.assertEqual(before.max_tokens, 0)
|
|
1854
|
+
self.pack("plain", "") # rewritten unchanged: same fingerprint as before the feature
|
|
1855
|
+
self.assertEqual(packs.load_pack(d).fingerprint, before.fingerprint)
|
|
1856
|
+
self.pack("plain", "max_tokens = 4096\n")
|
|
1857
|
+
p = packs.load_pack(d)
|
|
1858
|
+
self.assertEqual(p.max_tokens, 4096)
|
|
1859
|
+
self.assertNotEqual(p.fingerprint, before.fingerprint)
|
|
1860
|
+
self.pack("plain", "max_tokens = 8192\n")
|
|
1861
|
+
self.assertNotEqual(packs.load_pack(d).fingerprint, p.fingerprint)
|
|
1862
|
+
for bad in ("0", "-5", '"lots"', "1.5", "true"):
|
|
1863
|
+
self.pack("plain", f"max_tokens = {bad}\n")
|
|
1864
|
+
with self.assertRaises(packs.PackError, msg=bad):
|
|
1865
|
+
packs.load_pack(d)
|
|
1866
|
+
self.assertIn("max_tokens must be", tuieval(self.ws, "selftest", check=False).stdout)
|
|
1867
|
+
|
|
1868
|
+
def test_requests_use_the_packs_budget(self):
|
|
1869
|
+
mock = Mock(os.path.join(self.ws, "packs"), "oracle")
|
|
1870
|
+
try:
|
|
1871
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
1872
|
+
f.write(f'\n[servers.m]\nurl = "http://127.0.0.1:{mock.port}"\n')
|
|
1873
|
+
tuieval(self.ws, "add", "mock", "--server", "m", "--label", "m")
|
|
1874
|
+
tuieval(self.ws, "run", "--tier", "certify")
|
|
1875
|
+
res = {p: self.result("m", p) for p in ("short", "roomy", "plain")}
|
|
1876
|
+
self.assertEqual(res["short"]["results"][0]["finish"], "length") # 42 tokens > 20
|
|
1877
|
+
self.assertIn("TRUNCATED", res["short"]["results"][0]["reason"])
|
|
1878
|
+
self.assertTrue(res["roomy"]["results"][0]["pass"])
|
|
1879
|
+
self.assertTrue(res["plain"]["results"][0]["pass"]) # [sampling] max_tokens
|
|
1880
|
+
self.assertEqual(res["short"]["pack"]["max_tokens"], 20)
|
|
1881
|
+
self.assertNotIn("max_tokens", res["plain"]["pack"])
|
|
1882
|
+
self.assertEqual(res["short"]["settings"], res["plain"]["settings"]) # model settings untouched
|
|
1883
|
+
|
|
1884
|
+
# changing one pack's budget makes only that pack's results outdated
|
|
1885
|
+
self.pack("short", "max_tokens = 50\n")
|
|
1886
|
+
out = self.engine("print(e.result_status('m', 'short')); print(e.result_status('m', 'plain'))").stdout
|
|
1887
|
+
self.assertEqual(out.splitlines(), ["outdated: questions changed", "certified"])
|
|
1888
|
+
tuieval(self.ws, "run", "--tier", "certify")
|
|
1889
|
+
self.assertTrue(self.result("m", "short")["results"][0]["pass"])
|
|
1890
|
+
finally:
|
|
1891
|
+
mock.stop()
|
|
1892
|
+
|
|
1893
|
+
def test_skipped_when_the_context_is_too_small(self):
|
|
1894
|
+
self.pack("short", "max_tokens = 16000\n")
|
|
1895
|
+
mock = Mock(os.path.join(self.ws, "packs"), "oracle")
|
|
1896
|
+
try:
|
|
1897
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
1898
|
+
f.write(f'\n[servers.m]\nurl = "http://127.0.0.1:{mock.port}"\n'
|
|
1899
|
+
'\n[[models]]\nlabel = "small"\nserver = "m"\nmodel = "mock"\nmax_context = 8192\n')
|
|
1900
|
+
out = tuieval(self.ws, "run", "--tier", "certify").stdout
|
|
1901
|
+
finally:
|
|
1902
|
+
mock.stop()
|
|
1903
|
+
self.assertIsNone(self.result("small", "short"))
|
|
1904
|
+
self.assertIn("needs ~15k context", out)
|
|
1905
|
+
self.assertIsNotNone(self.result("small", "plain"))
|
|
1906
|
+
|
|
1907
|
+
def test_skipped_when_the_endpoint_allows_fewer_output_tokens(self):
|
|
1908
|
+
mock = Mock(os.path.join(self.ws, "packs"), "oracle", None, 0.0, "--max-output", "50")
|
|
1909
|
+
try:
|
|
1910
|
+
with open(os.path.join(self.ws, "models.toml"), "a") as f:
|
|
1911
|
+
f.write(f'\n[servers.hosted]\nurl = "http://127.0.0.1:{mock.port}/v1"\npin_endpoint = true\n'
|
|
1912
|
+
'\n[[models]]\nlabel = "h"\nserver = "hosted"\nmodel = "mock"\n')
|
|
1913
|
+
out = tuieval(self.ws, "run", "--tier", "certify").stdout
|
|
1914
|
+
finally:
|
|
1915
|
+
mock.stop()
|
|
1916
|
+
self.assertIsNone(self.result("h", "roomy"))
|
|
1917
|
+
self.assertIn("needs 100 output tokens, endpoint allows 50", out)
|
|
1918
|
+
self.assertIsNotNone(self.result("h", "short"))
|
|
1919
|
+
self.assertIsNotNone(self.result("h", "plain"))
|
|
1920
|
+
|
|
1921
|
+
|
|
1818
1922
|
def fake_gguf(path, embedding=5120, size=0, kv_heads=4):
|
|
1819
1923
|
"""A GGUF header with just the keys machines.read_gguf reads, padded to size bytes. kv_heads: a
|
|
1820
1924
|
number, or one per layer (a list, as some GGUFs store it)."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev87 → tuieval-0.2.0.dev89}/src/tuieval/templates/packs/tool_call/tools.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|