synthbench-eval 0.4.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.
- synthbench/__init__.py +16 -0
- synthbench/__main__.py +5 -0
- synthbench/adapter.py +131 -0
- synthbench/anomaly.py +503 -0
- synthbench/baseline_floors.py +213 -0
- synthbench/baselines.py +902 -0
- synthbench/cli.py +2929 -0
- synthbench/config_id.py +424 -0
- synthbench/contamination.py +783 -0
- synthbench/convergence/__init__.py +67 -0
- synthbench/convergence/baseline.py +172 -0
- synthbench/convergence/bootstrap.py +60 -0
- synthbench/convergence/cli_report.py +556 -0
- synthbench/convergence/curves.py +111 -0
- synthbench/convergence/real_sampling.py +146 -0
- synthbench/convergence/thresholds.py +49 -0
- synthbench/datasets/__init__.py +37 -0
- synthbench/datasets/base.py +147 -0
- synthbench/datasets/eurobarometer.py +335 -0
- synthbench/datasets/globalopinionqa.py +252 -0
- synthbench/datasets/gss.py +323 -0
- synthbench/datasets/michigan.py +505 -0
- synthbench/datasets/ntia.py +357 -0
- synthbench/datasets/opinionsqa.py +412 -0
- synthbench/datasets/pewtech.py +334 -0
- synthbench/datasets/policy.py +142 -0
- synthbench/datasets/subpop.py +302 -0
- synthbench/datasets/wvs.py +229 -0
- synthbench/findings.py +988 -0
- synthbench/holdout.py +347 -0
- synthbench/human_distributions.py +243 -0
- synthbench/leaderboard.py +715 -0
- synthbench/leaderboard_pr.py +274 -0
- synthbench/metrics/__init__.py +38 -0
- synthbench/metrics/composite.py +72 -0
- synthbench/metrics/conditioning.py +39 -0
- synthbench/metrics/distributional.py +41 -0
- synthbench/metrics/ranking.py +37 -0
- synthbench/metrics/refusal.py +270 -0
- synthbench/metrics/subgroup.py +58 -0
- synthbench/private_holdout.py +240 -0
- synthbench/providers/__init__.py +43 -0
- synthbench/providers/_parsing.py +150 -0
- synthbench/providers/_retry.py +107 -0
- synthbench/providers/base.py +212 -0
- synthbench/providers/http.py +108 -0
- synthbench/providers/majority_baseline.py +23 -0
- synthbench/providers/ollama.py +109 -0
- synthbench/providers/openrouter.py +144 -0
- synthbench/providers/population_baseline.py +87 -0
- synthbench/providers/random_baseline.py +24 -0
- synthbench/providers/raw_anthropic.py +149 -0
- synthbench/providers/raw_gemini.py +145 -0
- synthbench/providers/raw_openai.py +139 -0
- synthbench/providers/synthpanel.py +978 -0
- synthbench/publish.py +2772 -0
- synthbench/r2_upload.py +178 -0
- synthbench/recompute.py +235 -0
- synthbench/report.py +493 -0
- synthbench/run_hash.py +111 -0
- synthbench/run_validity.py +232 -0
- synthbench/runner.py +834 -0
- synthbench/stats.py +1492 -0
- synthbench/submission.py +284 -0
- synthbench/submission_pr.py +436 -0
- synthbench/submit_adapter.py +748 -0
- synthbench/suite.py +424 -0
- synthbench/suites/__init__.py +94 -0
- synthbench/topics.py +256 -0
- synthbench/user_config.py +409 -0
- synthbench/validation.py +1556 -0
- synthbench/visualize.py +455 -0
- synthbench_eval-0.4.0.dist-info/METADATA +279 -0
- synthbench_eval-0.4.0.dist-info/RECORD +78 -0
- synthbench_eval-0.4.0.dist-info/WHEEL +5 -0
- synthbench_eval-0.4.0.dist-info/entry_points.txt +2 -0
- synthbench_eval-0.4.0.dist-info/licenses/LICENSE +21 -0
- synthbench_eval-0.4.0.dist-info/top_level.txt +1 -0
synthbench/config_id.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Canonical config ID parsing for the run explorer.
|
|
2
|
+
|
|
3
|
+
Turns raw provider strings like
|
|
4
|
+
``synthpanel/openrouter/anthropic/claude-haiku-4-5 t=0.85 tpl=current``
|
|
5
|
+
into a structured record (framework / base_provider / model / knobs) and a
|
|
6
|
+
stable slug ``framework--model--t<temp>--tpl<name>[--eff<level>]--<hash8>``
|
|
7
|
+
suitable for use as a URL path segment (the ``eff`` segment appears only
|
|
8
|
+
when the run set a reasoning-effort level).
|
|
9
|
+
|
|
10
|
+
Hash collisions are unlikely but protected against by including every
|
|
11
|
+
distinguishing field (dataset, samples_per_question, question_set_hash,
|
|
12
|
+
full knob map) in the hashed canonical JSON — so two runs that differ only
|
|
13
|
+
in a field the human-readable slug drops still land on different IDs.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
BASELINE_SUFFIX = "-baseline"
|
|
26
|
+
|
|
27
|
+
# Path components that name a vendor but use a non-canonical label.
|
|
28
|
+
# E.g. ``raw-gemini/...`` refers to Google; ``openrouter/meta-llama/...``
|
|
29
|
+
# means Meta. Normalizing here keeps the base_provider field canonical so
|
|
30
|
+
# downstream dedup/grouping treats them as one vendor.
|
|
31
|
+
_BASE_VENDOR_NORMALIZE: dict[str, str] = {
|
|
32
|
+
"gemini": "google",
|
|
33
|
+
"meta-llama": "meta",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Model-name prefix → canonical vendor. Used to infer base_provider when the
|
|
37
|
+
# path itself doesn't carry a vendor segment (e.g. ``synthpanel/claude-...``).
|
|
38
|
+
_MODEL_VENDOR_PREFIXES: tuple[tuple[str, str], ...] = (
|
|
39
|
+
("claude-", "anthropic"),
|
|
40
|
+
("gemini-", "google"),
|
|
41
|
+
("gpt-", "openai"),
|
|
42
|
+
("llama-", "meta"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Version-date suffixes appended to model aliases.
|
|
46
|
+
# Anthropic-style: ``-YYYYMMDD`` (8 digits) → ``claude-haiku-4-5-20251001``
|
|
47
|
+
# OpenAI-style: ``-YYYY-MM-DD`` → ``gpt-4o-mini-2024-07-18``
|
|
48
|
+
# These identify the same weights as the alias form. Stripping them in the
|
|
49
|
+
# config_id path collapses the dated and undated variants into one group on
|
|
50
|
+
# /explore and the leaderboard. Check the longer (OpenAI) pattern first so
|
|
51
|
+
# ``gpt-X-2024-07-18`` is not misread as an 8-digit suffix.
|
|
52
|
+
_OPENAI_DATE_SUFFIX = re.compile(r"-\d{4}-\d{2}-\d{2}$")
|
|
53
|
+
_ANTHROPIC_DATE_SUFFIX = re.compile(r"-\d{8}$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def canonical_model(model: str) -> str:
|
|
57
|
+
"""Strip trailing version-date suffixes from a model name.
|
|
58
|
+
|
|
59
|
+
``claude-haiku-4-5-20251001`` → ``claude-haiku-4-5``
|
|
60
|
+
``gpt-4o-mini-2024-07-18`` → ``gpt-4o-mini``
|
|
61
|
+
|
|
62
|
+
Models without a recognized date suffix pass through unchanged. The
|
|
63
|
+
function is idempotent: running it twice yields the same result.
|
|
64
|
+
"""
|
|
65
|
+
if not model:
|
|
66
|
+
return model
|
|
67
|
+
stripped = _OPENAI_DATE_SUFFIX.sub("", model)
|
|
68
|
+
if stripped != model:
|
|
69
|
+
return stripped
|
|
70
|
+
return _ANTHROPIC_DATE_SUFFIX.sub("", model)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ParsedConfig:
|
|
75
|
+
"""Structured breakdown of a provider string + run config."""
|
|
76
|
+
|
|
77
|
+
framework: str
|
|
78
|
+
base_provider: str | None
|
|
79
|
+
model: str
|
|
80
|
+
knobs: dict[str, str] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def as_canonical_dict(self) -> dict[str, Any]:
|
|
83
|
+
"""Return a dict with stable key ordering for hashing."""
|
|
84
|
+
return {
|
|
85
|
+
"framework": self.framework,
|
|
86
|
+
"base_provider": self.base_provider,
|
|
87
|
+
"model": self.model,
|
|
88
|
+
"knobs": dict(sorted(self.knobs.items())),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _split_provider_and_knobs(provider: str) -> tuple[str, list[str]]:
|
|
93
|
+
"""Split the whitespace-separated provider/knob form.
|
|
94
|
+
|
|
95
|
+
``synthpanel/.../claude-haiku-4-5 t=0.85 tpl=current`` →
|
|
96
|
+
(``synthpanel/.../claude-haiku-4-5``, [``t=0.85``, ``tpl=current``]).
|
|
97
|
+
"""
|
|
98
|
+
tokens = provider.strip().split()
|
|
99
|
+
if not tokens:
|
|
100
|
+
return "", []
|
|
101
|
+
return tokens[0], tokens[1:]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_knob_tokens(tokens: list[str]) -> dict[str, str]:
|
|
105
|
+
"""Turn ``[``t=0.85``, ``tpl=current``]`` into a dict."""
|
|
106
|
+
knobs: dict[str, str] = {}
|
|
107
|
+
for tok in tokens:
|
|
108
|
+
if "=" not in tok:
|
|
109
|
+
continue
|
|
110
|
+
key, value = tok.split("=", 1)
|
|
111
|
+
key = key.strip()
|
|
112
|
+
value = value.strip()
|
|
113
|
+
if key:
|
|
114
|
+
knobs[key] = value
|
|
115
|
+
return knobs
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _normalize_base_vendor(vendor: str | None) -> str | None:
|
|
119
|
+
"""Map aliased vendor labels to their canonical name (gemini → google)."""
|
|
120
|
+
if vendor is None:
|
|
121
|
+
return None
|
|
122
|
+
return _BASE_VENDOR_NORMALIZE.get(vendor, vendor)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _infer_vendor_from_model(model: str) -> str | None:
|
|
126
|
+
"""Guess the vendor from a model name prefix (claude-* → anthropic, …).
|
|
127
|
+
|
|
128
|
+
Returns ``None`` for names that don't match a known family (baselines,
|
|
129
|
+
ensemble blends, bespoke identifiers).
|
|
130
|
+
"""
|
|
131
|
+
lower = model.lower()
|
|
132
|
+
for prefix, vendor in _MODEL_VENDOR_PREFIXES:
|
|
133
|
+
if lower.startswith(prefix):
|
|
134
|
+
return vendor
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_path(path: str) -> tuple[str, str | None, str]:
|
|
139
|
+
"""Parse the slash-separated path component into (framework, base, model).
|
|
140
|
+
|
|
141
|
+
Recognizes these shapes:
|
|
142
|
+
random-baseline → ("baseline", None, "random-baseline")
|
|
143
|
+
raw-anthropic/claude-haiku-4-5 → ("raw", "anthropic", "claude-haiku-4-5")
|
|
144
|
+
raw-gemini/gemini-2.5-flash-lite → ("raw", "google", "gemini-2.5-flash-lite")
|
|
145
|
+
ensemble/3-model-blend → ("ensemble", None, "3-model-blend")
|
|
146
|
+
synthpanel/claude-haiku-4-5-20251001 → ("synthpanel", "anthropic", "claude-haiku-4-5-20251001")
|
|
147
|
+
synthpanel/gemini-2.5-flash-lite → ("synthpanel", "google", "gemini-2.5-flash-lite")
|
|
148
|
+
openrouter/openai/gpt-4o-mini → ("raw", "openai", "gpt-4o-mini")
|
|
149
|
+
synthpanel/openrouter/anthropic/claude-haiku-4-5
|
|
150
|
+
→ ("synthpanel", "anthropic", "claude-haiku-4-5")
|
|
151
|
+
|
|
152
|
+
OpenRouter is a gateway, not a framework — paths that lead with
|
|
153
|
+
``openrouter/`` collapse to ``framework=raw`` so the same model reached
|
|
154
|
+
via the gateway vs a direct call dedupes on ``base_provider``/``model``.
|
|
155
|
+
"""
|
|
156
|
+
path = path.strip()
|
|
157
|
+
if not path:
|
|
158
|
+
return "unknown", None, "unknown"
|
|
159
|
+
|
|
160
|
+
if path.endswith(BASELINE_SUFFIX) and "/" not in path:
|
|
161
|
+
return "baseline", None, path
|
|
162
|
+
|
|
163
|
+
parts = path.split("/")
|
|
164
|
+
n = len(parts)
|
|
165
|
+
|
|
166
|
+
if n == 1:
|
|
167
|
+
return "raw", None, parts[0]
|
|
168
|
+
|
|
169
|
+
first = parts[0]
|
|
170
|
+
|
|
171
|
+
if first.startswith("raw-"):
|
|
172
|
+
return "raw", _normalize_base_vendor(first[4:]), parts[-1]
|
|
173
|
+
|
|
174
|
+
if first == "openrouter":
|
|
175
|
+
if n >= 3:
|
|
176
|
+
return "raw", _normalize_base_vendor(parts[-2]), parts[-1]
|
|
177
|
+
# openrouter/<model> — no explicit vendor segment, infer from model.
|
|
178
|
+
model = parts[1]
|
|
179
|
+
return "raw", _infer_vendor_from_model(model), model
|
|
180
|
+
|
|
181
|
+
if n == 2:
|
|
182
|
+
model = parts[1]
|
|
183
|
+
return first, _infer_vendor_from_model(model), model
|
|
184
|
+
|
|
185
|
+
if n == 3:
|
|
186
|
+
return first, _normalize_base_vendor(parts[1]), parts[2]
|
|
187
|
+
|
|
188
|
+
# n >= 4 — treat middle segments as a sub-framework we don't surface,
|
|
189
|
+
# take the last two as (base, model).
|
|
190
|
+
return first, _normalize_base_vendor(parts[-2]), parts[-1]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def parse_provider(provider: str) -> ParsedConfig:
|
|
194
|
+
"""Parse a provider string into a structured ParsedConfig.
|
|
195
|
+
|
|
196
|
+
Whitespace-tolerant. Unknown shapes degrade gracefully to framework='raw'.
|
|
197
|
+
"""
|
|
198
|
+
head, knob_tokens = _split_provider_and_knobs(provider)
|
|
199
|
+
framework, base, model = _parse_path(head)
|
|
200
|
+
knobs = _parse_knob_tokens(knob_tokens)
|
|
201
|
+
return ParsedConfig(
|
|
202
|
+
framework=framework,
|
|
203
|
+
base_provider=base,
|
|
204
|
+
model=model,
|
|
205
|
+
knobs=knobs,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _runnable_model_slug(head: str) -> str:
|
|
210
|
+
"""Build an OpenRouter-runnable ``<vendor>/<model>`` slug from a raw path.
|
|
211
|
+
|
|
212
|
+
Unlike :func:`parse_provider`'s ``base_provider`` (which canonicalizes
|
|
213
|
+
vendors for dedup — ``meta-llama`` → ``meta``), this preserves the on-path
|
|
214
|
+
vendor verbatim for gateway routes so the slug stays a valid OpenRouter
|
|
215
|
+
model id, and keeps the model segment verbatim (version dates intact) so it
|
|
216
|
+
can be piped straight to the gateway:
|
|
217
|
+
|
|
218
|
+
openrouter/google/gemini-2.5-flash-lite → google/gemini-2.5-flash-lite
|
|
219
|
+
openrouter/meta-llama/llama-3.3-70b-instruct → meta-llama/llama-3.3-70b-instruct
|
|
220
|
+
synthpanel/openrouter/anthropic/claude-haiku-4-5 → anthropic/claude-haiku-4-5
|
|
221
|
+
synthpanel/gemini-2.5-flash-lite → google/gemini-2.5-flash-lite
|
|
222
|
+
raw-anthropic/claude-haiku-4-5-20251001 → anthropic/claude-haiku-4-5-20251001
|
|
223
|
+
raw-gemini/gemini-2.5-flash-lite → google/gemini-2.5-flash-lite
|
|
224
|
+
ensemble/3-model-blend → 3-model-blend
|
|
225
|
+
random-baseline → random-baseline
|
|
226
|
+
"""
|
|
227
|
+
head = head.strip()
|
|
228
|
+
if not head:
|
|
229
|
+
return "unknown"
|
|
230
|
+
if head.endswith(BASELINE_SUFFIX) and "/" not in head:
|
|
231
|
+
return head
|
|
232
|
+
|
|
233
|
+
parts = head.split("/")
|
|
234
|
+
# Drop a leading runner segment that isn't part of the model id.
|
|
235
|
+
if parts and parts[0] in ("synthpanel", "ensemble"):
|
|
236
|
+
parts = parts[1:]
|
|
237
|
+
# Drop an OpenRouter gateway hop — the remaining segments ARE the slug.
|
|
238
|
+
if parts and parts[0] == "openrouter":
|
|
239
|
+
parts = parts[1:]
|
|
240
|
+
|
|
241
|
+
if parts and parts[0].startswith("raw-"):
|
|
242
|
+
# Direct vendor API (e.g. raw-gemini/...): map to the OpenRouter author.
|
|
243
|
+
raw_vendor = parts[0][len("raw-") :]
|
|
244
|
+
parts = [_normalize_base_vendor(raw_vendor) or raw_vendor, *parts[1:]]
|
|
245
|
+
elif len(parts) == 1:
|
|
246
|
+
# Bare model with no vendor segment — infer the OpenRouter author so the
|
|
247
|
+
# slug is pipeable (e.g. gemini-2.5-flash-lite → google/...).
|
|
248
|
+
vendor = _infer_vendor_from_model(parts[0])
|
|
249
|
+
if vendor:
|
|
250
|
+
parts = [vendor, *parts]
|
|
251
|
+
|
|
252
|
+
return "/".join(p for p in parts if p) or head
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def runnable_ids(provider: str) -> tuple[str, str]:
|
|
256
|
+
"""Derive machine-runnable ``(provider_id, model_id)`` from a provider string.
|
|
257
|
+
|
|
258
|
+
These are the runnable counterparts to the human-facing display labels:
|
|
259
|
+
|
|
260
|
+
- ``provider_id`` — the execution framework/runner taxonomy from
|
|
261
|
+
:func:`parse_provider` (``raw`` / ``synthpanel`` / ``ensemble`` /
|
|
262
|
+
``baseline``). Tells a consumer whether a score reflects a raw model, the
|
|
263
|
+
SynthPanel product layer, an ensemble, or a statistical baseline.
|
|
264
|
+
- ``model_id`` — an OpenRouter-runnable ``<vendor>/<model>`` slug a consumer
|
|
265
|
+
can pipe straight to the gateway without parsing display names. See
|
|
266
|
+
:func:`_runnable_model_slug` for the exact derivation per shape.
|
|
267
|
+
|
|
268
|
+
Hyperparameter knobs (``t=``, ``tpl=``, ``profile=``) are stripped.
|
|
269
|
+
"""
|
|
270
|
+
head, _ = _split_provider_and_knobs(provider)
|
|
271
|
+
framework, _base, _model = _parse_path(head)
|
|
272
|
+
return framework, _runnable_model_slug(head)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
_SLUG_UNSAFE = re.compile(r"[^a-z0-9.\-]+")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _slugify(value: str) -> str:
|
|
279
|
+
"""Lowercase and strip characters that would break a URL segment.
|
|
280
|
+
|
|
281
|
+
Preserves dots for version numbers (``t=0.85`` → ``t0.85``).
|
|
282
|
+
"""
|
|
283
|
+
out = _SLUG_UNSAFE.sub("-", value.lower())
|
|
284
|
+
return out.strip("-")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _format_temperature(temp: str | float | int | None) -> str:
|
|
288
|
+
"""Render the temperature portion of the slug.
|
|
289
|
+
|
|
290
|
+
``0.85 → 't0.85'``, ``1.0 → 't1.0'``, ``None → 'tdefault'``.
|
|
291
|
+
"""
|
|
292
|
+
if temp is None or temp == "":
|
|
293
|
+
return "tdefault"
|
|
294
|
+
if isinstance(temp, (int, float)):
|
|
295
|
+
return f"t{temp}"
|
|
296
|
+
return f"t{temp}"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _format_template(tpl: str | None) -> str:
|
|
300
|
+
if not tpl:
|
|
301
|
+
return "tplcurrent"
|
|
302
|
+
return f"tpl{_slugify(tpl)}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _hash_canonical(payload: dict[str, Any]) -> str:
|
|
306
|
+
"""Stable 8-char hex hash over a canonical JSON dump."""
|
|
307
|
+
serialized = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
308
|
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()[:8]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _format_effort(effort: str | None) -> str:
|
|
312
|
+
"""Render the reasoning-effort portion of the slug.
|
|
313
|
+
|
|
314
|
+
``'high' → 'effhigh'``. Returns ``""`` when effort is absent — the
|
|
315
|
+
segment is omitted entirely so pre-effort config_ids keep their exact
|
|
316
|
+
historical slug shape (and hash, see :func:`build_config_id`).
|
|
317
|
+
"""
|
|
318
|
+
if not effort:
|
|
319
|
+
return ""
|
|
320
|
+
return f"eff{_slugify(str(effort))}"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def build_config_id(
|
|
324
|
+
provider: str,
|
|
325
|
+
*,
|
|
326
|
+
dataset: str | None = None,
|
|
327
|
+
temperature: float | None = None,
|
|
328
|
+
template: str | None = None,
|
|
329
|
+
effort: str | None = None,
|
|
330
|
+
samples_per_question: int | None = None,
|
|
331
|
+
question_set_hash: str | None = None,
|
|
332
|
+
extra_knobs: dict[str, Any] | None = None,
|
|
333
|
+
) -> tuple[str, ParsedConfig]:
|
|
334
|
+
"""Return (config_id_slug, parsed) for a run.
|
|
335
|
+
|
|
336
|
+
Accepts structured fields extracted from the result file's ``config``
|
|
337
|
+
block. Values passed explicitly override knobs parsed from the provider
|
|
338
|
+
string (e.g. ``t=0.85`` vs the real ``config.temperature``).
|
|
339
|
+
|
|
340
|
+
``effort`` (reasoning effort: low/medium/high) is additive-only: when
|
|
341
|
+
absent it contributes nothing to the hash payload or the slug, so every
|
|
342
|
+
config_id minted before the effort dimension existed is unchanged.
|
|
343
|
+
"""
|
|
344
|
+
parsed = parse_provider(provider)
|
|
345
|
+
canon_model = canonical_model(parsed.model)
|
|
346
|
+
|
|
347
|
+
# Normalize template: drop path + extension
|
|
348
|
+
tpl_norm = None
|
|
349
|
+
if template:
|
|
350
|
+
tpl_norm = (
|
|
351
|
+
Path(template).stem
|
|
352
|
+
if ("/" in template or template.endswith(".md"))
|
|
353
|
+
else template
|
|
354
|
+
)
|
|
355
|
+
elif "tpl" in parsed.knobs:
|
|
356
|
+
tpl_norm = parsed.knobs["tpl"]
|
|
357
|
+
|
|
358
|
+
# Temperature: prefer explicit config value over knob string
|
|
359
|
+
temp_val: float | str | None = temperature
|
|
360
|
+
if temp_val is None and "t" in parsed.knobs:
|
|
361
|
+
try:
|
|
362
|
+
temp_val = float(parsed.knobs["t"])
|
|
363
|
+
except ValueError:
|
|
364
|
+
temp_val = parsed.knobs["t"]
|
|
365
|
+
|
|
366
|
+
# Reasoning effort: prefer explicit config value over knob string.
|
|
367
|
+
# Absent effort adds NO knob — pre-effort runs must hash identically.
|
|
368
|
+
effort_val: str | None = effort
|
|
369
|
+
if effort_val is None and "effort" in parsed.knobs:
|
|
370
|
+
effort_val = parsed.knobs["effort"]
|
|
371
|
+
|
|
372
|
+
# Canonicalize the full knob map for the hash
|
|
373
|
+
canonical_knobs: dict[str, Any] = dict(parsed.knobs)
|
|
374
|
+
if temp_val is not None:
|
|
375
|
+
canonical_knobs["t"] = temp_val
|
|
376
|
+
if tpl_norm is not None:
|
|
377
|
+
canonical_knobs["tpl"] = tpl_norm
|
|
378
|
+
if effort_val is not None:
|
|
379
|
+
canonical_knobs["effort"] = effort_val
|
|
380
|
+
if extra_knobs:
|
|
381
|
+
canonical_knobs.update(extra_knobs)
|
|
382
|
+
|
|
383
|
+
hash_payload: dict[str, Any] = {
|
|
384
|
+
"framework": parsed.framework,
|
|
385
|
+
"base_provider": parsed.base_provider,
|
|
386
|
+
"model": canon_model,
|
|
387
|
+
"knobs": {k: canonical_knobs[k] for k in sorted(canonical_knobs)},
|
|
388
|
+
}
|
|
389
|
+
if dataset is not None:
|
|
390
|
+
hash_payload["dataset"] = dataset
|
|
391
|
+
if samples_per_question is not None:
|
|
392
|
+
hash_payload["samples_per_question"] = samples_per_question
|
|
393
|
+
if question_set_hash is not None:
|
|
394
|
+
hash_payload["question_set_hash"] = question_set_hash
|
|
395
|
+
|
|
396
|
+
hash8 = _hash_canonical(hash_payload)
|
|
397
|
+
|
|
398
|
+
slug_model = _slugify(canon_model)
|
|
399
|
+
slug_framework = _slugify(parsed.framework)
|
|
400
|
+
temp_part = _format_temperature(temp_val)
|
|
401
|
+
tpl_part = _format_template(tpl_norm)
|
|
402
|
+
effort_part = _format_effort(effort_val)
|
|
403
|
+
|
|
404
|
+
parts = [slug_framework, slug_model, temp_part, tpl_part]
|
|
405
|
+
if effort_part:
|
|
406
|
+
parts.append(effort_part)
|
|
407
|
+
parts.append(hash8)
|
|
408
|
+
slug = "--".join(parts)
|
|
409
|
+
|
|
410
|
+
# Refresh parsed.knobs to include normalized t/tpl for downstream callers
|
|
411
|
+
resolved_knobs: dict[str, str] = dict(parsed.knobs)
|
|
412
|
+
if temp_val is not None:
|
|
413
|
+
resolved_knobs["t"] = str(temp_val)
|
|
414
|
+
if tpl_norm is not None:
|
|
415
|
+
resolved_knobs["tpl"] = tpl_norm
|
|
416
|
+
if effort_val is not None:
|
|
417
|
+
resolved_knobs["effort"] = effort_val
|
|
418
|
+
|
|
419
|
+
return slug, ParsedConfig(
|
|
420
|
+
framework=parsed.framework,
|
|
421
|
+
base_provider=parsed.base_provider,
|
|
422
|
+
model=canon_model,
|
|
423
|
+
knobs=resolved_knobs,
|
|
424
|
+
)
|