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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kipimo
3
- Version: 0.2.2
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.2.2"
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.2.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.2.2
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) == {"id", "label", "family", "params_b", "endpoint_env", "notes"}
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