tuieval 0.2.0.dev121__tar.gz → 0.2.0.dev123__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.dev121 → tuieval-0.2.0.dev123}/CHANGELOG.md +1 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/PKG-INFO +1 -1
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/models.md +2 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/builtin.toml +1 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/client.py +20 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/compare.py +35 -7
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/engine.py +15 -3
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/insights.py +1 -1
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/pta.py +13 -2
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/tui.py +7 -2
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/tests/test_tuieval.py +75 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/.gitignore +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/LICENSE +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/README.md +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/dates.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/logs.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/tests/mock_server.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Answers that used more tokens than the `max_tokens` sent (a provider that didn't stop them) are marked over budget: `(n over budget)` in the PTA index, an **over budget** column, `over` in Per question and a note in the answer view. `[defaults] over_budget = "fail"` fails them instead. Works on stored results.
|
|
5
6
|
- Fix: Results → Per question opened the answers of the model one column to the right of the cell picked, and crashed on the last model's column.
|
|
6
7
|
- Fix: `v` on the run screen switches to the last model still running when the stream shows one that has finished.
|
|
7
8
|
- Setup opens with no models ticked: at launch, after New run, and after Back from a run or tune. Packs and options are still remembered.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev123
|
|
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
|
|
@@ -105,6 +105,7 @@ The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a
|
|
|
105
105
|
- **P parsimony (tokens)**: the tokens used, reasoning and answer, on every question compared (each question at its median over repeats). Beside the bar: how many times the leanest model's, e.g. `2.4×` (`1.0×` is the leanest, ★ in the table's **tokens/answer**). Unlike time it doesn't depend on your machine: it's how much a model says to get to an answer, which is what you pay for on an API and what fills its context.
|
|
106
106
|
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
107
107
|
- **A accuracy**: the share of answers to those questions that passed. Answers cut off at `max_tokens` count as wrong, but show in orange after the green, with `(n% truncated)` beside the number and a **truncated** column in the table: those failures are a model out of tokens (overthinking), not a wrong answer yet. A pack's own `max_tokens` gives it more room.
|
|
108
|
+
- **Over budget**: some providers don't stop a model at `max_tokens` (thinking goes on past it), so the answer isn't cut off but the model had more room than the others. Answers that used more tokens than were sent as `max_tokens` show as `(n over budget)` beside the accuracy, an **over budget** column, `over` in Results → Per question and a note in the answer view. They're only flagged; `over_budget = "fail"` in `[defaults]` counts them as failed instead (stored results aren't changed, so switching back restores them).
|
|
108
109
|
|
|
109
110
|
The P and T bars are as long as the tokens and time used: the model that used the most fills its bar, the others in proportion (at least one block), so **shorter is better**. The A bar is the share of answers right, so **longer is better**; it's full only when every answer passed. Each heading says which way is better.
|
|
110
111
|
|
|
@@ -207,6 +208,7 @@ A workspace's `models.toml` only needs what differs from these. `[defaults]` fil
|
|
|
207
208
|
repeat = 3
|
|
208
209
|
request_timeout_ms = 1800000 # per request; long thinking runs need it
|
|
209
210
|
stall_timeout_ms = 300000 # no tokens this long once answering began: the server stalled (retried, not counted)
|
|
211
|
+
over_budget = "flag" # answers past the max_tokens sent (a provider that didn't stop them): "flag" or "fail"
|
|
210
212
|
# reference = "<label>" # the model Results → Insights compares against by default
|
|
211
213
|
ready_timeout_s = 1200 # how long a model may take to load before giving up
|
|
212
214
|
model_dirs = ["~/models"] # where "Scan for models" looks for new GGUFs
|
|
@@ -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.dev123'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev123')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
repeat = 3
|
|
7
7
|
request_timeout_ms = 1800000 # per request; long thinking runs need it
|
|
8
8
|
stall_timeout_ms = 300000 # no tokens this long once answering began: the server stalled (retried, not counted)
|
|
9
|
+
over_budget = "flag" # answers past the max_tokens sent (a provider that didn't stop them): "flag" or "fail"
|
|
9
10
|
ready_timeout_s = 1200 # how long a model may take to load before giving up
|
|
10
11
|
model_dirs = ["~/models"] # where "Scan for models" looks for new GGUFs
|
|
11
12
|
|
|
@@ -42,6 +42,26 @@ def server_error_row(rec):
|
|
|
42
42
|
return not rec.get("pass") and (is_server_error(rec.get("reason")) or is_unavailable(rec.get("reason")))
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
OVER_BUDGET_MODES = ("flag", "fail")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def mark_over_budget(records, limit, mode="flag"):
|
|
49
|
+
"""Answers that used more tokens than the max_tokens sent and weren't cut off there: some providers
|
|
50
|
+
don't stop thinking at the limit. Each gets `over_budget` = the limit; with mode "fail", a passing
|
|
51
|
+
one fails. Estimated token counts are never flagged."""
|
|
52
|
+
if not limit:
|
|
53
|
+
return records
|
|
54
|
+
for r in records:
|
|
55
|
+
if r.get("finish") == "length" or r.get("tokens_estimated") or (r.get("completion_tokens") or 0) <= limit:
|
|
56
|
+
continue
|
|
57
|
+
r["over_budget"] = limit
|
|
58
|
+
if mode == "fail" and r.get("pass"):
|
|
59
|
+
r.update({"pass": False, "score": 0.0, "severity": None,
|
|
60
|
+
"reason": f"OVER BUDGET: {r['completion_tokens']:,} tokens, over the {limit:,} limit sent; "
|
|
61
|
+
+ r.get("reason", "")})
|
|
62
|
+
return records
|
|
63
|
+
|
|
64
|
+
|
|
45
65
|
class Cancelled(Exception):
|
|
46
66
|
pass
|
|
47
67
|
|
|
@@ -51,6 +51,8 @@ def _load_native(data):
|
|
|
51
51
|
"ok": bool(r["pass"]), "score": float(r["score"]), "reason": r["reason"],
|
|
52
52
|
"error": r["reason"].startswith(("server returned", "connection error")),
|
|
53
53
|
"truncated": r.get("finish") == "length", "output": r.get("answer") or "",
|
|
54
|
+
# tokens past the max_tokens sent, which the provider didn't stop (client.mark_over_budget)
|
|
55
|
+
"over_budget": r.get("over_budget"),
|
|
54
56
|
"tokens": r.get("completion_tokens") or 0, "reasoning_tokens": r.get("reasoning_tokens"),
|
|
55
57
|
# a reused (cached) prompt makes time-to-first-token and prompt speed look faster than they are
|
|
56
58
|
"answer_tokens": r.get("answer_tokens"), "ttft": None if r.get("cached_tokens") else r.get("ttft_s"),
|
|
@@ -67,13 +69,16 @@ def _load_native(data):
|
|
|
67
69
|
|
|
68
70
|
def load(paths):
|
|
69
71
|
"""(rows, infos): one row per answer; one info per (model, result file)."""
|
|
72
|
+
from . import engine
|
|
70
73
|
rows, infos = [], []
|
|
74
|
+
mode = over_budget_mode(paths)
|
|
71
75
|
for path in paths:
|
|
72
76
|
with open(path) as f:
|
|
73
77
|
data = json.load(f)
|
|
74
78
|
if data.get("format") != "evals/1":
|
|
75
79
|
continue
|
|
76
80
|
data["results"] = [r for r in data["results"] if not client.server_error_row(r)]
|
|
81
|
+
client.mark_over_budget(data["results"], engine.sent_max_tokens(data), mode)
|
|
77
82
|
r, i = _load_native(data)
|
|
78
83
|
for row, rec in zip(r, data["results"]):
|
|
79
84
|
row.update(raw=rec, path=path, pack_fp=data["pack"]["fingerprint"])
|
|
@@ -93,17 +98,31 @@ def load(paths):
|
|
|
93
98
|
return rows, infos
|
|
94
99
|
|
|
95
100
|
|
|
101
|
+
def _configs(paths):
|
|
102
|
+
"""The models.toml configs of the workspaces these results/<label>/<pack>.json are from (unreadable
|
|
103
|
+
ones left out)."""
|
|
104
|
+
from . import engine
|
|
105
|
+
out = []
|
|
106
|
+
for root in sorted({os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(p)))) for p in paths}):
|
|
107
|
+
try:
|
|
108
|
+
out.append(engine.load_config(os.path.join(root, "models.toml")))
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def over_budget_mode(paths):
|
|
115
|
+
"""[defaults] over_budget of the workspace(s) the results are from: "fail" if any says so."""
|
|
116
|
+
return "fail" if any(c["defaults"]["over_budget"] == "fail" for c in _configs(paths)) else "flag"
|
|
117
|
+
|
|
118
|
+
|
|
96
119
|
def configured_hosted(paths):
|
|
97
120
|
"""(labels, label prefixes) models.toml says are hosted, in the workspaces these results/<label>/
|
|
98
121
|
<pack>.json are from: its hosted models, and the prefix of models named at run time on a hosted
|
|
99
122
|
server (e.g. "or-"). Covers hosted models whose results aren't loaded."""
|
|
100
123
|
from . import engine
|
|
101
124
|
labels, prefixes = set(), set()
|
|
102
|
-
for
|
|
103
|
-
try:
|
|
104
|
-
cfg = engine.load_config(os.path.join(root, "models.toml"))
|
|
105
|
-
except Exception:
|
|
106
|
-
continue # no readable models.toml there: the results' own marks still apply
|
|
125
|
+
for cfg in _configs(paths): # no readable models.toml: the results' own marks still apply
|
|
107
126
|
labels |= {m["label"] for m in cfg["models"] if engine.hosted(cfg, m)}
|
|
108
127
|
prefixes |= {engine.remote_label(n, s, "") for n, s in cfg["servers"].items()
|
|
109
128
|
if s.get("any_model") and engine.hosted(cfg, {"server": n})}
|
|
@@ -125,6 +144,7 @@ def load_partials(results_dir=None):
|
|
|
125
144
|
<pack>.json.partial.jsonl, marked partial. They never count toward scores or verdicts."""
|
|
126
145
|
results_dir = results_dir or workspace.path("results")
|
|
127
146
|
rows = []
|
|
147
|
+
mode = over_budget_mode([os.path.join(results_dir, "x", "x.json")])
|
|
128
148
|
for path in sorted(glob.glob(os.path.join(results_dir, "*", "*" + PARTIAL_SUFFIX))):
|
|
129
149
|
label = os.path.basename(os.path.dirname(path))
|
|
130
150
|
if label in ("smoke", "archive"):
|
|
@@ -137,6 +157,9 @@ def load_partials(results_dir=None):
|
|
|
137
157
|
if len(lines) < 2:
|
|
138
158
|
continue
|
|
139
159
|
head, recs = lines[0], [r for r in lines[1:] if not client.server_error_row(r)]
|
|
160
|
+
# the limit sent: recorded in the header (older ones: the model's own)
|
|
161
|
+
client.mark_over_budget(recs, head.get("max_tokens") or ((head.get("settings") or {}).get("sampling") or {})
|
|
162
|
+
.get("max_tokens"), mode)
|
|
140
163
|
data = {"model": {"label": label}, "pack": {"name": os.path.basename(path)[:-len(PARTIAL_SUFFIX)],
|
|
141
164
|
"fingerprint": head.get("pack_fingerprint")},
|
|
142
165
|
"run": {}, "results": recs}
|
|
@@ -222,7 +245,9 @@ def scorecard(rows, rng=None):
|
|
|
222
245
|
models = sorted({r["model"] for r in rows})
|
|
223
246
|
suites = sorted({r["suite"] for r in rows})
|
|
224
247
|
extra = dict(graders.COLUMNS) # columns your graders add (graders.scorecard_column)
|
|
225
|
-
|
|
248
|
+
over = any(r.get("over_budget") for r in rows) # a column only when some answer ran past max_tokens
|
|
249
|
+
header = ["model"] + suites + ["overall", "95% CI", "mean score", "all repeats", *extra, "errors", "trunc",
|
|
250
|
+
*(["over budget"] if over else []), "rep"]
|
|
226
251
|
table = []
|
|
227
252
|
for m in models:
|
|
228
253
|
mine = [r for r in rows if r["model"] == m]
|
|
@@ -243,6 +268,7 @@ def scorecard(rows, rng=None):
|
|
|
243
268
|
cell = "error"
|
|
244
269
|
line.append("-" if cell is None else str(cell))
|
|
245
270
|
line += [str(sum(r["error"] for r in mine)), str(sum(r["truncated"] for r in mine)),
|
|
271
|
+
*([str(sum(bool(r.get("over_budget")) for r in mine))] if over else []),
|
|
246
272
|
str(sum(repetitive(r["output"]) for r in mine))]
|
|
247
273
|
table.append(line)
|
|
248
274
|
return header, table
|
|
@@ -339,6 +365,7 @@ def per_question(rows):
|
|
|
339
365
|
for c in t["cells"].values():
|
|
340
366
|
v = sorted(c["rows"], key=lambda r: r.get("repeat", 0))
|
|
341
367
|
c.update(rows=v, n=len(v), passed=sum(r["ok"] for r in v), truncated=sum(r["truncated"] for r in v),
|
|
368
|
+
over=sum(bool(r.get("over_budget")) for r in v),
|
|
342
369
|
partial=any(r.get("partial") for r in v),
|
|
343
370
|
tokens=median(r["tokens"] or None for r in v), secs=median(r["latency"] / 1000 or None for r in v),
|
|
344
371
|
tps=median(r["gen_tps"] for r in v))
|
|
@@ -386,7 +413,8 @@ def compare_models(tests, models):
|
|
|
386
413
|
"med_tokens": median(c["tokens"] for c in cs), "med_secs": median(c["secs"] for c in cs),
|
|
387
414
|
"total_secs": sum(c["secs"] or 0 for c in cs) or None,
|
|
388
415
|
"tokens_per_pass": total / passed if passed else None,
|
|
389
|
-
"fewest": fewest, "fastest": fastest, "cut": sum(c["truncated"] for c in cs)
|
|
416
|
+
"fewest": fewest, "fastest": fastest, "cut": sum(c["truncated"] for c in cs),
|
|
417
|
+
"over": sum(c.get("over", 0) for c in cs)})
|
|
390
418
|
return out
|
|
391
419
|
|
|
392
420
|
|
|
@@ -107,6 +107,9 @@ def load_config(path):
|
|
|
107
107
|
cfg = tomllib.load(f)
|
|
108
108
|
builtin = builtin_config()
|
|
109
109
|
cfg["defaults"] = {**builtin["defaults"], **cfg.get("defaults", {})}
|
|
110
|
+
if cfg["defaults"]["over_budget"] not in client.OVER_BUDGET_MODES:
|
|
111
|
+
raise ConfigError(f"{path}: [defaults] over_budget must be one of "
|
|
112
|
+
f"{', '.join(map(repr, client.OVER_BUDGET_MODES))}, not {cfg['defaults']['over_budget']!r}")
|
|
110
113
|
cfg.setdefault("sampling", builtin["sampling"])
|
|
111
114
|
own = cfg.get("servers", {})
|
|
112
115
|
cfg["servers"] = {**builtin["servers"], **own}
|
|
@@ -528,6 +531,11 @@ def read_result(path):
|
|
|
528
531
|
return data
|
|
529
532
|
|
|
530
533
|
|
|
534
|
+
def sent_max_tokens(data):
|
|
535
|
+
"""The max_tokens a result file's requests were sent with: the pack's own, else the model's."""
|
|
536
|
+
return (data.get("pack") or {}).get("max_tokens") or (data.get("settings") or {}).get("max_tokens")
|
|
537
|
+
|
|
538
|
+
|
|
531
539
|
def server_error(log, start):
|
|
532
540
|
"""Why a server stopped while loading: the last error line it wrote since start, when it
|
|
533
541
|
names one (some servers refuse a context that does not fit with one)."""
|
|
@@ -925,9 +933,13 @@ class Engine:
|
|
|
925
933
|
return cached[1]
|
|
926
934
|
|
|
927
935
|
def records(self, label, pack_name):
|
|
928
|
-
"""Stored result rows for a model and pack, or [] (smoke runs excluded)
|
|
936
|
+
"""Stored result rows for a model and pack, or [] (smoke runs excluded), as scored: copies, with
|
|
937
|
+
answers past the max_tokens sent marked (and failed, with [defaults] over_budget = "fail")."""
|
|
929
938
|
data = self._result(self.result_path(label, pack_name))
|
|
930
|
-
|
|
939
|
+
if not data:
|
|
940
|
+
return []
|
|
941
|
+
return client.mark_over_budget([dict(r) for r in data["results"]], sent_max_tokens(data),
|
|
942
|
+
self.cfg["defaults"]["over_budget"])
|
|
931
943
|
|
|
932
944
|
def certify_repeat(self, pack):
|
|
933
945
|
return pack.certify_repeat or self.cfg["defaults"]["repeat"]
|
|
@@ -1126,7 +1138,7 @@ class Engine:
|
|
|
1126
1138
|
|
|
1127
1139
|
def _partial_header(self, job):
|
|
1128
1140
|
return {"pack_fingerprint": job.pack.fingerprint, "settings_fingerprint": settings_fingerprint(job.settings),
|
|
1129
|
-
"settings": job.settings}
|
|
1141
|
+
"settings": job.settings, "max_tokens": job.pack.max_tokens or job.sampling["max_tokens"]}
|
|
1130
1142
|
|
|
1131
1143
|
def _header_ok(self, head, job):
|
|
1132
1144
|
return header_matches(head, job.pack.fingerprint, job.settings)
|
|
@@ -278,7 +278,7 @@ def kind(r):
|
|
|
278
278
|
return "right"
|
|
279
279
|
if r.get("severity") == "critical":
|
|
280
280
|
return "critical"
|
|
281
|
-
if r["truncated"] or r["reason"].startswith("TRUNCATED"):
|
|
281
|
+
if r["truncated"] or r["reason"].startswith(("TRUNCATED", "OVER BUDGET")):
|
|
282
282
|
return "cut off"
|
|
283
283
|
if r["reason"].startswith(FORMAT):
|
|
284
284
|
return "no answer / format"
|
|
@@ -78,6 +78,8 @@ def index(rows, models=None, memory=None):
|
|
|
78
78
|
# failed only because max_tokens ran out (overthinking, not wrong yet)
|
|
79
79
|
"cut": sum(c["truncated"] for c in cells),
|
|
80
80
|
"cut_pct": 100 * sum(c["truncated"] for c in cells) / answers if answers else None,
|
|
81
|
+
# ran past the max_tokens sent: the provider didn't stop it, so it had more room than asked
|
|
82
|
+
"over": sum(c.get("over", 0) for c in cells),
|
|
81
83
|
# generation speed and time to first token over the same answers (medians)
|
|
82
84
|
"tps": compare.median(r.get("gen_tps") for c in cells for r in c["rows"]),
|
|
83
85
|
"ttft": compare.median(r.get("ttft") for c in cells for r in c["rows"]),
|
|
@@ -147,6 +149,9 @@ def bars(result, width=20, markup=True, click=None):
|
|
|
147
149
|
part = min(width - full, max(1, round(width * cut / 100))) if cut else 0
|
|
148
150
|
if cut: # after the accuracy, which stays aligned with the other rows'
|
|
149
151
|
num = f"{num:>6} ({compare.pct(cut / 100)} truncated)"
|
|
152
|
+
if x.get("over"):
|
|
153
|
+
note = f"({x['over']} over budget)"
|
|
154
|
+
num = (f"{num} " if cut else f"{num:>6} ") + (f"[yellow]{note}[/yellow]" if markup else note)
|
|
150
155
|
else:
|
|
151
156
|
full = _blocks(width, x.get(measure), most[measure])
|
|
152
157
|
num = times(x.get(shown))
|
|
@@ -183,10 +188,13 @@ def left_out_lines(result):
|
|
|
183
188
|
def table(result):
|
|
184
189
|
"""(header, rows) for a plain table of the index."""
|
|
185
190
|
n = result["questions"] or 1
|
|
186
|
-
|
|
191
|
+
over = any(x.get("over") for x in result["models"]) # a column only when some answer ran past max_tokens
|
|
192
|
+
header = ["model", "tokens/answer", "time/answer", "answers right", "truncated", *(["over budget"] if over else []),
|
|
193
|
+
"tok/s", "TTFT s", "memory GB"]
|
|
187
194
|
rows = [[x["model"], f"{round(x['tokens'] / n):,}" + (" ★" if x.get("fewest") else ""),
|
|
188
195
|
fmt_secs(x["total_s"] / n) + (" ★" if x.get("fastest") else ""),
|
|
189
196
|
f"{x['passed']}/{x['answers']}", f"{x.get('cut') or 0}/{x['answers']}",
|
|
197
|
+
*([f"{x.get('over') or 0}/{x['answers']}"] if over else []),
|
|
190
198
|
_num(x.get("tps"), ".0f"), _num(x.get("ttft"), ".1f"),
|
|
191
199
|
_num(x.get("memory_gb"), ".1f")] for x in result["models"]]
|
|
192
200
|
return header, rows
|
|
@@ -222,7 +230,10 @@ def scope_note(result, models):
|
|
|
222
230
|
"and how many times the leanest / fastest model's (1.0×, ★ in the table). A accuracy: answers right "
|
|
223
231
|
"(longer is better)"
|
|
224
232
|
+ ("; orange, (n% truncated): answers cut off at max_tokens. They count as wrong, but the model ran "
|
|
225
|
-
"out of tokens rather than answering wrong." if any(x.get("cut") for x in result["models"]) else ".")
|
|
233
|
+
"out of tokens rather than answering wrong." if any(x.get("cut") for x in result["models"]) else ".")
|
|
234
|
+
+ (" Over budget: answers that used more tokens than the max_tokens sent, because the provider didn't "
|
|
235
|
+
"stop them, so the model had more room than the others (over_budget = \"fail\" in models.toml's defaults fails them)."
|
|
236
|
+
if any(x.get("over") for x in result["models"]) else ""))
|
|
226
237
|
|
|
227
238
|
|
|
228
239
|
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
@@ -610,7 +610,9 @@ class AnswerScreen(ModalScreen):
|
|
|
610
610
|
split = ", ".join(f"{k} {rec[k + '_tokens']:,}" for k in ("reasoning", "answer")
|
|
611
611
|
if rec.get(k + "_tokens") is not None)
|
|
612
612
|
facts = [f"finish {finish}",
|
|
613
|
-
(f"{toks:,} tokens" + (f" ({split})" if split else "")
|
|
613
|
+
(f"{toks:,} tokens" + (f" ({split})" if split else "")
|
|
614
|
+
+ (f" [yellow]over the {rec['over_budget']:,} limit sent (the provider didn't stop it)[/yellow]"
|
|
615
|
+
if rec.get("over_budget") else "")) if toks else "",
|
|
614
616
|
f"prompt {rec['prompt_tokens']:,} tokens" if rec.get("prompt_tokens") else "",
|
|
615
617
|
(f"TTFT {rec['ttft_s']:.1f}s" + (f" [yellow]⟲ {rec['cached_tokens']} cached[/yellow]"
|
|
616
618
|
if rec.get("cached_tokens") else "")) if rec.get("ttft_s") is not None else "",
|
|
@@ -3317,6 +3319,7 @@ class ResultsScreen(Screen):
|
|
|
3317
3319
|
self.query_one("#pq-legend", Static).update(
|
|
3318
3320
|
f"[dim]{len(tests)} questions. Each cell: result (passes/answers if repeated), median tokens, median seconds. "
|
|
3319
3321
|
"[green]bold[/green] = fewest tokens among passing answers · [red]cut[/red] = hit max_tokens · "
|
|
3322
|
+
"[yellow]over[/yellow] = ran past max_tokens (the provider didn't stop it) · "
|
|
3320
3323
|
"◐ = pack unfinished (not in scores) · dim seconds = hosted, or measured on another machine; tokens compare "
|
|
3321
3324
|
"across machines, seconds don't. ×tokens = most / fewest tokens across models. Enter on a cell opens the "
|
|
3322
3325
|
"answers. Click a column name to sort by it (a model's column: by its tokens).[/dim]" + ("\n[yellow]Left out, being rerun with other questions or settings: " + ", ".join(
|
|
@@ -3376,7 +3379,8 @@ class ResultsScreen(Screen):
|
|
|
3376
3379
|
"max"),
|
|
3377
3380
|
hi(r["fewest"], str(r["fewest"]), "fewest", "max"),
|
|
3378
3381
|
hi(r["fastest"], str(r["fastest"]), "fastest", "max"),
|
|
3379
|
-
f"[red]{r['cut']}[/red]" if r["cut"] else "0"
|
|
3382
|
+
(f"[red]{r['cut']}[/red]" if r["cut"] else "0")
|
|
3383
|
+
+ (f" [yellow]+{r['over']} over[/yellow]" if r.get("over") else "")])
|
|
3380
3384
|
table.add_columns(*header)
|
|
3381
3385
|
table.add_rows(fit_first_column(header, cells, (self.size.width or self.app.size.width) - 3))
|
|
3382
3386
|
line = f"[b]Comparison[/b] over the {len(shared)} questions all {len(models)} answered"
|
|
@@ -3413,6 +3417,7 @@ class ResultsScreen(Screen):
|
|
|
3413
3417
|
if remote:
|
|
3414
3418
|
secs = f"[dim]{secs}[/dim]"
|
|
3415
3419
|
return (f"{mark} {tok_s} {secs}" + (" [red]cut[/red]" if c["truncated"] else "")
|
|
3420
|
+
+ (" [yellow]over[/yellow]" if c.get("over") else "")
|
|
3416
3421
|
+ (" [cyan]◐[/cyan]" if c["partial"] else ""))
|
|
3417
3422
|
|
|
3418
3423
|
PQ_FIXED = ("Test", "Pack", "Level", "Added", "×tokens") # Per question's columns before the models
|
|
@@ -2421,6 +2421,81 @@ def fake_gguf(path, embedding=5120, size=0, kv_heads=4):
|
|
|
2421
2421
|
f.write(out + b"\0" * max(0, size - len(out)))
|
|
2422
2422
|
|
|
2423
2423
|
|
|
2424
|
+
class OverBudget(unittest.TestCase):
|
|
2425
|
+
"""Answers that used more tokens than the max_tokens sent (a provider that didn't stop them) are
|
|
2426
|
+
marked when results are read: flagged, or failed with [defaults] over_budget = "fail"."""
|
|
2427
|
+
|
|
2428
|
+
def setUp(self):
|
|
2429
|
+
from tuieval import packs
|
|
2430
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
2431
|
+
self.ws = os.path.join(self.tmp.name, "ws")
|
|
2432
|
+
tuieval(self.tmp.name, "init", self.ws)
|
|
2433
|
+
d = os.path.join(self.ws, "packs", "p")
|
|
2434
|
+
os.makedirs(d)
|
|
2435
|
+
write(os.path.join(d, "pack.toml"), 'label = "p"\nmax_tokens = 100\n[gate]\nmin_accuracy = 0.5\n')
|
|
2436
|
+
write(os.path.join(d, "tests.yaml"), "".join(f"- id: q{i}\n input: q{i}\n expected: 7\n" for i in range(4)))
|
|
2437
|
+
rec = lambda i, ok, toks, **kw: {"test": f"q{i}", "description": f"q{i}", "pass": ok, "score": float(ok),
|
|
2438
|
+
"reason": "ok" if ok else "wrong", "finish": "stop", "repeat": 0,
|
|
2439
|
+
"completion_tokens": toks, **kw}
|
|
2440
|
+
self.path = os.path.join(self.ws, "results", "m", "p.json")
|
|
2441
|
+
os.makedirs(os.path.dirname(self.path))
|
|
2442
|
+
with open(self.path, "w") as f:
|
|
2443
|
+
json.dump({"format": "evals/1", "model": {"label": "m"},
|
|
2444
|
+
"pack": {"name": "p", "fingerprint": packs.load_pack(d).fingerprint, "max_tokens": 100},
|
|
2445
|
+
"run": {}, "settings": {"max_tokens": 16384},
|
|
2446
|
+
"results": [rec(0, True, 150), # past the pack's 100: over
|
|
2447
|
+
rec(1, True, 90),
|
|
2448
|
+
rec(2, False, 100, finish="length"), # cut off: truncated, not over
|
|
2449
|
+
rec(3, True, 500, tokens_estimated=True)]}, f) # an estimate: never flagged
|
|
2450
|
+
|
|
2451
|
+
def tearDown(self):
|
|
2452
|
+
self.tmp.cleanup()
|
|
2453
|
+
|
|
2454
|
+
def set_mode(self, mode):
|
|
2455
|
+
path = os.path.join(self.ws, "models.toml")
|
|
2456
|
+
write(path, read(path).replace("[defaults]\n", f'[defaults]\nover_budget = "{mode}"\n', 1))
|
|
2457
|
+
|
|
2458
|
+
def test_flag_marks_copies_and_shows_counts(self):
|
|
2459
|
+
from tuieval import compare, engine, pta
|
|
2460
|
+
e = engine.Engine(root=self.ws)
|
|
2461
|
+
recs = e.records("m", "p")
|
|
2462
|
+
self.assertEqual([r.get("over_budget") for r in recs], [100, None, None, None])
|
|
2463
|
+
self.assertEqual([r["pass"] for r in recs], [True, True, False, True])
|
|
2464
|
+
self.assertNotIn("over_budget", e._result(self.path)["results"][0]) # what a rerun writes back stays as stored
|
|
2465
|
+
self.assertNotIn("over_budget", read(self.path))
|
|
2466
|
+
rows, _ = compare.load([self.path])
|
|
2467
|
+
self.assertEqual([r["over_budget"] for r in rows], [100, None, None, None])
|
|
2468
|
+
cell = compare.per_question(rows)[0]["cells"]["m"]
|
|
2469
|
+
self.assertEqual((cell["over"], cell["passed"]), (1, 1))
|
|
2470
|
+
header, table = pta.table(pta.index(rows))
|
|
2471
|
+
self.assertEqual(table[0][header.index("over budget")], "1/4")
|
|
2472
|
+
self.assertIn("(1 over budget)", "\n".join(pta.bars(pta.index(rows), markup=False)))
|
|
2473
|
+
self.assertIn("over budget", compare.scorecard(rows)[0])
|
|
2474
|
+
|
|
2475
|
+
def test_fail_mode_fails_them(self):
|
|
2476
|
+
from tuieval import compare, engine
|
|
2477
|
+
self.set_mode("fail") # records() is what verdicts are computed from
|
|
2478
|
+
e = engine.Engine(root=self.ws)
|
|
2479
|
+
recs = e.records("m", "p")
|
|
2480
|
+
self.assertEqual([r["pass"] for r in recs], [False, True, False, True])
|
|
2481
|
+
self.assertTrue(recs[0]["reason"].startswith("OVER BUDGET: 150 tokens, over the 100 limit sent; ok"))
|
|
2482
|
+
rows, _ = compare.load([self.path])
|
|
2483
|
+
self.assertEqual([r["ok"] for r in rows], [False, True, False, True])
|
|
2484
|
+
self.assertIn('"pass": true', read(self.path)) # the stored grade is unchanged
|
|
2485
|
+
|
|
2486
|
+
def test_partials_and_bad_setting(self):
|
|
2487
|
+
from tuieval import compare, engine
|
|
2488
|
+
part = os.path.join(self.ws, "results", "m", "q.json.partial.jsonl")
|
|
2489
|
+
with open(part, "w") as f:
|
|
2490
|
+
f.write(json.dumps({"pack_fingerprint": "x", "max_tokens": 50, "settings": {"sampling": {"max_tokens": 999}}}) + "\n")
|
|
2491
|
+
f.write(json.dumps({"test": "a", "description": "a", "pass": True, "score": 1.0, "reason": "ok",
|
|
2492
|
+
"finish": "stop", "completion_tokens": 60}) + "\n")
|
|
2493
|
+
self.assertEqual([r["over_budget"] for r in compare.load_partials(os.path.join(self.ws, "results"))], [50])
|
|
2494
|
+
self.set_mode("sometimes")
|
|
2495
|
+
with self.assertRaisesRegex(engine.ConfigError, "over_budget must be one of"):
|
|
2496
|
+
engine.Engine(root=self.ws)
|
|
2497
|
+
|
|
2498
|
+
|
|
2424
2499
|
class FitCheck(unittest.TestCase):
|
|
2425
2500
|
def test_default_follows_the_ctx_placeholder(self):
|
|
2426
2501
|
from tuieval import engine
|
|
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.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/pack.toml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev121 → tuieval-0.2.0.dev123}/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
|
|
File without changes
|