kipimo 0.2.2__tar.gz → 0.4.0__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.
- {kipimo-0.2.2/src/kipimo.egg-info → kipimo-0.4.0}/PKG-INFO +49 -1
- {kipimo-0.2.2 → kipimo-0.4.0}/README.md +48 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/pyproject.toml +1 -1
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo/__init__.py +5 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo/cli.py +30 -1
- kipimo-0.4.0/src/kipimo/harness.py +145 -0
- kipimo-0.4.0/src/kipimo/pareto.py +139 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo/targets.py +66 -0
- {kipimo-0.2.2 → kipimo-0.4.0/src/kipimo.egg-info}/PKG-INFO +49 -1
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo.egg-info/SOURCES.txt +4 -0
- kipimo-0.4.0/tests/test_harness.py +69 -0
- kipimo-0.4.0/tests/test_pareto.py +55 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/tests/test_targets.py +20 -1
- {kipimo-0.2.2 → kipimo-0.4.0}/LICENSE +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/setup.cfg +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo/data/kipimo_v0.1.jsonl +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo.egg-info/dependency_links.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo.egg-info/entry_points.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/src/kipimo.egg-info/top_level.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/tests/test_kipimo.py +0 -0
- {kipimo-0.2.2 → kipimo-0.4.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kipimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing).
|
|
5
5
|
Author: AfriKaziOS maintainers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,3 +65,51 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
65
65
|
|
|
66
66
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
67
67
|
<!-- /interconnect:v1 -->
|
|
68
|
+
|
|
69
|
+
## Running a scorecard (`kipimo run`)
|
|
70
|
+
|
|
71
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
72
|
+
still never calls a model API — you supply a *generator command* per target
|
|
73
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
74
|
+
stdout; see `examples/generate_predictions.py`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cat > generators.json <<'JSON'
|
|
78
|
+
{
|
|
79
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
80
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
81
|
+
"kimi-k3": ""
|
|
82
|
+
}
|
|
83
|
+
JSON
|
|
84
|
+
|
|
85
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
89
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
90
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
91
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
92
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
93
|
+
cannot be misread as a ranking of the field.
|
|
94
|
+
|
|
95
|
+
## From leaderboard to deployment decision (`kipimo analyze`)
|
|
96
|
+
|
|
97
|
+
A ranking says which model scored highest. Institutions need a different answer:
|
|
98
|
+
**what is the cheapest model we may lawfully run ourselves that clears the bar?**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
kipimo run generators.json > card.json
|
|
102
|
+
kipimo analyze card.json --costs costs.json --threshold 0.85
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Targets carry a deployment profile (`license`, `self_hostable`, `hardware_tier`,
|
|
106
|
+
`offline_capable`), which `analyze` joins with measured accuracy and
|
|
107
|
+
operator-supplied cost to report the Pareto frontier and the cheapest
|
|
108
|
+
self-hostable qualifier:
|
|
109
|
+
|
|
110
|
+
> *"gemma-3-12b is self-hostable, clears 0.85, and reaches 90% of the best
|
|
111
|
+
> measured target's score."*
|
|
112
|
+
|
|
113
|
+
Costs are supplied per run, never stored in the registry — vendor prices move
|
|
114
|
+
weekly and a stale number in a public benchmark is worse than none. A target
|
|
115
|
+
with no cost supplied is excluded from the frontier, **not treated as free**.
|
|
@@ -47,3 +47,51 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
47
47
|
|
|
48
48
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
49
49
|
<!-- /interconnect:v1 -->
|
|
50
|
+
|
|
51
|
+
## Running a scorecard (`kipimo run`)
|
|
52
|
+
|
|
53
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
54
|
+
still never calls a model API — you supply a *generator command* per target
|
|
55
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
56
|
+
stdout; see `examples/generate_predictions.py`).
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cat > generators.json <<'JSON'
|
|
60
|
+
{
|
|
61
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
62
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
63
|
+
"kimi-k3": ""
|
|
64
|
+
}
|
|
65
|
+
JSON
|
|
66
|
+
|
|
67
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
71
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
72
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
73
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
74
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
75
|
+
cannot be misread as a ranking of the field.
|
|
76
|
+
|
|
77
|
+
## From leaderboard to deployment decision (`kipimo analyze`)
|
|
78
|
+
|
|
79
|
+
A ranking says which model scored highest. Institutions need a different answer:
|
|
80
|
+
**what is the cheapest model we may lawfully run ourselves that clears the bar?**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
kipimo run generators.json > card.json
|
|
84
|
+
kipimo analyze card.json --costs costs.json --threshold 0.85
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Targets carry a deployment profile (`license`, `self_hostable`, `hardware_tier`,
|
|
88
|
+
`offline_capable`), which `analyze` joins with measured accuracy and
|
|
89
|
+
operator-supplied cost to report the Pareto frontier and the cheapest
|
|
90
|
+
self-hostable qualifier:
|
|
91
|
+
|
|
92
|
+
> *"gemma-3-12b is self-hostable, clears 0.85, and reaches 90% of the best
|
|
93
|
+
> measured target's score."*
|
|
94
|
+
|
|
95
|
+
Costs are supplied per run, never stored in the registry — vendor prices move
|
|
96
|
+
weekly and a stale number in a public benchmark is worse than none. A target
|
|
97
|
+
with no cost supplied is excluded from the frontier, **not treated as free**.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kipimo"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -8,3 +8,8 @@ from .targets import FAMILIES as FAMILIES
|
|
|
8
8
|
from .targets import TARGETS as TARGETS
|
|
9
9
|
from .targets import get_target as get_target
|
|
10
10
|
from .targets import list_targets as list_targets
|
|
11
|
+
from .harness import run_scorecard as run_scorecard
|
|
12
|
+
from .harness import run_target as run_target
|
|
13
|
+
from .pareto import analyze as analyze
|
|
14
|
+
from .pareto import pareto_frontier as pareto_frontier
|
|
15
|
+
from .pareto import cheapest_qualifier as cheapest_qualifier
|
|
@@ -10,6 +10,8 @@ Usage:
|
|
|
10
10
|
kipimo tasks # emit the task set (JSONL) to stdout
|
|
11
11
|
kipimo template # emit an empty predictions file to fill in
|
|
12
12
|
kipimo score preds.jsonl # score predictions against gold
|
|
13
|
+
kipimo run generators.json # run targets in parallel -> ranked scorecard
|
|
14
|
+
kipimo analyze card.json # -> Pareto frontier + cheapest sovereign qualifier
|
|
13
15
|
kipimo targets # emit the scorecard target registry (v0.2)
|
|
14
16
|
"""
|
|
15
17
|
|
|
@@ -21,7 +23,7 @@ import sys
|
|
|
21
23
|
from collections import defaultdict
|
|
22
24
|
from importlib import resources
|
|
23
25
|
|
|
24
|
-
__version__ = "0.
|
|
26
|
+
__version__ = "0.4.0"
|
|
25
27
|
|
|
26
28
|
DISCLAIMER = ("kipimo v0.1 is a SEED benchmark (46 tasks). Swahili phrasing is "
|
|
27
29
|
"simple-register and pending native-speaker review (issue #1). "
|
|
@@ -86,6 +88,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
88
|
tp.add_argument("--family", choices=("closed-api", "open-weight", "small-open"),
|
|
87
89
|
default=None, help="filter by target family")
|
|
88
90
|
sub.add_parser("template", help="emit empty predictions JSONL to stdout")
|
|
91
|
+
rp = sub.add_parser("run", help="run generators across targets in parallel -> scorecard")
|
|
92
|
+
rp.add_argument("generators", help='JSON file: {"target_id": "generator command", ...}')
|
|
93
|
+
rp.add_argument("--timeout", type=int, default=600, help="seconds per target (default 600)")
|
|
94
|
+
rp.add_argument("--workers", type=int, default=4, help="parallel targets (default 4)")
|
|
95
|
+
ap = sub.add_parser("analyze", help="scorecard -> deployment decision (Pareto + cheapest qualifier)")
|
|
96
|
+
ap.add_argument("scorecard", help="JSON scorecard from `kipimo run`")
|
|
97
|
+
ap.add_argument("--costs", default=None, help='JSON file: {"target_id": cost_per_1k_tasks}')
|
|
98
|
+
ap.add_argument("--threshold", type=float, default=0.8, help="competence bar (default 0.8)")
|
|
89
99
|
sp = sub.add_parser("score", help="score a predictions file")
|
|
90
100
|
sp.add_argument("predictions", help="JSONL with {id, prediction:[...]} rows")
|
|
91
101
|
args = p.parse_args(argv)
|
|
@@ -100,6 +110,25 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
100
110
|
elif args.cmd == "template":
|
|
101
111
|
for t in load_tasks():
|
|
102
112
|
print(json.dumps({"id": t["id"], "prediction": []}))
|
|
113
|
+
elif args.cmd == "analyze":
|
|
114
|
+
from .pareto import analyze
|
|
115
|
+
with open(args.scorecard, encoding="utf-8") as f:
|
|
116
|
+
card = json.load(f)
|
|
117
|
+
costs = {}
|
|
118
|
+
if args.costs:
|
|
119
|
+
with open(args.costs, encoding="utf-8") as f:
|
|
120
|
+
costs = {str(k): float(v) for k, v in json.load(f).items()}
|
|
121
|
+
print(json.dumps(analyze(card, costs, args.threshold), indent=2, ensure_ascii=False))
|
|
122
|
+
print(f"\n{DISCLAIMER}", file=sys.stderr)
|
|
123
|
+
elif args.cmd == "run":
|
|
124
|
+
from .harness import load_generators, run_scorecard
|
|
125
|
+
card = run_scorecard(load_generators(args.generators),
|
|
126
|
+
timeout=args.timeout, max_workers=args.workers)
|
|
127
|
+
print(json.dumps(card, indent=2, ensure_ascii=False))
|
|
128
|
+
if card["targets_untested"]:
|
|
129
|
+
print(f"\nNote: {card['targets_untested']} target(s) UNTESTED (unknown, not zero). "
|
|
130
|
+
f"See 'untested' in the scorecard.", file=sys.stderr)
|
|
131
|
+
print(f"\n{DISCLAIMER}", file=sys.stderr)
|
|
103
132
|
else:
|
|
104
133
|
rep = score_file(args.predictions)
|
|
105
134
|
print(json.dumps(rep, indent=2))
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""kipimo.harness — run the benchmark across targets in parallel, converge to a scorecard.
|
|
2
|
+
|
|
3
|
+
The diamond: fan out one worker per target, converge into a single ranked
|
|
4
|
+
scorecard. Targets are independent (a real dependency test — no target's
|
|
5
|
+
prediction informs another's), so they run concurrently; the scorecard is the
|
|
6
|
+
only true convergence point.
|
|
7
|
+
|
|
8
|
+
Two design rules, both deliberate:
|
|
9
|
+
|
|
10
|
+
1. **kipimo still never calls a model API.** This module orchestrates *generator
|
|
11
|
+
commands you supply* (any process that reads tasks JSONL on stdin and writes
|
|
12
|
+
predictions JSONL on stdout). Model access, keys, and endpoints stay outside
|
|
13
|
+
the package, so the benchmark remains vendor-neutral and reproducible by
|
|
14
|
+
anyone. See examples/generate_predictions.py for a stdlib generator.
|
|
15
|
+
|
|
16
|
+
2. **A missing run is never a zero.** The scoring path already treats an absent
|
|
17
|
+
prediction as 0.0, which is correct *within* a run. Across targets it would
|
|
18
|
+
be a lie: a target whose generator crashed, timed out, or was never
|
|
19
|
+
configured has an UNKNOWN score, not a bad one. Conflating "the model failed
|
|
20
|
+
the task" with "we never tested the model" is how an evaluation system starts
|
|
21
|
+
training on its own assumptions instead of on evidence. Every result
|
|
22
|
+
therefore carries an explicit ``status``:
|
|
23
|
+
|
|
24
|
+
ok — generator ran, predictions scored
|
|
25
|
+
error — generator failed (message retained)
|
|
26
|
+
timeout — generator exceeded its deadline
|
|
27
|
+
skipped — no generator configured for this target
|
|
28
|
+
|
|
29
|
+
Only ``ok`` results carry a score. The scorecard reports coverage alongside
|
|
30
|
+
results so a partial run can never be read as a complete one.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import concurrent.futures
|
|
35
|
+
import json
|
|
36
|
+
import shlex
|
|
37
|
+
import subprocess
|
|
38
|
+
import tempfile
|
|
39
|
+
import time
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from .cli import load_tasks, score_file
|
|
43
|
+
from .targets import get_target, list_targets
|
|
44
|
+
|
|
45
|
+
DEFAULT_TIMEOUT = 600 # seconds per target; generators are network-bound
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _tasks_jsonl() -> str:
|
|
49
|
+
return "\n".join(json.dumps(t, ensure_ascii=False) for t in load_tasks())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_target(target_id: str, command: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
53
|
+
"""Run one generator command and score its predictions.
|
|
54
|
+
|
|
55
|
+
``command`` is a shell-style command that reads tasks JSONL on stdin and
|
|
56
|
+
writes predictions JSONL ({"id":..., "prediction":[...]}) on stdout.
|
|
57
|
+
Returns a result dict with an explicit status; never raises for generator
|
|
58
|
+
failure (a broken target must not abort the whole scorecard).
|
|
59
|
+
"""
|
|
60
|
+
started = time.time()
|
|
61
|
+
base = {"target": target_id, "command": command}
|
|
62
|
+
try:
|
|
63
|
+
get_target(target_id) # validate against the registry
|
|
64
|
+
except Exception:
|
|
65
|
+
base["registry"] = "unregistered"
|
|
66
|
+
if not command:
|
|
67
|
+
return {**base, "status": "skipped", "score": None,
|
|
68
|
+
"reason": "no generator command configured"}
|
|
69
|
+
try:
|
|
70
|
+
proc = subprocess.run(
|
|
71
|
+
shlex.split(command), input=_tasks_jsonl(), text=True,
|
|
72
|
+
capture_output=True, timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
return {**base, "status": "timeout", "score": None,
|
|
76
|
+
"elapsed_s": round(time.time() - started, 1),
|
|
77
|
+
"reason": f"exceeded {timeout}s"}
|
|
78
|
+
except Exception as e: # generator binary missing, bad command, etc.
|
|
79
|
+
return {**base, "status": "error", "score": None,
|
|
80
|
+
"reason": f"{type(e).__name__}: {e}"}
|
|
81
|
+
if proc.returncode != 0:
|
|
82
|
+
return {**base, "status": "error", "score": None,
|
|
83
|
+
"reason": f"exit {proc.returncode}: {(proc.stderr or '').strip()[:200]}"}
|
|
84
|
+
if not proc.stdout.strip():
|
|
85
|
+
return {**base, "status": "error", "score": None,
|
|
86
|
+
"reason": "generator produced no predictions"}
|
|
87
|
+
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False,
|
|
88
|
+
encoding="utf-8") as fh:
|
|
89
|
+
fh.write(proc.stdout)
|
|
90
|
+
path = fh.name
|
|
91
|
+
try:
|
|
92
|
+
report = score_file(path)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return {**base, "status": "error", "score": None,
|
|
95
|
+
"reason": f"unscoreable predictions: {type(e).__name__}: {e}"}
|
|
96
|
+
finally:
|
|
97
|
+
Path(path).unlink(missing_ok=True)
|
|
98
|
+
return {**base, "status": "ok", "score": report["overall"], "report": report,
|
|
99
|
+
"elapsed_s": round(time.time() - started, 1)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_scorecard(generators: dict[str, str], timeout: int = DEFAULT_TIMEOUT,
|
|
103
|
+
max_workers: int = 4) -> dict:
|
|
104
|
+
"""Fan out across targets, converge to one scorecard.
|
|
105
|
+
|
|
106
|
+
``generators`` maps target_id -> generator command. Targets run in parallel
|
|
107
|
+
because they are genuinely independent; the scorecard is the convergence.
|
|
108
|
+
"""
|
|
109
|
+
results: list[dict] = []
|
|
110
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
111
|
+
futures = {pool.submit(run_target, tid, cmd, timeout): tid
|
|
112
|
+
for tid, cmd in generators.items()}
|
|
113
|
+
for fut in concurrent.futures.as_completed(futures):
|
|
114
|
+
results.append(fut.result())
|
|
115
|
+
ok = [r for r in results if r["status"] == "ok"]
|
|
116
|
+
ranked = sorted(ok, key=lambda r: r["score"], reverse=True)
|
|
117
|
+
untested = [r for r in results if r["status"] != "ok"]
|
|
118
|
+
registry_total = len(list_targets())
|
|
119
|
+
return {
|
|
120
|
+
"kipimo_tasks": len(load_tasks()),
|
|
121
|
+
"targets_attempted": len(results),
|
|
122
|
+
"targets_scored": len(ok),
|
|
123
|
+
"targets_untested": len(untested),
|
|
124
|
+
"registry_targets": registry_total,
|
|
125
|
+
# coverage makes an incomplete run impossible to misread as complete
|
|
126
|
+
"coverage": f"{len(ok)}/{registry_total} registry targets scored",
|
|
127
|
+
"ranked": [{"target": r["target"], "score": r["score"],
|
|
128
|
+
"by_type": {k: v for k, v in r["report"].items()
|
|
129
|
+
if k not in ("overall", "n_tasks", "n_missing")},
|
|
130
|
+
"n_missing": r["report"]["n_missing"]} for r in ranked],
|
|
131
|
+
"untested": [{"target": r["target"], "status": r["status"],
|
|
132
|
+
"reason": r.get("reason", "")} for r in untested],
|
|
133
|
+
"caveat": ("Scores are stack-routing competence, not general Swahili fluency. "
|
|
134
|
+
"Untested targets have UNKNOWN scores — not zero. A partial "
|
|
135
|
+
"scorecard is not a ranking of the field."),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_generators(path: str) -> dict[str, str]:
|
|
140
|
+
"""Load a target_id -> command mapping from a JSON file."""
|
|
141
|
+
with open(path, encoding="utf-8") as f:
|
|
142
|
+
data = json.load(f)
|
|
143
|
+
if not isinstance(data, dict):
|
|
144
|
+
raise ValueError("generators file must be a JSON object: {target_id: command}")
|
|
145
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""kipimo.pareto — turn a scorecard into a deployment decision.
|
|
2
|
+
|
|
3
|
+
A leaderboard answers "which model scored highest?". That is rarely the question
|
|
4
|
+
an African institution faces. Theirs is:
|
|
5
|
+
|
|
6
|
+
What is the CHEAPEST model we are allowed to run OURSELVES that clears the
|
|
7
|
+
competence bar for this task?
|
|
8
|
+
|
|
9
|
+
Answering it needs three things a score alone does not carry: what the model
|
|
10
|
+
costs to run, whether the licence permits self-hosting, and what hardware it
|
|
11
|
+
demands. This module joins measured accuracy (from the harness) with the
|
|
12
|
+
registry's deployment profile and operator-supplied cost, then reports:
|
|
13
|
+
|
|
14
|
+
* the **Pareto frontier** — targets not beaten on BOTH accuracy and cost by
|
|
15
|
+
some other target. Everything off the frontier is strictly dominated and can
|
|
16
|
+
be dropped from consideration without argument.
|
|
17
|
+
* the **cheapest qualifier** at a competence threshold, optionally restricted
|
|
18
|
+
to self-hostable / offline-capable / hardware-tier-limited targets. This is
|
|
19
|
+
the sovereign-deployment answer.
|
|
20
|
+
|
|
21
|
+
Cost is passed in per run, never stored in the registry: vendor prices move
|
|
22
|
+
weekly and differ by region, and a stale number in a public benchmark is worse
|
|
23
|
+
than no number. Targets without a cost are ranked on accuracy and reported
|
|
24
|
+
separately rather than silently assigned zero — the same discipline the harness
|
|
25
|
+
applies to untested targets.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from .targets import get_target
|
|
30
|
+
|
|
31
|
+
HARDWARE_ORDER = ["edge", "workstation", "server", "datacenter", "api-only"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _profile(target_id: str) -> dict:
|
|
35
|
+
try:
|
|
36
|
+
t = get_target(target_id)
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
return {k: t.get(k) for k in
|
|
40
|
+
("license", "self_hostable", "hardware_tier", "offline_capable", "family")}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_rows(scorecard: dict, costs: dict[str, float] | None = None) -> list[dict]:
|
|
44
|
+
"""Join harness results with registry profiles and operator-supplied costs.
|
|
45
|
+
|
|
46
|
+
``costs`` maps target_id -> cost per 1k tasks (any consistent currency).
|
|
47
|
+
"""
|
|
48
|
+
costs = costs or {}
|
|
49
|
+
rows = []
|
|
50
|
+
for r in scorecard.get("ranked", []):
|
|
51
|
+
tid = r["target"]
|
|
52
|
+
rows.append({"target": tid, "score": r["score"],
|
|
53
|
+
"cost_per_1k": costs.get(tid), # None = not supplied, not free
|
|
54
|
+
**_profile(tid)})
|
|
55
|
+
return rows
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def pareto_frontier(rows: list[dict]) -> list[dict]:
|
|
59
|
+
"""Targets not dominated on both accuracy (higher better) and cost (lower better).
|
|
60
|
+
|
|
61
|
+
Rows without a cost cannot be compared on cost and are excluded here; they
|
|
62
|
+
are reported separately by ``analyze`` rather than dropped silently.
|
|
63
|
+
"""
|
|
64
|
+
priced = [r for r in rows if r.get("cost_per_1k") is not None and r.get("score") is not None]
|
|
65
|
+
frontier = []
|
|
66
|
+
for r in priced:
|
|
67
|
+
dominated = any(
|
|
68
|
+
o is not r
|
|
69
|
+
and o["score"] >= r["score"]
|
|
70
|
+
and o["cost_per_1k"] <= r["cost_per_1k"]
|
|
71
|
+
and (o["score"] > r["score"] or o["cost_per_1k"] < r["cost_per_1k"])
|
|
72
|
+
for o in priced
|
|
73
|
+
)
|
|
74
|
+
if not dominated:
|
|
75
|
+
frontier.append(r)
|
|
76
|
+
return sorted(frontier, key=lambda r: r["cost_per_1k"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cheapest_qualifier(rows: list[dict], threshold: float, *,
|
|
80
|
+
self_hostable: bool | None = None,
|
|
81
|
+
offline_capable: bool | None = None,
|
|
82
|
+
max_hardware_tier: str | None = None) -> dict | None:
|
|
83
|
+
"""Cheapest target clearing ``threshold``, under deployment constraints.
|
|
84
|
+
|
|
85
|
+
With ``self_hostable=True`` this answers the sovereignty question directly:
|
|
86
|
+
the cheapest model an institution may lawfully run on its own hardware while
|
|
87
|
+
still clearing the competence bar.
|
|
88
|
+
"""
|
|
89
|
+
cands = [r for r in rows if (r.get("score") or 0) >= threshold]
|
|
90
|
+
if self_hostable is not None:
|
|
91
|
+
cands = [r for r in cands if r.get("self_hostable") is self_hostable]
|
|
92
|
+
if offline_capable is not None:
|
|
93
|
+
cands = [r for r in cands if r.get("offline_capable") is offline_capable]
|
|
94
|
+
if max_hardware_tier:
|
|
95
|
+
limit = HARDWARE_ORDER.index(max_hardware_tier)
|
|
96
|
+
cands = [r for r in cands
|
|
97
|
+
if r.get("hardware_tier") in HARDWARE_ORDER
|
|
98
|
+
and HARDWARE_ORDER.index(r["hardware_tier"]) <= limit]
|
|
99
|
+
if not cands:
|
|
100
|
+
return None
|
|
101
|
+
priced = [r for r in cands if r.get("cost_per_1k") is not None]
|
|
102
|
+
# cheapest if costs known; otherwise the qualifier on the smallest hardware
|
|
103
|
+
if priced:
|
|
104
|
+
return min(priced, key=lambda r: r["cost_per_1k"])
|
|
105
|
+
return min(cands, key=lambda r: HARDWARE_ORDER.index(r.get("hardware_tier") or "api-only"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def analyze(scorecard: dict, costs: dict[str, float] | None = None,
|
|
109
|
+
threshold: float = 0.8) -> dict:
|
|
110
|
+
"""Full deployment analysis over a harness scorecard."""
|
|
111
|
+
rows = build_rows(scorecard, costs)
|
|
112
|
+
unpriced = [r["target"] for r in rows if r.get("cost_per_1k") is None]
|
|
113
|
+
sovereign = cheapest_qualifier(rows, threshold, self_hostable=True)
|
|
114
|
+
edge = cheapest_qualifier(rows, threshold, self_hostable=True,
|
|
115
|
+
max_hardware_tier="workstation")
|
|
116
|
+
best = max(rows, key=lambda r: r["score"]) if rows else None
|
|
117
|
+
gap = None
|
|
118
|
+
if best and sovereign and best["score"]:
|
|
119
|
+
gap = round(sovereign["score"] / best["score"], 3)
|
|
120
|
+
return {
|
|
121
|
+
"threshold": threshold,
|
|
122
|
+
"rows": rows,
|
|
123
|
+
"pareto_frontier": [r["target"] for r in pareto_frontier(rows)],
|
|
124
|
+
"unpriced_targets": unpriced,
|
|
125
|
+
"best_overall": best["target"] if best else None,
|
|
126
|
+
"cheapest_sovereign_qualifier": sovereign["target"] if sovereign else None,
|
|
127
|
+
"cheapest_edge_qualifier": edge["target"] if edge else None,
|
|
128
|
+
"sovereign_vs_frontier_ratio": gap,
|
|
129
|
+
"reading": (
|
|
130
|
+
f"{sovereign['target']} is self-hostable, clears {threshold}, and reaches "
|
|
131
|
+
f"{int(gap*100)}% of the best measured target's score."
|
|
132
|
+
if sovereign and gap else
|
|
133
|
+
f"No self-hostable target clears {threshold} in this run."
|
|
134
|
+
),
|
|
135
|
+
"caveat": ("Costs are operator-supplied for this run, not registry facts. "
|
|
136
|
+
"Unpriced targets are excluded from the Pareto frontier — not "
|
|
137
|
+
"treated as free. Licences marked '(verify)' must be confirmed "
|
|
138
|
+
"on the model's own repository before deployment."),
|
|
139
|
+
}
|
|
@@ -15,6 +15,24 @@ Families:
|
|
|
15
15
|
|
|
16
16
|
`endpoint_env` names the environment variables an external generator is
|
|
17
17
|
expected to read (OpenAI-compatible convention). Nothing here reads them.
|
|
18
|
+
|
|
19
|
+
DEPLOYMENT PROFILE (optional, per target)
|
|
20
|
+
-----------------------------------------
|
|
21
|
+
Accuracy alone cannot answer the question East African institutions actually
|
|
22
|
+
face, which is not "which model is best?" but "what is the cheapest model we
|
|
23
|
+
can run ourselves that clears the competence bar?" Targets may therefore carry:
|
|
24
|
+
|
|
25
|
+
license SPDX-ish string ("MIT", "Apache-2.0", "CC-BY-NC-4.0",
|
|
26
|
+
"proprietary", "custom") — governs whether self-hosting and
|
|
27
|
+
commercial/civic redeployment are even permitted
|
|
28
|
+
self_hostable bool — weights obtainable and runnable outside a vendor API
|
|
29
|
+
hardware_tier "edge" | "workstation" | "server" | "datacenter" | "api-only"
|
|
30
|
+
offline_capable bool — can serve with no internet at inference time
|
|
31
|
+
|
|
32
|
+
Cost and latency are deliberately NOT stored here. Vendor prices change weekly
|
|
33
|
+
and vary by region; a number frozen in a registry is a number that quietly goes
|
|
34
|
+
wrong. Cost is supplied per-run by the operator (see kipimo.pareto) and latency
|
|
35
|
+
is measured by the harness. The registry holds only slow-moving facts.
|
|
18
36
|
"""
|
|
19
37
|
|
|
20
38
|
from __future__ import annotations
|
|
@@ -30,6 +48,10 @@ TARGETS: list[dict] = [
|
|
|
30
48
|
"params_b": None,
|
|
31
49
|
"endpoint_env": ["ANTHROPIC_API_KEY"],
|
|
32
50
|
"notes": "reference frontier closed model",
|
|
51
|
+
"license": "proprietary",
|
|
52
|
+
"self_hostable": False,
|
|
53
|
+
"hardware_tier": "api-only",
|
|
54
|
+
"offline_capable": False,
|
|
33
55
|
},
|
|
34
56
|
{
|
|
35
57
|
"id": "gpt-5.6",
|
|
@@ -38,6 +60,10 @@ TARGETS: list[dict] = [
|
|
|
38
60
|
"params_b": None,
|
|
39
61
|
"endpoint_env": ["OPENAI_API_KEY"],
|
|
40
62
|
"notes": "reference frontier closed model",
|
|
63
|
+
"license": "proprietary",
|
|
64
|
+
"self_hostable": False,
|
|
65
|
+
"hardware_tier": "api-only",
|
|
66
|
+
"offline_capable": False,
|
|
41
67
|
},
|
|
42
68
|
# --- open weights (frontier-adjacent, downloadable) ---
|
|
43
69
|
{
|
|
@@ -47,6 +73,10 @@ TARGETS: list[dict] = [
|
|
|
47
73
|
"params_b": 1000,
|
|
48
74
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
49
75
|
"notes": "weights public (HF, ~900k downloads); 1T MoE; strong agentic/tool-use; self-hostable via vLLM/SGLang/Ollama",
|
|
76
|
+
"license": "Modified-MIT",
|
|
77
|
+
"self_hostable": True,
|
|
78
|
+
"hardware_tier": "datacenter",
|
|
79
|
+
"offline_capable": True,
|
|
50
80
|
},
|
|
51
81
|
{
|
|
52
82
|
"id": "kimi-k3",
|
|
@@ -55,6 +85,10 @@ TARGETS: list[dict] = [
|
|
|
55
85
|
"params_b": 2800,
|
|
56
86
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
57
87
|
"notes": "2.8T MoE, 104B active; weights PUBLIC on HF (moonshotai/Kimi-K3, native multimodal, 1M ctx) under the custom \"Kimi K3 License\" — read LICENSE before redistribution (NOT MIT/Apache); strong agentic/MCP scores make it a valid frontier reference",
|
|
88
|
+
"license": "custom (Kimi K3 License)",
|
|
89
|
+
"self_hostable": True,
|
|
90
|
+
"hardware_tier": "datacenter",
|
|
91
|
+
"offline_capable": True,
|
|
58
92
|
},
|
|
59
93
|
{
|
|
60
94
|
"id": "glm-5.2",
|
|
@@ -63,6 +97,10 @@ TARGETS: list[dict] = [
|
|
|
63
97
|
"params_b": None,
|
|
64
98
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
65
99
|
"notes": "leads open-weight intelligence indices at time of listing",
|
|
100
|
+
"license": "MIT",
|
|
101
|
+
"self_hostable": True,
|
|
102
|
+
"hardware_tier": "datacenter",
|
|
103
|
+
"offline_capable": True,
|
|
66
104
|
},
|
|
67
105
|
{
|
|
68
106
|
"id": "deepseek-v4-pro",
|
|
@@ -71,6 +109,10 @@ TARGETS: list[dict] = [
|
|
|
71
109
|
"params_b": None,
|
|
72
110
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
73
111
|
"notes": "measurable regional install base (11-14% share in four African markets)",
|
|
112
|
+
"license": "MIT (verify on repo)",
|
|
113
|
+
"self_hostable": True,
|
|
114
|
+
"hardware_tier": "datacenter",
|
|
115
|
+
"offline_capable": True,
|
|
74
116
|
},
|
|
75
117
|
{
|
|
76
118
|
"id": "nemotron-3-ultra",
|
|
@@ -79,6 +121,10 @@ TARGETS: list[dict] = [
|
|
|
79
121
|
"params_b": None,
|
|
80
122
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
81
123
|
"notes": "US open-weight entrant; ships with open data and recipes",
|
|
124
|
+
"license": "NVIDIA-Open-Model (verify)",
|
|
125
|
+
"self_hostable": True,
|
|
126
|
+
"hardware_tier": "datacenter",
|
|
127
|
+
"offline_capable": True,
|
|
82
128
|
},
|
|
83
129
|
# --- small open (the deployment tier nobody measures) ---
|
|
84
130
|
{
|
|
@@ -88,6 +134,10 @@ TARGETS: list[dict] = [
|
|
|
88
134
|
"params_b": 14,
|
|
89
135
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
90
136
|
"notes": "runnable on a single consumer GPU",
|
|
137
|
+
"license": "Apache-2.0",
|
|
138
|
+
"self_hostable": True,
|
|
139
|
+
"hardware_tier": "workstation",
|
|
140
|
+
"offline_capable": True,
|
|
91
141
|
},
|
|
92
142
|
{
|
|
93
143
|
"id": "llama-4-scout",
|
|
@@ -96,6 +146,10 @@ TARGETS: list[dict] = [
|
|
|
96
146
|
"params_b": 17,
|
|
97
147
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
98
148
|
"notes": "active-parameter class; edge-adjacent",
|
|
149
|
+
"license": "Llama-4-Community",
|
|
150
|
+
"self_hostable": True,
|
|
151
|
+
"hardware_tier": "server",
|
|
152
|
+
"offline_capable": True,
|
|
99
153
|
},
|
|
100
154
|
{
|
|
101
155
|
"id": "gemma-3-12b",
|
|
@@ -104,6 +158,10 @@ TARGETS: list[dict] = [
|
|
|
104
158
|
"params_b": 12,
|
|
105
159
|
"endpoint_env": ["KIPIMO_BASE_URL", "KIPIMO_API_KEY"],
|
|
106
160
|
"notes": "runnable on a single consumer GPU",
|
|
161
|
+
"license": "Gemma-Terms",
|
|
162
|
+
"self_hostable": True,
|
|
163
|
+
"hardware_tier": "workstation",
|
|
164
|
+
"offline_capable": True,
|
|
107
165
|
},
|
|
108
166
|
{
|
|
109
167
|
"id": "tiny-aya-earth",
|
|
@@ -117,6 +175,10 @@ TARGETS: list[dict] = [
|
|
|
117
175
|
"CC-BY-NC-4.0. Sits between InkubaLM 0.4B and the ~12B tier. Verify exact "
|
|
118
176
|
"HF repo id before use."
|
|
119
177
|
),
|
|
178
|
+
"license": "CC-BY-NC-4.0",
|
|
179
|
+
"self_hostable": True,
|
|
180
|
+
"hardware_tier": "edge",
|
|
181
|
+
"offline_capable": True,
|
|
120
182
|
},
|
|
121
183
|
{
|
|
122
184
|
"id": "inkubalm-0.4b",
|
|
@@ -131,6 +193,10 @@ TARGETS: list[dict] = [
|
|
|
131
193
|
"non-commercial redistribution. The floor of the deployment tier this "
|
|
132
194
|
"benchmark exists to measure."
|
|
133
195
|
),
|
|
196
|
+
"license": "CC-BY-NC-4.0",
|
|
197
|
+
"self_hostable": True,
|
|
198
|
+
"hardware_tier": "edge",
|
|
199
|
+
"offline_capable": True,
|
|
134
200
|
},
|
|
135
201
|
]
|
|
136
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kipimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing).
|
|
5
5
|
Author: AfriKaziOS maintainers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,3 +65,51 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
65
65
|
|
|
66
66
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
67
67
|
<!-- /interconnect:v1 -->
|
|
68
|
+
|
|
69
|
+
## Running a scorecard (`kipimo run`)
|
|
70
|
+
|
|
71
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
72
|
+
still never calls a model API — you supply a *generator command* per target
|
|
73
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
74
|
+
stdout; see `examples/generate_predictions.py`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cat > generators.json <<'JSON'
|
|
78
|
+
{
|
|
79
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
80
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
81
|
+
"kimi-k3": ""
|
|
82
|
+
}
|
|
83
|
+
JSON
|
|
84
|
+
|
|
85
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
89
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
90
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
91
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
92
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
93
|
+
cannot be misread as a ranking of the field.
|
|
94
|
+
|
|
95
|
+
## From leaderboard to deployment decision (`kipimo analyze`)
|
|
96
|
+
|
|
97
|
+
A ranking says which model scored highest. Institutions need a different answer:
|
|
98
|
+
**what is the cheapest model we may lawfully run ourselves that clears the bar?**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
kipimo run generators.json > card.json
|
|
102
|
+
kipimo analyze card.json --costs costs.json --threshold 0.85
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Targets carry a deployment profile (`license`, `self_hostable`, `hardware_tier`,
|
|
106
|
+
`offline_capable`), which `analyze` joins with measured accuracy and
|
|
107
|
+
operator-supplied cost to report the Pareto frontier and the cheapest
|
|
108
|
+
self-hostable qualifier:
|
|
109
|
+
|
|
110
|
+
> *"gemma-3-12b is self-hostable, clears 0.85, and reaches 90% of the best
|
|
111
|
+
> measured target's score."*
|
|
112
|
+
|
|
113
|
+
Costs are supplied per run, never stored in the registry — vendor prices move
|
|
114
|
+
weekly and a stale number in a public benchmark is worse than none. A target
|
|
115
|
+
with no cost supplied is excluded from the frontier, **not treated as free**.
|
|
@@ -3,6 +3,8 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/kipimo/__init__.py
|
|
5
5
|
src/kipimo/cli.py
|
|
6
|
+
src/kipimo/harness.py
|
|
7
|
+
src/kipimo/pareto.py
|
|
6
8
|
src/kipimo/targets.py
|
|
7
9
|
src/kipimo.egg-info/PKG-INFO
|
|
8
10
|
src/kipimo.egg-info/SOURCES.txt
|
|
@@ -10,6 +12,8 @@ src/kipimo.egg-info/dependency_links.txt
|
|
|
10
12
|
src/kipimo.egg-info/entry_points.txt
|
|
11
13
|
src/kipimo.egg-info/top_level.txt
|
|
12
14
|
src/kipimo/data/kipimo_v0.1.jsonl
|
|
15
|
+
tests/test_harness.py
|
|
13
16
|
tests/test_kipimo.py
|
|
17
|
+
tests/test_pareto.py
|
|
14
18
|
tests/test_smoke.py
|
|
15
19
|
tests/test_targets.py
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Harness tests — the load-bearing property is that UNTESTED is never scored as zero."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from kipimo.harness import run_target, run_scorecard
|
|
6
|
+
|
|
7
|
+
GOOD = f'{sys.executable} -c "import sys,json\nfor l in sys.stdin:\n l=l.strip()\n if l: t=json.loads(l); print(json.dumps({{\'id\':t[\'id\'],\'prediction\':[\'mpesa-mcp\']}}))"'
|
|
8
|
+
CRASH = f'{sys.executable} -c "import sys; sys.exit(3)"'
|
|
9
|
+
EMPTY = f'{sys.executable} -c "pass"'
|
|
10
|
+
HANG = f'{sys.executable} -c "import time; time.sleep(60)"'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_working_generator_scores():
|
|
14
|
+
r = run_target("gemma-3-12b", GOOD, timeout=60)
|
|
15
|
+
assert r["status"] == "ok"
|
|
16
|
+
assert isinstance(r["score"], float)
|
|
17
|
+
assert r["report"]["n_missing"] == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_crashed_generator_is_error_not_zero():
|
|
21
|
+
r = run_target("gemma-3-12b", CRASH, timeout=60)
|
|
22
|
+
assert r["status"] == "error"
|
|
23
|
+
assert r["score"] is None # NOT 0.0 — we did not test it
|
|
24
|
+
assert "exit 3" in r["reason"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_empty_output_is_error_not_zero():
|
|
28
|
+
r = run_target("gemma-3-12b", EMPTY, timeout=60)
|
|
29
|
+
assert r["status"] == "error"
|
|
30
|
+
assert r["score"] is None
|
|
31
|
+
assert "no predictions" in r["reason"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_timeout_is_recorded_not_zero():
|
|
35
|
+
r = run_target("gemma-3-12b", HANG, timeout=2)
|
|
36
|
+
assert r["status"] == "timeout"
|
|
37
|
+
assert r["score"] is None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_unconfigured_target_is_skipped():
|
|
41
|
+
r = run_target("kimi-k3", "", timeout=5)
|
|
42
|
+
assert r["status"] == "skipped"
|
|
43
|
+
assert r["score"] is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_scorecard_separates_scored_from_untested():
|
|
47
|
+
card = run_scorecard({"gemma-3-12b": GOOD, "inkubalm-0.4b": CRASH, "kimi-k3": ""},
|
|
48
|
+
timeout=30, max_workers=3)
|
|
49
|
+
assert card["targets_scored"] == 1
|
|
50
|
+
assert card["targets_untested"] == 2
|
|
51
|
+
ranked_ids = [r["target"] for r in card["ranked"]]
|
|
52
|
+
assert ranked_ids == ["gemma-3-12b"] # only real results ranked
|
|
53
|
+
untested_ids = {r["target"] for r in card["untested"]}
|
|
54
|
+
assert untested_ids == {"inkubalm-0.4b", "kimi-k3"}
|
|
55
|
+
# coverage must make a partial run unmistakable
|
|
56
|
+
assert "/" in card["coverage"]
|
|
57
|
+
assert "not zero" in card["caveat"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_one_broken_target_does_not_abort_the_run():
|
|
61
|
+
card = run_scorecard({"a": CRASH, "b": GOOD}, timeout=30, max_workers=2)
|
|
62
|
+
assert card["targets_attempted"] == 2
|
|
63
|
+
assert card["targets_scored"] == 1 # the good one still ran
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_ranking_is_by_score_descending():
|
|
67
|
+
card = run_scorecard({"gemma-3-12b": GOOD, "qwen3-14b": GOOD}, timeout=30, max_workers=2)
|
|
68
|
+
scores = [r["score"] for r in card["ranked"]]
|
|
69
|
+
assert scores == sorted(scores, reverse=True)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Pareto/deployment tests — the sovereignty question, and honesty about unknowns."""
|
|
2
|
+
from kipimo.pareto import analyze, build_rows, cheapest_qualifier, pareto_frontier
|
|
3
|
+
|
|
4
|
+
CARD = {"ranked": [
|
|
5
|
+
{"target": "gpt-5.6", "score": 0.95}, {"target": "glm-5.2", "score": 0.93},
|
|
6
|
+
{"target": "qwen3-14b", "score": 0.88}, {"target": "gemma-3-12b", "score": 0.86},
|
|
7
|
+
{"target": "inkubalm-0.4b", "score": 0.61}], "untested": []}
|
|
8
|
+
COSTS = {"gpt-5.6": 12.5, "glm-5.2": 1.8, "qwen3-14b": 0.09,
|
|
9
|
+
"gemma-3-12b": 0.05, "inkubalm-0.4b": 0.01}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_rows_join_registry_profile():
|
|
13
|
+
rows = build_rows(CARD, COSTS)
|
|
14
|
+
g = next(r for r in rows if r["target"] == "glm-5.2")
|
|
15
|
+
assert g["license"] == "MIT" and g["self_hostable"] is True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_dominated_target_is_off_the_frontier():
|
|
19
|
+
# a target both worse AND pricier than another must be excluded
|
|
20
|
+
card = {"ranked": [{"target": "qwen3-14b", "score": 0.88},
|
|
21
|
+
{"target": "gemma-3-12b", "score": 0.70}]}
|
|
22
|
+
front = pareto_frontier(build_rows(card, {"qwen3-14b": 0.09, "gemma-3-12b": 0.50}))
|
|
23
|
+
assert [r["target"] for r in front] == ["qwen3-14b"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_cheapest_sovereign_qualifier_excludes_api_only():
|
|
27
|
+
rows = build_rows(CARD, COSTS)
|
|
28
|
+
pick = cheapest_qualifier(rows, 0.85, self_hostable=True)
|
|
29
|
+
assert pick["target"] == "gemma-3-12b" # cheapest self-hostable clearing 0.85
|
|
30
|
+
assert pick["self_hostable"] is True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_proprietary_wins_overall_but_not_sovereignty():
|
|
34
|
+
a = analyze(CARD, COSTS, threshold=0.85)
|
|
35
|
+
assert a["best_overall"] == "gpt-5.6" # highest score
|
|
36
|
+
assert a["cheapest_sovereign_qualifier"] == "gemma-3-12b" # but not deployable
|
|
37
|
+
assert 0.8 < a["sovereign_vs_frontier_ratio"] < 1.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_hardware_tier_constraint_respected():
|
|
41
|
+
rows = build_rows(CARD, COSTS)
|
|
42
|
+
pick = cheapest_qualifier(rows, 0.85, self_hostable=True, max_hardware_tier="workstation")
|
|
43
|
+
assert pick["hardware_tier"] in ("edge", "workstation") # datacenter excluded
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_threshold_with_no_qualifier_reports_honestly():
|
|
47
|
+
a = analyze(CARD, COSTS, threshold=0.99)
|
|
48
|
+
assert a["cheapest_sovereign_qualifier"] is None
|
|
49
|
+
assert "No self-hostable target clears" in a["reading"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_unpriced_target_is_not_treated_as_free():
|
|
53
|
+
a = analyze(CARD, {"gpt-5.6": 12.5}, threshold=0.85) # only one cost supplied
|
|
54
|
+
assert "qwen3-14b" in a["unpriced_targets"]
|
|
55
|
+
assert "qwen3-14b" not in a["pareto_frontier"] # excluded, not ranked cheapest
|
|
@@ -6,15 +6,34 @@ from kipimo import FAMILIES, TARGETS, get_target, list_targets
|
|
|
6
6
|
from kipimo.cli import main
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
CORE = {"id", "label", "family", "params_b", "endpoint_env", "notes"}
|
|
10
|
+
PROFILE = {"license", "self_hostable", "hardware_tier", "offline_capable"}
|
|
11
|
+
HARDWARE = {"edge", "workstation", "server", "datacenter", "api-only"}
|
|
12
|
+
|
|
13
|
+
|
|
9
14
|
def test_schema_and_uniqueness():
|
|
10
15
|
ids = [t["id"] for t in TARGETS]
|
|
11
16
|
assert len(ids) == len(set(ids))
|
|
12
17
|
for t in TARGETS:
|
|
13
|
-
assert set(t)
|
|
18
|
+
assert CORE <= set(t), f"{t['id']} missing core keys"
|
|
19
|
+
assert set(t) <= CORE | PROFILE, f"{t['id']} has unknown keys"
|
|
14
20
|
assert t["family"] in FAMILIES
|
|
15
21
|
assert isinstance(t["endpoint_env"], list) and t["endpoint_env"]
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
def test_deployment_profile_is_valid_where_present():
|
|
25
|
+
for t in TARGETS:
|
|
26
|
+
if "hardware_tier" in t:
|
|
27
|
+
assert t["hardware_tier"] in HARDWARE, t["id"]
|
|
28
|
+
if "self_hostable" in t:
|
|
29
|
+
assert isinstance(t["self_hostable"], bool)
|
|
30
|
+
if "offline_capable" in t:
|
|
31
|
+
assert isinstance(t["offline_capable"], bool)
|
|
32
|
+
# an api-only target can be neither self-hostable nor offline
|
|
33
|
+
if t.get("hardware_tier") == "api-only":
|
|
34
|
+
assert t.get("self_hostable") is False and t.get("offline_capable") is False, t["id"]
|
|
35
|
+
|
|
36
|
+
|
|
18
37
|
def test_every_family_represented():
|
|
19
38
|
for fam in FAMILIES:
|
|
20
39
|
assert list_targets(fam), f"no targets in family {fam}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|