podstack commited on
Commit
7a74082
·
verified ·
1 Parent(s): 126508a

Jedi v1.0

Browse files
Files changed (16) hide show
  1. .gitattributes +1 -0
  2. README.md +357 -0
  3. build.py +67 -0
  4. calibration.json +6 -0
  5. chat_template.jinja +54 -0
  6. common.py +165 -0
  7. config.json +57 -0
  8. eval_results.json +709 -0
  9. evaluate.py +279 -0
  10. gen.py +194 -0
  11. generation_config.json +14 -0
  12. jedi.py +99 -0
  13. model.safetensors +3 -0
  14. tokenizer.json +3 -0
  15. tokenizer_config.json +30 -0
  16. train.py +18 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: Qwen/Qwen2.5-0.5B-Instruct
3
+ license: apache-2.0
4
+ language: [en]
5
+ library_name: transformers
6
+ pipeline_tag: text-generation
7
+ tags: [verifier, answer-verification, llm-cascade, model-cascade, escalation, llm-router, orchestration, multi-agent]
8
+ model-index:
9
+ - name: Jedi v1.0
10
+ results:
11
+ - task: {type: text-classification, name: answer verification (accept / escalate)}
12
+ dataset: {type: custom, name: held-out test answers (2,532 answers to 633 unseen tasks)}
13
+ metrics:
14
+ - {type: roc_auc, value: 0.9195, name: AUROC (correct vs wrong answers)}
15
+ - {type: ece, value: 0.0170, name: expected calibration error}
16
+ - task: {type: text-generation, name: LLM escalation ladder}
17
+ dataset: {type: custom, name: Shaktimaan held-out text and code tasks (491 tasks)}
18
+ metrics:
19
+ - {type: accuracy, value: 0.9185, name: end-to-end task accuracy (threshold 0.7)}
20
+ - {type: relative_cost, value: 0.4538, name: cost vs always-strongest agent}
21
+ ---
22
+ # Jedi v1.0 (0.5B)
23
+
24
+ **Jedi** is a 0.5B-parameter **answer verifier** that runs an LLM escalation ladder. A cheap model answers first.
25
+ Jedi reads the task *and that answer* and returns a calibrated probability that the answer is correct. If the
26
+ probability is high enough, the answer is accepted. Otherwise the task goes to the next, more expensive model,
27
+ whose answer Jedi checks in turn. Jedi never answers tasks itself; it emits a single token.
28
+
29
+ Fine-tuned from [Qwen2.5-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct) on 38,558 graded
30
+ answers from three LLMs. It is the successor to [Shaktimaan](https://huggingface.co/podstack/Shaktimaan), which
31
+ picks a model *before* any answer exists (see [How Jedi differs from Shaktimaan](#how-jedi-differs-from-shaktimaan)).
32
+
33
+ ## At a glance
34
+ - **91.9%** end-to-end accuracy on 491 held-out text and code tasks at **0.45x** the cost of always using the
35
+ strongest model. Always using the strongest model gets 84.7%; Shaktimaan v1.0 gets 84.9% at 0.32x on the same tasks.
36
+ - Beats every single model in the pool. At threshold 0.3 it also beats both standard cascade checks (a format
37
+ check and self-consistency) on accuracy, cost and silent failures at once.
38
+ - Within 1.4 points of a *perfect* verifier (93.3%) on the same ladder.
39
+ - **AUROC 0.92** at telling correct answers from wrong ones on unseen tasks; calibrated confidence (ECE 0.017).
40
+ - One forward pass and **one output token** per check: ~56 ms at batch size 1, ~450 input tokens.
41
+ - **Weak outside its training domains**: AUROC 0.72 on BIG-Bench Hard and 0.59 on CommonsenseQA. See *Limitations*.
42
+
43
+ ## How it works
44
+
45
+ ```
46
+ task
47
+ |
48
+ v
49
+ +-----------------------+ answer +------------------+
50
+ | rung 1: glm-5.3-flash | ---------------> | Jedi | P(correct) >= threshold ?
51
+ | (cost 0.2) | | 0.5B, 1 token | --- yes --> return answer
52
+ +-----------------------+ +------------------+
53
+ | no (escalate)
54
+ v
55
+ +-----------------------+ answer +------------------+
56
+ | rung 2: qwen3.8-27b | ---------------> | Jedi | --- yes --> return answer
57
+ | (cost 0.5) | +------------------+
58
+ +-----------------------+
59
+ | no (escalate)
60
+ v
61
+ +-----------------------+
62
+ | rung 3: nemotron-ultra| -----------------------------------------------> return answer (always accepted)
63
+ | (cost 1.0) |
64
+ +-----------------------+
65
+ ```
66
+
67
+ 1. **What Jedi reads.** `common.judge_view()` builds a short header naming the agent, the task type, the answer
68
+ length and whether the answer was cut off at the token limit. It then appends the task (first 2,500 + last 1,000
69
+ characters) and the candidate's visible answer (first 1,500 + last 2,500 characters, with `<think>` blocks
70
+ removed). The system prompt is `common.JEDI_SYSTEM`.
71
+ 2. **What Jedi outputs.** One word, `accept` or `escalate`. Jedi is never sampled. The runtime reads the logits of
72
+ those two tokens at the first output position and takes the gap `z = logit(accept) - logit(escalate)`.
73
+ 3. **Calibration.** `P(correct) = sigmoid(a·z + b)`, with `a`, `b` fitted by Platt scaling on a held-out validation
74
+ split (`calibration.json`). This drops calibration error from 0.061 to 0.017 on the test split, so a threshold
75
+ of 0.7 means roughly "accept when at least 70% likely to be right".
76
+ 4. **The single dial.** The threshold trades cost for accuracy. Raise it to escalate more and catch more wrong
77
+ answers; lower it to save money. The top rung is always accepted, so every task gets an answer.
78
+
79
+ ## Quick start
80
+
81
+ ### Score one answer
82
+ ```python
83
+ import importlib.util, json, math, torch
84
+ from huggingface_hub import hf_hub_download
85
+ from transformers import AutoModelForCausalLM, AutoTokenizer
86
+
87
+ repo = "podstack/Jedi"
88
+ spec = importlib.util.spec_from_file_location("common", hf_hub_download(repo, "common.py"))
89
+ common = importlib.util.module_from_spec(spec); spec.loader.exec_module(common)
90
+ cal = json.load(open(hf_hub_download(repo, "calibration.json")))
91
+ tok = AutoTokenizer.from_pretrained(repo)
92
+ model = AutoModelForCausalLM.from_pretrained(repo, dtype=torch.bfloat16).eval()
93
+
94
+ def p_correct(task, answer, agent="glm-5.3-flash", kind="num", finish="stop"):
95
+ msgs = [{"role": "system", "content": common.JEDI_SYSTEM},
96
+ {"role": "user", "content": common.judge_view(task, answer, agent, kind, finish)}]
97
+ enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", return_dict=True)
98
+ with torch.no_grad():
99
+ logits = model(**enc).logits[0, -1].float()
100
+ gap = (logits[cal["accept_token"]] - logits[cal["escalate_token"]]).item()
101
+ return 1 / (1 + math.exp(-(cal["a"] * gap + cal["b"])))
102
+
103
+ task = "A shop sells pencils at 3 for $1.20. How much do 15 pencils cost, in dollars?"
104
+ print(p_correct(task, "Each pencil costs 1.20 / 3 = 0.40. 15 x 0.40 = 6.00.\nFinal answer: 6"))
105
+ print(p_correct(task, "15 pencils is 15 / 3 = 5 groups. 5 x 1.20 = 5.00.\nFinal answer: 5"))
106
+ ```
107
+
108
+ Always build the input with `common.judge_view(...)` and use `common.JEDI_SYSTEM` as the system prompt; Jedi was
109
+ trained on exactly that format. `kind` is the task type it was trained with: `num`, `math`, `mc`
110
+ (multiple choice), `code`, `exact`. `finish` is the agent's `finish_reason` (`"length"` means the answer was truncated).
111
+
112
+ ### Run the full ladder
113
+ `jedi.py` runs the ladder end to end against OpenAI-compatible endpoints:
114
+
115
+ ```bash
116
+ export JEDI_ENDPOINTS='{"glm-5.3-flash": "http://host-a:8000/v1", "qwen3.8-27b": "http://host-b:8000/v1",
117
+ "nemotron-ultra": "http://host-c:8000/v1"}'
118
+ python jedi.py "If a train travels 120 km in 1.5 hours, what is its average speed in km/h?"
119
+ python jedi.py --threshold 0.9 --code "Write a function that checks whether a string is a palindrome."
120
+ ```
121
+
122
+ It prints the final answer to stdout and a per-rung trace to stderr, e.g.
123
+ `{"agent": "glm-5.3-flash", "verdict": "accept", "confidence": 0.97, "escalate_to": null}`. Rungs missing from
124
+ `JEDI_ENDPOINTS` are skipped, so a two-rung ladder works too.
125
+
126
+ ## Agent pool (the ladder)
127
+ | rung | agent | model | relative cost / token |
128
+ |---|---|---|---|
129
+ | 1 | `glm-5.3-flash` | `zai-org/GLM-5.3-Flash` | 0.2 |
130
+ | 2 | `qwen3.8-27b` | `Qwen/Qwen3.8-27B` | 0.5 |
131
+ | 3 | `nemotron-ultra` | `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4` | 1.0 |
132
+
133
+ `kimi-vl` (`Kimi-VL-A3B-Thinking`) is not on the ladder, but its answers are in the training data. It is the weakest
134
+ of the three answering models, so it supplied most of the wrong answers Jedi learned from. "Relative cost" is a
135
+ per-token weight for roughly relative serving cost, not a vendor price.
136
+
137
+ ## How Jedi differs from Shaktimaan
138
+ Both models are 0.5B fine-tunes of Qwen2.5-0.5B-Instruct built for the same agent pool. They solve the same
139
+ problem, getting good answers without paying for the biggest model every time, at different moments.
140
+
141
+ | | **Shaktimaan v1.0** (router) | **Jedi v1.0** (verifier) |
142
+ |---|---|---|
143
+ | When it decides | **before** any model answers | **after** each answer |
144
+ | What it reads | the task only | the task **and** a specific candidate answer |
145
+ | What it outputs | route JSON: domain, difficulty, strategy, agent list (~30 tokens) | one token -> calibrated P(correct) |
146
+ | Question it learns | "which agent is likely to solve this kind of task?" | "is *this* answer right?" |
147
+ | Model calls per task | exactly the routed agent(s), all up front | 1 call, plus more only on escalation (1.15 on average at threshold 0.7) |
148
+ | Cost/quality control | fixed by training labels | one threshold, tunable at run time |
149
+ | Inputs it handles | text, code, images (routes to the vision agent), up to ~260k tokens (routes to long-context agents) | text and code; long inputs are truncated for judging |
150
+ | Extra latency | one router call before the agent | ~56 ms per check, plus the next rung's latency when it escalates (~14 s on average at 0.7) |
151
+
152
+ **Why the switch.** Shaktimaan's main finding was that, on ordinary text and code tasks, *which* mid-size model
153
+ solves a given task is largely sampling noise that cannot be predicted from the prompt. A router has to guess
154
+ blind. Jedi waits until a cheap answer exists and judges that answer: unsupported reasoning, arithmetic that doesn't
155
+ add up, truncation, hedging and code that won't pass its tests. Its errors are mostly answers that look right but
156
+ aren't, which a router cannot see at all.
157
+
158
+ **Head to head**, on the same 491 held-out tasks with the same fresh agent samples:
159
+
160
+ | policy | accuracy | relative cost (always-Ultra = 1.00) |
161
+ |---|---|---|
162
+ | Shaktimaan v1.0 | 84.9% | 0.323 |
163
+ | Jedi ladder, threshold 0.1 | 91.5% | 0.263 |
164
+ | Jedi ladder, threshold 0.7 | **91.9%** | 0.454 |
165
+
166
+ At threshold 0.1, Jedi is both more accurate (+6.5 points) and cheaper than Shaktimaan. (Shaktimaan's ensemble
167
+ routes are scored as the aggregating agent's own answer here, which is slightly conservative for Shaktimaan.)
168
+
169
+ **They are complementary.** Jedi's ladder is text-only with a 32k-context first rung; it has no notion of images or
170
+ 256k-token inputs. A sensible deployment routes by capability first (images -> the vision agent, very long inputs
171
+ -> long-context agents, as Shaktimaan or simple rules do) and sends everything else through Jedi's ladder.
172
+
173
+ ## Training data
174
+ Every task was answered by `glm-5.3-flash` (2 samples), `qwen3.8-27b` (1) and `kimi-vl` (1) at temperature 0.6
175
+ with a "be concise, end with `Final answer:`" prompt. Each answer was graded automatically: exact, numeric or
176
+ LaTeX-normalised matching, and execution of unit tests in a network-less Docker sandbox for code. That gave 28,296
177
+ graded answers on 7,074 tasks. A second **borderline pass** collected 8,580 more GLM and Qwen answers on the 2,145
178
+ tasks where the models disagreed. This is where the hard-to-judge answers are; those extra samples are used for
179
+ training only.
180
+
181
+ | source | category | tasks | grading | license |
182
+ |---|---|---|---|---|
183
+ | [GSM8K](https://huggingface.co/datasets/openai/gsm8k) | math word problems | 1,000 | numeric match | MIT |
184
+ | [MATH](https://huggingface.co/datasets/EleutherAI/hendrycks_math) | competition math | 1,000 | normalised LaTeX / numeric | MIT |
185
+ | [MMLU-Pro](https://huggingface.co/datasets/TIGER-Lab/MMLU-Pro) | knowledge MCQ (10 options) | 1,200 | option letter | MIT |
186
+ | [ARC-Challenge](https://huggingface.co/datasets/allenai/ai2_arc) | science MCQ | 800 | option letter | CC-BY-SA-4.0 |
187
+ | [MBPP](https://huggingface.co/datasets/google-research-datasets/mbpp) | Python coding | 700 | unit tests in sandbox | CC-BY-4.0 |
188
+ | [HumanEval](https://huggingface.co/datasets/openai/openai_humaneval) | Python coding | 164 | unit tests in sandbox | MIT |
189
+ | [AQuA-RAT](https://huggingface.co/datasets/deepmind/aqua_rat) | algebra word problems (MCQ) | 800 | option letter | Apache-2.0 |
190
+ | [CommonsenseQA](https://huggingface.co/datasets/tau/commonsense_qa) | commonsense MCQ | 600 | option letter | MIT |
191
+ | [BIG-Bench Hard](https://huggingface.co/datasets/lukaemon/bbh) | 27 reasoning tasks | 810 | exact match | MIT |
192
+
193
+ The first six sources use the same task ids as
194
+ [Shaktimaan-routing](https://huggingface.co/datasets/podstack/Shaktimaan-routing), so the two models can be
195
+ compared on identical tasks. Charts and long documents were left out.
196
+
197
+ **Solve rate per model** (share of answers graded correct):
198
+
199
+ | source | glm-5.3-flash | qwen3.8-27b | kimi-vl |
200
+ |---|---|---|---|
201
+ | GSM8K | 0.96 | 0.95 | 0.81 |
202
+ | MATH | 0.75 | 0.75 | 0.42 |
203
+ | MMLU-Pro | 0.81 | 0.80 | 0.49 |
204
+ | ARC-Challenge | 0.97 | 0.97 | 0.92 |
205
+ | MBPP | 0.93 | 0.82 | 0.65 |
206
+ | HumanEval | 0.94 | 0.91 | 0.81 |
207
+ | AQuA-RAT | 0.89 | 0.92 | 0.64 |
208
+ | CommonsenseQA | 0.80 | 0.81 | 0.75 |
209
+ | BIG-Bench Hard | 0.90 | 0.91 | 0.54 |
210
+
211
+ **Splits are by task, never by answer**, so no task appears in two splits:
212
+
213
+ | split | tasks | answers | wrong answers | used for |
214
+ |---|---|---|---|---|
215
+ | train | 5,261 | 38,558 | 40.0% (oversampled from ~18%) | fine-tuning |
216
+ | val | 370 | 1,480 | 17.6% | Platt calibration only |
217
+ | test | 633 | 2,532 | 17.6% | Shaktimaan's held-out test tasks + 10% of AQuA / CommonsenseQA |
218
+ | ood | 810 | 3,240 | 18.8% | all of BIG-Bench Hard, never trained on |
219
+
220
+ Each example's label is `accept` if the answer was graded correct and `escalate` otherwise. Held-out splits use
221
+ only the base samples, without the borderline extras, so every task is sampled the same way.
222
+
223
+ ## Training
224
+ | setting | value |
225
+ |---|---|
226
+ | base model | Qwen2.5-0.5B-Instruct (494M parameters) |
227
+ | method | full fine-tune, supervised (TRL `SFTTrainer`); loss on the one-word verdict only |
228
+ | epochs | 2 (2,380 steps) |
229
+ | learning rate | 2e-5, cosine schedule, 30 warm-up steps |
230
+ | effective batch size | 32 |
231
+ | max sequence length | 2,048 tokens |
232
+ | precision | bf16, gradient checkpointing |
233
+ | hardware / time | one MIG 1g.67gb slice of an NVIDIA B300, 1 h 54 min |
234
+ | eval loss (val) | 0.095 after epoch 1, 0.142 after epoch 2 (the published weights are from epoch 2) |
235
+
236
+ ## Results
237
+
238
+ ### As a verifier
239
+ Calibrated P(correct) on answers to tasks Jedi never saw. *False accept* = share of wrong answers scored >= 0.5;
240
+ *false escalate* = share of correct answers scored < 0.5.
241
+
242
+ | split | answers | wrong | AUROC | false accept | false escalate | ECE | Brier |
243
+ |---|---|---|---|---|---|---|---|
244
+ | test (in-domain) | 2,532 | 17.6% | **0.920** | 35.9% | 1.9% | 0.017 | 0.063 |
245
+ | val | 1,480 | 17.6% | 0.884 | 42.3% | 1.3% | 0.017 | 0.072 |
246
+ | ood (BIG-Bench Hard) | 3,240 | 18.8% | 0.720 | 70.2% | 6.7% | 0.060 | 0.140 |
247
+
248
+ **Test split by answering model and by source:**
249
+
250
+ | slice | answers | wrong | AUROC | false accept |
251
+ |---|---|---|---|---|
252
+ | glm-5.3-flash | 1,266 | 11.8% | 0.910 | 41.6% |
253
+ | qwen3.8-27b | 633 | 12.3% | 0.915 | 50.0% |
254
+ | kimi-vl | 633 | 34.6% | 0.896 | 26.9% |
255
+ | HumanEval | 72 | 9.7% | 0.991 | 14.3% |
256
+ | MATH | 424 | 31.8% | 0.963 | 14.1% |
257
+ | MBPP | 268 | 9.7% | 0.955 | 38.5% |
258
+ | MMLU-Pro | 480 | 29.4% | 0.928 | 34.0% |
259
+ | AQuA-RAT | 316 | 16.8% | 0.896 | 30.2% |
260
+ | GSM8K | 420 | 6.2% | 0.854 | 53.8% |
261
+ | ARC-Challenge | 300 | 4.0% | 0.842 | 91.7% |
262
+ | CommonsenseQA | 252 | 18.3% | 0.593 | 89.1% |
263
+
264
+ Jedi is strongest where correctness can be checked from the answer itself: code, multi-step math and
265
+ knowledge questions with worked reasoning. It is weakest where a wrong answer reads exactly like a right one,
266
+ such as single-hop commonsense and easy science multiple choice.
267
+
268
+ ### As the gate of the ladder
269
+ 491 held-out tasks: Shaktimaan's test tasks from GSM8K, MATH, MMLU-Pro, ARC, MBPP and HumanEval. GLM and Qwen
270
+ answers are fresh samples; Ultra's outcome and token count come from its graded run in Shaktimaan-routing (same task
271
+ ids). Cost = sum over called agents of (relative cost x output tokens), normalised so always-Ultra = 1.00. *Silent
272
+ failure* = a wrong answer accepted by a lower rung, i.e. a mistake the ladder had a chance to escalate and didn't.
273
+
274
+ | policy | accuracy | relative cost | silent failures | calls / task |
275
+ |---|---|---|---|---|
276
+ | Always `glm-5.3-flash` | 88.8% | 0.115 | - | 1.00 |
277
+ | Always `qwen3.8-27b` | 87.4% | 0.421 | - | 1.00 |
278
+ | Always `nemotron-ultra` | 84.7% | 1.000 | - | 1.00 |
279
+ | Shaktimaan v1.0 routing | 84.9% | 0.323 | - | - |
280
+ | Ladder + format check (escalate truncated / unparseable answers) | 89.8% | 0.406 | 7.3% | 1.07 |
281
+ | Ladder + self-consistency (escalate if two samples disagree) | 90.4% | 0.554 | 6.5% | 1.14 |
282
+ | **Ladder + Jedi, threshold 0.7** | **91.9%** | **0.454** | **2.9%** | 1.15 |
283
+ | Ladder + perfect verifier (oracle, upper bound) | 93.3% | 0.473 | 0.0% | 1.19 |
284
+
285
+ **Threshold sweep** (same 491 tasks):
286
+
287
+ | threshold | accuracy | relative cost | silent failures | calls / task | extra latency / task |
288
+ |---|---|---|---|---|---|
289
+ | 0.05 | 91.0% | 0.244 | 7.5% | 1.09 | 8.9 s |
290
+ | 0.1 | 91.5% | 0.263 | 6.5% | 1.10 | 9.5 s |
291
+ | 0.3 | 91.5% | 0.323 | 5.5% | 1.11 | 10.7 s |
292
+ | 0.5 | 91.7% | 0.387 | 4.5% | 1.12 | 12.1 s |
293
+ | **0.7** | **91.9%** | **0.454** | **2.9%** | 1.15 | 14.3 s |
294
+ | 0.8 | 91.0% | 0.506 | 2.9% | 1.20 | 16.4 s |
295
+ | 0.9 | 90.2% | 0.609 | 1.6% | 1.32 | 20.8 s |
296
+ | 0.95 | 87.0% | 0.891 | 1.4% | 1.65 | 34.5 s |
297
+
298
+ Above 0.7, accuracy *falls* even as silent failures keep dropping. Escalating a correct answer is not free: the
299
+ next rung can get it wrong, and in this pool Ultra's recorded solve rate (84.7%) is below GLM's. Use **0.7** for
300
+ the best accuracy, or **0.1-0.3** for most of the gain at a quarter of always-Ultra's cost.
301
+
302
+ ### Outside the training domains (two-rung ladder: GLM -> Qwen)
303
+ Cost is relative to always using Qwen.
304
+
305
+ | tasks | policy | accuracy | cost vs always-Qwen | silent failures |
306
+ |---|---|---|---|---|
307
+ | AQuA + CommonsenseQA test (142) | always GLM / always Qwen | 85.2% / 88.7% | 0.30 / 1.00 | - |
308
+ | | self-consistency | 88.7% | 1.05 | 7.0% |
309
+ | | Jedi @ 0.5 | 88.7% | 0.68 | 8.5% |
310
+ | | Jedi @ 0.7 | 87.3% | 0.72 | 8.5% |
311
+ | | oracle | 92.3% | 0.71 | 0.0% |
312
+ | BIG-Bench Hard, unseen (810) | always GLM / always Qwen | 90.1% / 90.6% | 0.14 / 1.00 | - |
313
+ | | self-consistency | 91.4% | 0.53 | 6.1% |
314
+ | | Jedi @ 0.7 | 91.1% | 0.27 | 7.8% |
315
+ | | Jedi @ 0.9 | 92.0% | 0.69 | 2.2% |
316
+ | | oracle | 94.6% | 0.37 | 0.0% |
317
+
318
+ On unseen task families the ladder still improves on both single models, because the cheap model is usually right
319
+ and Jedi rarely escalates correct answers. It catches far fewer wrong ones than it does in-domain, though. On the
320
+ CommonsenseQA-heavy new-domain set, Jedi at 0.7 does not beat always using Qwen.
321
+
322
+ ### Overhead
323
+ ~56 ms per check (batch size 1, one MIG 1g.67gb slice of a B300), ~451 input tokens, 1 output token. Checks are a
324
+ single forward pass and batch well.
325
+
326
+ ## Limitations
327
+ - **Domain-bound.** Verification skill transfers poorly to unseen task families (BIG-Bench Hard AUROC 0.72) and is
328
+ near chance on CommonsenseQA (0.59). Measure it on your own traffic before relying on a threshold.
329
+ - **A third of wrong in-domain answers get through at 0.5.** False-accept rates are highest for the strongest
330
+ models, whose mistakes are rarer and more fluent. Jedi is a cost-saving filter, not a correctness guarantee.
331
+ - **Pool-specific.** Trained on answers from GLM-5.3-Flash, Qwen3.8-27B and Kimi-VL; the agent name is part of its
332
+ input. Other models' answers are out of distribution, and a new pool needs new data and retraining.
333
+ - **Text and code only**, English only, single-turn tasks with a checkable final answer. Not trained on open-ended
334
+ writing, agentic / tool use, multi-turn or image inputs. Long tasks and answers are truncated (head + tail) for judging.
335
+ - **Labels inherit grading noise** from automatic answer extraction and matching.
336
+ - **Ladder numbers use recorded Ultra outcomes**, a single sample per task from the Shaktimaan-routing runs, not
337
+ fresh calls.
338
+ - **Validation loss rose in epoch 2** (0.095 -> 0.142) while AUROC and calibration stayed good; a one-epoch model
339
+ may be marginally better.
340
+
341
+ ## Files
342
+ | file | purpose |
343
+ |---|---|
344
+ | `model.safetensors`, `config.json`, tokenizer files | the verifier |
345
+ | `calibration.json` | Platt scaling `a`, `b` and the `accept` / `escalate` token ids |
346
+ | `common.py` | agent pool, ladder, prompts, `judge_view`, answer extraction and graders |
347
+ | `jedi.py` | end-to-end runtime (answer -> verify -> escalate) |
348
+ | `gen.py` | collects and grades agent answers (stage 1) |
349
+ | `build.py` | builds the accept / escalate dataset and task-level splits (stage 2) |
350
+ | `train.py` | fine-tuning script (stage 3) |
351
+ | `evaluate.py` | verifier metrics, ladder, baselines and threshold sweep (stage 4) |
352
+ | `eval_results.json` | raw evaluation output behind every number on this card |
353
+
354
+ The graded answer data is not published; `gen.py` and `build.py` regenerate it given endpoints for the three models.
355
+
356
+ ## License
357
+ Model weights: Apache-2.0 (as the base model). Task sources keep their own licenses (table above).
build.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage 2: turn graded answers into Jedi's verifier dataset.
2
+
3
+ One example per (task, agent answer): Jedi reads `judge_view(...)` and must reply `accept` (the answer was graded
4
+ correct) or `escalate` (graded wrong). Splits are by task, never by answer, so no task leaks across splits:
5
+ * test - the in-domain Shaktimaan-routing test tasks (text + code) plus 10% of the new-domain tasks;
6
+ * ood - every BIG-Bench Hard task (never trained on: measures generalisation to unseen task families);
7
+ * val - 6% of the remaining tasks, used only to calibrate confidence (Platt scaling);
8
+ * train - the rest. Wrong answers are the minority, so they are oversampled in train to ~40% of examples.
9
+ Extra samples from the borderline pass (gen.py BORDERLINE=1) are used for train only, so held-out splits keep the
10
+ same, unselected sampling for every task.
11
+ """
12
+ import json, random
13
+ from collections import Counter
14
+ from datasets import Dataset, DatasetDict, load_dataset
15
+ from common import JEDI_SYSTEM, judge_view
16
+
17
+ random.seed(0)
18
+ WRONG_SHARE = 0.40
19
+ BASE_SAMPLES = {"glm-5.3-flash": 2, "qwen3.8-27b": 1, "kimi-vl": 1}
20
+
21
+
22
+ def main():
23
+ tasks = {t["id"]: t for t in map(json.loads, open("data/tasks.jsonl"))}
24
+ answers = [a for a in map(json.loads, open("data/answers.jsonl")) if not a.get("error") and not a.get("skipped")]
25
+ shak_test = set(load_dataset("podstack/Shaktimaan-routing", split="test")["id"])
26
+ split = {}
27
+ for tid, t in tasks.items():
28
+ if t["source"] == "bbh":
29
+ split[tid] = "ood"
30
+ elif tid in shak_test or (t["source"] in ("aqua", "csqa") and random.random() < 0.10):
31
+ split[tid] = "test"
32
+ else:
33
+ split[tid] = "val" if random.random() < 0.06 else "train"
34
+ rows = {k: [] for k in ("train", "val", "test", "ood")}
35
+ for a in answers:
36
+ t = tasks[a["task_id"]]
37
+ if split[t["id"]] != "train" and a["sample"] >= BASE_SAMPLES.get(a["agent"], 1):
38
+ continue
39
+ verdict = "accept" if a["correct"] else "escalate"
40
+ view = judge_view(t["prompt"], a["text"], a["agent"], t["kind"], a["finish"])
41
+ rows[split[t["id"]]].append(dict(
42
+ task_id=t["id"], source=t["source"], kind=t["kind"], agent=a["agent"], sample=a["sample"],
43
+ correct=bool(a["correct"]), finish=a["finish"] or "", completion_tokens=a["completion_tokens"],
44
+ latency=a["latency"], pred=a["pred"], gold=t["gold"], judge_input=view, verdict=verdict,
45
+ prompt=[{"role": "system", "content": JEDI_SYSTEM}, {"role": "user", "content": view}],
46
+ completion=[{"role": "assistant", "content": verdict}]))
47
+ train = rows["train"]
48
+ wrong = [r for r in train if not r["correct"]]
49
+ need = int(WRONG_SHARE * (len(train) - len(wrong)) / (1 - WRONG_SHARE)) - len(wrong)
50
+ train += random.choices(wrong, k=max(0, need))
51
+ random.shuffle(train)
52
+ dd = DatasetDict({k: Dataset.from_list(v) for k, v in rows.items()})
53
+ dd.save_to_disk("data/jedi_judge")
54
+ print(dd)
55
+ for k, v in rows.items():
56
+ print(f"{k:5s} tasks={len({r['task_id'] for r in v}):5d} answers={len(v):6d} "
57
+ f"wrong={sum(not r['correct'] for r in v) / len(v):.1%} "
58
+ f"agents={dict(Counter(r['agent'] for r in v))}")
59
+ print("solve rate by source x agent (all answers):")
60
+ by = Counter((tasks[a["task_id"]]["source"], a["agent"]) for a in answers)
61
+ ok = Counter((tasks[a["task_id"]]["source"], a["agent"]) for a in answers if a["correct"])
62
+ for src in sorted({s for s, _ in by}):
63
+ print(f" {src:10s} " + " ".join(f"{ag}={ok[src, ag] / by[src, ag]:.2f}" for s, ag in sorted(by) if s == src))
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
calibration.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "a": 0.32936498522758484,
3
+ "b": 0.13781841099262238,
4
+ "accept_token": 10330,
5
+ "escalate_token": 81082
6
+ }
chat_template.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0]['role'] == 'system' %}
4
+ {{- messages[0]['content'] }}
5
+ {%- else %}
6
+ {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
7
+ {%- endif %}
8
+ {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
9
+ {%- for tool in tools %}
10
+ {{- "\n" }}
11
+ {{- tool | tojson }}
12
+ {%- endfor %}
13
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
14
+ {%- else %}
15
+ {%- if messages[0]['role'] == 'system' %}
16
+ {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
17
+ {%- else %}
18
+ {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
19
+ {%- endif %}
20
+ {%- endif %}
21
+ {%- for message in messages %}
22
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
23
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
24
+ {%- elif message.role == "assistant" %}
25
+ {{- '<|im_start|>' + message.role }}
26
+ {%- if message.content %}
27
+ {{- '\n' + message.content }}
28
+ {%- endif %}
29
+ {%- for tool_call in message.tool_calls %}
30
+ {%- if tool_call.function is defined %}
31
+ {%- set tool_call = tool_call.function %}
32
+ {%- endif %}
33
+ {{- '\n<tool_call>\n{"name": "' }}
34
+ {{- tool_call.name }}
35
+ {{- '", "arguments": ' }}
36
+ {{- tool_call.arguments | tojson }}
37
+ {{- '}\n</tool_call>' }}
38
+ {%- endfor %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- elif message.role == "tool" %}
41
+ {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
42
+ {{- '<|im_start|>user' }}
43
+ {%- endif %}
44
+ {{- '\n<tool_response>\n' }}
45
+ {{- message.content }}
46
+ {{- '\n</tool_response>' }}
47
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
48
+ {{- '<|im_end|>\n' }}
49
+ {%- endif %}
50
+ {%- endif %}
51
+ {%- endfor %}
52
+ {%- if add_generation_prompt %}
53
+ {{- '<|im_start|>assistant\n' }}
54
+ {%- endif %}
common.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared config for Jedi: the agent pool, the escalation ladder, prompts, capabilities, graders and the judge view."""
2
+ import json, os, re
3
+
4
+ # agent -> served model id, max concurrent requests we send, relative cost per token
5
+ POOL = {
6
+ "gemma-3-1b": dict(model="google/gemma-3-1b-it", concurrency=32, cost=0.02),
7
+ "kimi-vl": dict(model="Kimi-VL-A3B-Thinking", concurrency=64, cost=0.05),
8
+ "nemotron-lightning": dict(model="nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4", concurrency=64, cost=0.10),
9
+ "glm-5.3-flash": dict(model="zai-org/GLM-5.3-Flash", concurrency=32, cost=0.20),
10
+ "qwen3.8-27b": dict(model="Qwen/Qwen3.8-27B", concurrency=12, cost=0.50),
11
+ "nemotron-ultra": dict(model="nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4", concurrency=64, cost=1.00),
12
+ }
13
+ AGENTS = list(POOL) # ordered cheapest -> most expensive
14
+ STRONGEST = "nemotron-ultra"
15
+ CONTEXT = {"gemma-3-1b": 32768, "kimi-vl": 8192, "nemotron-lightning": 131072, "glm-5.3-flash": 32768,
16
+ "qwen3.8-27b": 32768, "nemotron-ultra": 262144}
17
+ VISION = {"kimi-vl"} # only agent served with image input enabled
18
+ MAX_OUT = 8192
19
+
20
+
21
+ LADDER = ["glm-5.3-flash", "qwen3.8-27b", "nemotron-ultra"] # cheap -> mid -> strongest; Jedi checks each rung
22
+
23
+
24
+ def endpoints() -> dict:
25
+ """Agent -> OpenAI-compatible base URL (".../v1"), from $JEDI_ENDPOINTS: inline JSON or a path to a JSON file."""
26
+ raw = os.environ["JEDI_ENDPOINTS"]
27
+ return json.load(open(raw)) if os.path.exists(raw) else json.loads(raw)
28
+
29
+ SOLVER_SYSTEM = ("Solve the task. Be concise. End your response with a final line exactly of the form:\n"
30
+ "Final answer: <answer>\nFor multiple-choice questions, the answer is the option letter only.")
31
+ CODE_SYSTEM = ("Write a correct, self-contained Python solution. Put the complete code (imports, required function "
32
+ "names and signatures) in a single ```python code block. Do not include tests or example usage.")
33
+
34
+ JEDI_SYSTEM = (
35
+ "You are Jedi, a verifier. You are shown a task and a candidate answer written by an AI agent. Do not solve "
36
+ "the task yourself. Judge whether the candidate's final answer is correct: look for reasoning that does not hold "
37
+ "up, arithmetic that does not add up, code that would fail its tests, hedging, truncation or a wrong answer "
38
+ "format. Reply with one word: accept if the answer is correct, escalate if it is likely wrong."
39
+ )
40
+ VERDICTS = ("accept", "escalate")
41
+
42
+
43
+ def est_tokens(text: str) -> int:
44
+ """Conservative token estimate (real tokenizers average ~3.5-4 chars/token on English)."""
45
+ return len(text) // 3 + 1
46
+
47
+
48
+ def max_out(agent: str, prompt_tokens: int) -> int:
49
+ return min(MAX_OUT, CONTEXT[agent] - prompt_tokens - 256)
50
+
51
+
52
+ def capable(agent: str, prompt: str, has_image: bool) -> bool:
53
+ if has_image and agent not in VISION:
54
+ return False
55
+ return max_out(agent, est_tokens(prompt) + 1500 * has_image) >= 1024
56
+
57
+
58
+ def truncate(prompt: str, head: int = 5000, tail: int = 1500) -> str:
59
+ return prompt if len(prompt) <= head + tail + 7 else prompt[:head] + "\n[...]\n" + prompt[-tail:]
60
+
61
+
62
+ def judge_view(prompt: str, answer: str, agent: str, kind: str, finish: str | None) -> str:
63
+ """What Jedi reads: who answered and how the answer ended, then the task and the visible answer (head + tail)."""
64
+ answer = strip_think(answer)
65
+ head = f"[agent: {agent}; task type: {kind}; answer ~{est_tokens(answer)} tokens" + \
66
+ ("; answer was cut off at the token limit" if finish == "length" else "") + "]"
67
+ return f"{head}\n### Task\n{truncate(prompt, 2500, 1000)}\n\n### Candidate answer\n{truncate(answer, 1500, 2500)}"
68
+
69
+
70
+ def strip_think(text: str) -> str:
71
+ text = text or ""
72
+ if "</think>" in text:
73
+ text = text.rsplit("</think>", 1)[1]
74
+ text = re.sub(r"◁think▷.*?◁/think▷", "", text, flags=re.S) # Kimi-VL thinking markers
75
+ return text.strip()
76
+
77
+
78
+ def extract_final(text: str) -> str:
79
+ text = strip_think(text)
80
+ m = re.findall(r"final answer\s*[::]\s*(.+)", text, flags=re.I)
81
+ if m:
82
+ return m[-1].strip()
83
+ m = re.findall(r"^\s*answer\s*[::]\s*(.+)", text, flags=re.I | re.M)
84
+ if m:
85
+ return m[-1].strip()
86
+ boxed = last_boxed(text)
87
+ if boxed is not None:
88
+ return boxed
89
+ lines = [l for l in text.splitlines() if l.strip()]
90
+ return lines[-1].strip() if lines else ""
91
+
92
+
93
+ def extract_code(text: str) -> str:
94
+ text = strip_think(text)
95
+ blocks = re.findall(r"```(?:python|py)?\s*\n(.*?)```", text, flags=re.S)
96
+ return blocks[-1] if blocks else ""
97
+
98
+
99
+ def last_boxed(s: str):
100
+ i = s.rfind("\\boxed{")
101
+ if i < 0:
102
+ return None
103
+ i += len("\\boxed{")
104
+ depth, j = 1, i
105
+ while j < len(s) and depth:
106
+ depth += {"{": 1, "}": -1}.get(s[j], 0)
107
+ j += 1
108
+ return s[i:j - 1] if depth == 0 else None
109
+
110
+
111
+ def _norm_math(s: str) -> str:
112
+ s = s.strip().strip("$").strip()
113
+ b = last_boxed(s)
114
+ if b is not None:
115
+ s = b
116
+ for a, b2 in [("\\left", ""), ("\\right", ""), ("\\!", ""), ("\\,", ""), ("\\;", ""), ("dfrac", "frac"),
117
+ ("tfrac", "frac"), ("^\\circ", ""), ("^{\\circ}", ""), ("\\%", ""), ("%", ""), ("\\text{", "{"),
118
+ ("\\mbox{", "{"), (" ", ""), ("\\$", "")]:
119
+ s = s.replace(a, b2)
120
+ s = re.sub(r"\\frac(\d)(\d)", r"\\frac{\1}{\2}", s)
121
+ s = s.rstrip(".")
122
+ if s.startswith("{") and s.endswith("}") and s.count("{") == 1:
123
+ s = s[1:-1]
124
+ return s
125
+
126
+
127
+ def _num(s: str):
128
+ s = s.replace(",", "").replace("$", "").strip()
129
+ m = re.findall(r"-?\d+(?:\.\d+)?", s)
130
+ return float(m[-1]) if m else None
131
+
132
+
133
+ def grade(kind: str, pred: str, gold: str) -> bool:
134
+ """Grades extracted answers. Code is graded by execution (see gen.run_tests), not here."""
135
+ if not pred:
136
+ return False
137
+ if kind == "mc":
138
+ m = re.search(r"\b([A-J])\b", pred.upper().replace("(", " ").replace(")", " "))
139
+ return bool(m) and m.group(1) == gold.upper()
140
+ if kind == "num":
141
+ p, g = _num(pred), _num(gold)
142
+ return p is not None and g is not None and abs(p - g) < 1e-6 * max(1, abs(g))
143
+ if kind == "relaxed": # ChartQA convention: numbers within 5%, otherwise case-insensitive exact match
144
+ pred = last_boxed(pred) or pred
145
+ pred = re.sub(r"\\(?:mathbf|textbf|text|mathrm)\{([^}]*)\}", r"\1", pred).replace("\\(", "").replace("\\)", "")
146
+ p, g = _num(pred), _num(gold)
147
+ if g is not None and p is not None and re.fullmatch(r"[-\d.,%$ ]+", gold.strip()):
148
+ return abs(p - g) <= 0.05 * abs(g) if g else p == 0
149
+ return pred.strip().strip(".").lower() == gold.strip().lower()
150
+ if kind == "math":
151
+ p, g = _norm_math(pred), _norm_math(gold)
152
+ if p == g:
153
+ return True
154
+ try:
155
+ return abs(float(p) - float(g)) < 1e-6
156
+ except ValueError:
157
+ return False
158
+ if kind == "exact": # BIG-Bench Hard targets: an option like "(B)", or a short string / number / list
159
+ norm = lambda x: re.sub(r"\s+", " ", x.strip().strip("*").strip().rstrip(".").lower())
160
+ p, g = norm(pred), norm(gold)
161
+ if re.fullmatch(r"\([a-r]\)", g):
162
+ m = re.match(r"^\(?([a-r])\)?(?:\W|$)", p)
163
+ return bool(m) and m.group(1) == g[1]
164
+ return p == g
165
+ raise ValueError(kind)
config.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen2ForCausalLM"
4
+ ],
5
+ "attention_dropout": 0.0,
6
+ "bos_token_id": 151643,
7
+ "dtype": "float32",
8
+ "eos_token_id": 151645,
9
+ "hidden_act": "silu",
10
+ "hidden_size": 896,
11
+ "initializer_range": 0.02,
12
+ "intermediate_size": 4864,
13
+ "layer_types": [
14
+ "full_attention",
15
+ "full_attention",
16
+ "full_attention",
17
+ "full_attention",
18
+ "full_attention",
19
+ "full_attention",
20
+ "full_attention",
21
+ "full_attention",
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention",
28
+ "full_attention",
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention"
38
+ ],
39
+ "max_position_embeddings": 32768,
40
+ "max_window_layers": 21,
41
+ "model_type": "qwen2",
42
+ "num_attention_heads": 14,
43
+ "num_hidden_layers": 24,
44
+ "num_key_value_heads": 2,
45
+ "pad_token_id": 151643,
46
+ "rms_norm_eps": 1e-06,
47
+ "rope_parameters": {
48
+ "rope_theta": 1000000.0,
49
+ "rope_type": "default"
50
+ },
51
+ "sliding_window": null,
52
+ "tie_word_embeddings": true,
53
+ "transformers_version": "5.19.0",
54
+ "use_cache": false,
55
+ "use_sliding_window": false,
56
+ "vocab_size": 151936
57
+ }
eval_results.json ADDED
@@ -0,0 +1,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "calibration": {
3
+ "a": 0.32936498522758484,
4
+ "b": 0.13781841099262238,
5
+ "accept_token": 10330,
6
+ "escalate_token": 81082
7
+ },
8
+ "verifier": {
9
+ "val": {
10
+ "all": {
11
+ "n": 1480,
12
+ "answers_wrong": 0.1757,
13
+ "auroc": 0.8839,
14
+ "false_accept_rate": 0.4231,
15
+ "false_escalate_rate": 0.0131,
16
+ "ece": 0.0173,
17
+ "brier": 0.0723
18
+ },
19
+ "uncalibrated_ece": 0.0724,
20
+ "glm-5.3-flash": {
21
+ "n": 740,
22
+ "answers_wrong": 0.123,
23
+ "auroc": 0.8637,
24
+ "false_accept_rate": 0.5604,
25
+ "false_escalate_rate": 0.0046,
26
+ "ece": 0.0223,
27
+ "brier": 0.0629
28
+ },
29
+ "kimi-vl": {
30
+ "n": 370,
31
+ "answers_wrong": 0.3243,
32
+ "auroc": 0.9005,
33
+ "false_accept_rate": 0.2583,
34
+ "false_escalate_rate": 0.04,
35
+ "ece": 0.0632,
36
+ "brier": 0.0928
37
+ },
38
+ "qwen3.8-27b": {
39
+ "n": 370,
40
+ "answers_wrong": 0.1324,
41
+ "auroc": 0.8666,
42
+ "false_accept_rate": 0.5714,
43
+ "false_escalate_rate": 0.0093,
44
+ "ece": 0.0299,
45
+ "brier": 0.0704
46
+ },
47
+ "source:aqua": {
48
+ "n": 212,
49
+ "answers_wrong": 0.184,
50
+ "auroc": 0.8519,
51
+ "false_accept_rate": 0.4359,
52
+ "false_escalate_rate": 0.0058,
53
+ "ece": 0.0353,
54
+ "brier": 0.0735
55
+ },
56
+ "source:arc": {
57
+ "n": 160,
58
+ "answers_wrong": 0.0625,
59
+ "auroc": 0.742,
60
+ "false_accept_rate": 1.0,
61
+ "false_escalate_rate": 0.0067,
62
+ "ece": 0.0419,
63
+ "brier": 0.0556
64
+ },
65
+ "source:csqa": {
66
+ "n": 116,
67
+ "answers_wrong": 0.25,
68
+ "auroc": 0.4935,
69
+ "false_accept_rate": 0.931,
70
+ "false_escalate_rate": 0.023,
71
+ "ece": 0.1432,
72
+ "brier": 0.203
73
+ },
74
+ "source:gsm8k": {
75
+ "n": 264,
76
+ "answers_wrong": 0.0417,
77
+ "auroc": 0.9677,
78
+ "false_accept_rate": 0.2727,
79
+ "false_escalate_rate": 0.004,
80
+ "ece": 0.0499,
81
+ "brier": 0.0147
82
+ },
83
+ "source:humaneval": {
84
+ "n": 36,
85
+ "answers_wrong": 0.0278,
86
+ "auroc": 1.0,
87
+ "false_accept_rate": 0.0,
88
+ "false_escalate_rate": 0.0,
89
+ "ece": 0.0512,
90
+ "brier": 0.0055
91
+ },
92
+ "source:math": {
93
+ "n": 248,
94
+ "answers_wrong": 0.3024,
95
+ "auroc": 0.9272,
96
+ "false_accept_rate": 0.1467,
97
+ "false_escalate_rate": 0.0173,
98
+ "ece": 0.0534,
99
+ "brier": 0.0539
100
+ },
101
+ "source:mbpp": {
102
+ "n": 168,
103
+ "answers_wrong": 0.131,
104
+ "auroc": 0.9692,
105
+ "false_accept_rate": 0.2273,
106
+ "false_escalate_rate": 0.0274,
107
+ "ece": 0.0483,
108
+ "brier": 0.0449
109
+ },
110
+ "source:mmlu_pro": {
111
+ "n": 276,
112
+ "answers_wrong": 0.2645,
113
+ "auroc": 0.8385,
114
+ "false_accept_rate": 0.5068,
115
+ "false_escalate_rate": 0.0197,
116
+ "ece": 0.0517,
117
+ "brier": 0.1229
118
+ },
119
+ "reliability": [
120
+ {
121
+ "bin": "0.0-0.1",
122
+ "n": 104,
123
+ "confidence": 0.029,
124
+ "accuracy": 0.0
125
+ },
126
+ {
127
+ "bin": "0.1-0.2",
128
+ "n": 20,
129
+ "confidence": 0.13,
130
+ "accuracy": 0.1
131
+ },
132
+ {
133
+ "bin": "0.2-0.3",
134
+ "n": 17,
135
+ "confidence": 0.263,
136
+ "accuracy": 0.176
137
+ },
138
+ {
139
+ "bin": "0.3-0.4",
140
+ "n": 11,
141
+ "confidence": 0.355,
142
+ "accuracy": 0.273
143
+ },
144
+ {
145
+ "bin": "0.4-0.5",
146
+ "n": 14,
147
+ "confidence": 0.464,
148
+ "accuracy": 0.571
149
+ },
150
+ {
151
+ "bin": "0.5-0.6",
152
+ "n": 23,
153
+ "confidence": 0.563,
154
+ "accuracy": 0.696
155
+ },
156
+ {
157
+ "bin": "0.6-0.7",
158
+ "n": 36,
159
+ "confidence": 0.66,
160
+ "accuracy": 0.778
161
+ },
162
+ {
163
+ "bin": "0.7-0.8",
164
+ "n": 84,
165
+ "confidence": 0.762,
166
+ "accuracy": 0.81
167
+ },
168
+ {
169
+ "bin": "0.8-0.9",
170
+ "n": 252,
171
+ "confidence": 0.858,
172
+ "accuracy": 0.857
173
+ },
174
+ {
175
+ "bin": "0.9-1.0",
176
+ "n": 919,
177
+ "confidence": 0.96,
178
+ "accuracy": 0.953
179
+ }
180
+ ]
181
+ },
182
+ "test": {
183
+ "all": {
184
+ "n": 2532,
185
+ "answers_wrong": 0.1761,
186
+ "auroc": 0.9195,
187
+ "false_accept_rate": 0.3587,
188
+ "false_escalate_rate": 0.0187,
189
+ "ece": 0.017,
190
+ "brier": 0.0631
191
+ },
192
+ "uncalibrated_ece": 0.0614,
193
+ "glm-5.3-flash": {
194
+ "n": 1266,
195
+ "answers_wrong": 0.1177,
196
+ "auroc": 0.9102,
197
+ "false_accept_rate": 0.4161,
198
+ "false_escalate_rate": 0.0063,
199
+ "ece": 0.0106,
200
+ "brier": 0.0458
201
+ },
202
+ "kimi-vl": {
203
+ "n": 633,
204
+ "answers_wrong": 0.346,
205
+ "auroc": 0.896,
206
+ "false_accept_rate": 0.2694,
207
+ "false_escalate_rate": 0.0507,
208
+ "ece": 0.0418,
209
+ "brier": 0.1013
210
+ },
211
+ "qwen3.8-27b": {
212
+ "n": 633,
213
+ "answers_wrong": 0.1232,
214
+ "auroc": 0.9152,
215
+ "false_accept_rate": 0.5,
216
+ "false_escalate_rate": 0.0198,
217
+ "ece": 0.0248,
218
+ "brier": 0.0595
219
+ },
220
+ "source:aqua": {
221
+ "n": 316,
222
+ "answers_wrong": 0.1677,
223
+ "auroc": 0.8961,
224
+ "false_accept_rate": 0.3019,
225
+ "false_escalate_rate": 0.0304,
226
+ "ece": 0.0288,
227
+ "brier": 0.0627
228
+ },
229
+ "source:arc": {
230
+ "n": 300,
231
+ "answers_wrong": 0.04,
232
+ "auroc": 0.8423,
233
+ "false_accept_rate": 0.9167,
234
+ "false_escalate_rate": 0.0069,
235
+ "ece": 0.0286,
236
+ "brier": 0.0392
237
+ },
238
+ "source:csqa": {
239
+ "n": 252,
240
+ "answers_wrong": 0.1825,
241
+ "auroc": 0.5933,
242
+ "false_accept_rate": 0.8913,
243
+ "false_escalate_rate": 0.0583,
244
+ "ece": 0.087,
245
+ "brier": 0.1548
246
+ },
247
+ "source:gsm8k": {
248
+ "n": 420,
249
+ "answers_wrong": 0.0619,
250
+ "auroc": 0.8544,
251
+ "false_accept_rate": 0.5385,
252
+ "false_escalate_rate": 0.0025,
253
+ "ece": 0.0175,
254
+ "brier": 0.0335
255
+ },
256
+ "source:humaneval": {
257
+ "n": 72,
258
+ "answers_wrong": 0.0972,
259
+ "auroc": 0.9912,
260
+ "false_accept_rate": 0.1429,
261
+ "false_escalate_rate": 0.0,
262
+ "ece": 0.0366,
263
+ "brier": 0.0148
264
+ },
265
+ "source:math": {
266
+ "n": 424,
267
+ "answers_wrong": 0.3184,
268
+ "auroc": 0.9625,
269
+ "false_accept_rate": 0.1407,
270
+ "false_escalate_rate": 0.0035,
271
+ "ece": 0.033,
272
+ "brier": 0.0427
273
+ },
274
+ "source:mbpp": {
275
+ "n": 268,
276
+ "answers_wrong": 0.097,
277
+ "auroc": 0.9545,
278
+ "false_accept_rate": 0.3846,
279
+ "false_escalate_rate": 0.0124,
280
+ "ece": 0.041,
281
+ "brier": 0.0411
282
+ },
283
+ "source:mmlu_pro": {
284
+ "n": 480,
285
+ "answers_wrong": 0.2937,
286
+ "auroc": 0.9275,
287
+ "false_accept_rate": 0.3404,
288
+ "false_escalate_rate": 0.0354,
289
+ "ece": 0.0448,
290
+ "brier": 0.0935
291
+ },
292
+ "reliability": [
293
+ {
294
+ "bin": "0.0-0.1",
295
+ "n": 204,
296
+ "confidence": 0.03,
297
+ "accuracy": 0.005
298
+ },
299
+ {
300
+ "bin": "0.1-0.2",
301
+ "n": 32,
302
+ "confidence": 0.143,
303
+ "accuracy": 0.156
304
+ },
305
+ {
306
+ "bin": "0.2-0.3",
307
+ "n": 40,
308
+ "confidence": 0.248,
309
+ "accuracy": 0.175
310
+ },
311
+ {
312
+ "bin": "0.3-0.4",
313
+ "n": 23,
314
+ "confidence": 0.359,
315
+ "accuracy": 0.478
316
+ },
317
+ {
318
+ "bin": "0.4-0.5",
319
+ "n": 26,
320
+ "confidence": 0.454,
321
+ "accuracy": 0.577
322
+ },
323
+ {
324
+ "bin": "0.5-0.6",
325
+ "n": 33,
326
+ "confidence": 0.553,
327
+ "accuracy": 0.636
328
+ },
329
+ {
330
+ "bin": "0.6-0.7",
331
+ "n": 71,
332
+ "confidence": 0.657,
333
+ "accuracy": 0.746
334
+ },
335
+ {
336
+ "bin": "0.7-0.8",
337
+ "n": 153,
338
+ "confidence": 0.758,
339
+ "accuracy": 0.771
340
+ },
341
+ {
342
+ "bin": "0.8-0.9",
343
+ "n": 360,
344
+ "confidence": 0.86,
345
+ "accuracy": 0.878
346
+ },
347
+ {
348
+ "bin": "0.9-1.0",
349
+ "n": 1590,
350
+ "confidence": 0.961,
351
+ "accuracy": 0.968
352
+ }
353
+ ]
354
+ },
355
+ "ood": {
356
+ "all": {
357
+ "n": 3240,
358
+ "answers_wrong": 0.1877,
359
+ "auroc": 0.72,
360
+ "false_accept_rate": 0.7023,
361
+ "false_escalate_rate": 0.0672,
362
+ "ece": 0.0602,
363
+ "brier": 0.1398
364
+ },
365
+ "uncalibrated_ece": 0.1358,
366
+ "glm-5.3-flash": {
367
+ "n": 1620,
368
+ "answers_wrong": 0.0963,
369
+ "auroc": 0.6469,
370
+ "false_accept_rate": 0.9487,
371
+ "false_escalate_rate": 0.0342,
372
+ "ece": 0.0679,
373
+ "brier": 0.1001
374
+ },
375
+ "kimi-vl": {
376
+ "n": 810,
377
+ "answers_wrong": 0.4642,
378
+ "auroc": 0.6174,
379
+ "false_accept_rate": 0.5851,
380
+ "false_escalate_rate": 0.1797,
381
+ "ece": 0.119,
382
+ "brier": 0.2476
383
+ },
384
+ "qwen3.8-27b": {
385
+ "n": 810,
386
+ "answers_wrong": 0.0938,
387
+ "auroc": 0.6742,
388
+ "false_accept_rate": 0.7763,
389
+ "false_escalate_rate": 0.0668,
390
+ "ece": 0.1386,
391
+ "brier": 0.1115
392
+ },
393
+ "source:bbh": {
394
+ "n": 3240,
395
+ "answers_wrong": 0.1877,
396
+ "auroc": 0.72,
397
+ "false_accept_rate": 0.7023,
398
+ "false_escalate_rate": 0.0672,
399
+ "ece": 0.0602,
400
+ "brier": 0.1398
401
+ },
402
+ "reliability": [
403
+ {
404
+ "bin": "0.0-0.1",
405
+ "n": 30,
406
+ "confidence": 0.066,
407
+ "accuracy": 0.033
408
+ },
409
+ {
410
+ "bin": "0.1-0.2",
411
+ "n": 56,
412
+ "confidence": 0.149,
413
+ "accuracy": 0.125
414
+ },
415
+ {
416
+ "bin": "0.2-0.3",
417
+ "n": 73,
418
+ "confidence": 0.254,
419
+ "accuracy": 0.589
420
+ },
421
+ {
422
+ "bin": "0.3-0.4",
423
+ "n": 82,
424
+ "confidence": 0.357,
425
+ "accuracy": 0.598
426
+ },
427
+ {
428
+ "bin": "0.4-0.5",
429
+ "n": 117,
430
+ "confidence": 0.449,
431
+ "accuracy": 0.658
432
+ },
433
+ {
434
+ "bin": "0.5-0.6",
435
+ "n": 199,
436
+ "confidence": 0.553,
437
+ "accuracy": 0.734
438
+ },
439
+ {
440
+ "bin": "0.6-0.7",
441
+ "n": 375,
442
+ "confidence": 0.654,
443
+ "accuracy": 0.773
444
+ },
445
+ {
446
+ "bin": "0.7-0.8",
447
+ "n": 525,
448
+ "confidence": 0.755,
449
+ "accuracy": 0.794
450
+ },
451
+ {
452
+ "bin": "0.8-0.9",
453
+ "n": 906,
454
+ "confidence": 0.854,
455
+ "accuracy": 0.87
456
+ },
457
+ {
458
+ "bin": "0.9-1.0",
459
+ "n": 877,
460
+ "confidence": 0.939,
461
+ "accuracy": 0.928
462
+ }
463
+ ]
464
+ }
465
+ },
466
+ "ladder": [
467
+ "glm-5.3-flash",
468
+ "qwen3.8-27b",
469
+ "nemotron-ultra"
470
+ ],
471
+ "n_ladder_tasks": 491,
472
+ "policies": {
473
+ "always:glm-5.3-flash": {
474
+ "accuracy": 0.888,
475
+ "relative_cost": 0.1146,
476
+ "silent_failure_rate": 0.0,
477
+ "calls_per_task": 1.0,
478
+ "extra_agent_latency_s": 0.0
479
+ },
480
+ "always:qwen3.8-27b": {
481
+ "accuracy": 0.8737,
482
+ "relative_cost": 0.4208,
483
+ "silent_failure_rate": 0.0,
484
+ "calls_per_task": 1.0,
485
+ "extra_agent_latency_s": 0.0
486
+ },
487
+ "always:nemotron-ultra": {
488
+ "accuracy": 0.8473,
489
+ "relative_cost": 1.0,
490
+ "silent_failure_rate": 0.0,
491
+ "calls_per_task": 1.0,
492
+ "extra_agent_latency_s": 0.0
493
+ },
494
+ "ladder:format-check": {
495
+ "accuracy": 0.8982,
496
+ "relative_cost": 0.4058,
497
+ "silent_failure_rate": 0.0733,
498
+ "calls_per_task": 1.073,
499
+ "extra_agent_latency_s": 10.95
500
+ },
501
+ "ladder:self-consistency": {
502
+ "accuracy": 0.9043,
503
+ "relative_cost": 0.5535,
504
+ "silent_failure_rate": 0.0652,
505
+ "calls_per_task": 1.141,
506
+ "extra_agent_latency_s": 15.29
507
+ },
508
+ "ladder:oracle": {
509
+ "accuracy": 0.9328,
510
+ "relative_cost": 0.473,
511
+ "silent_failure_rate": 0.0,
512
+ "calls_per_task": 1.185,
513
+ "extra_agent_latency_s": 15.89
514
+ },
515
+ "shaktimaan-v1.0": {
516
+ "accuracy": 0.8493,
517
+ "relative_cost": 0.3226
518
+ }
519
+ },
520
+ "jedi_curve": [
521
+ {
522
+ "threshold": 0.05,
523
+ "accuracy": 0.9104,
524
+ "relative_cost": 0.2441,
525
+ "silent_failure_rate": 0.0754,
526
+ "calls_per_task": 1.086,
527
+ "extra_agent_latency_s": 8.94
528
+ },
529
+ {
530
+ "threshold": 0.1,
531
+ "accuracy": 0.9145,
532
+ "relative_cost": 0.2634,
533
+ "silent_failure_rate": 0.0652,
534
+ "calls_per_task": 1.096,
535
+ "extra_agent_latency_s": 9.49
536
+ },
537
+ {
538
+ "threshold": 0.2,
539
+ "accuracy": 0.9124,
540
+ "relative_cost": 0.2746,
541
+ "silent_failure_rate": 0.0652,
542
+ "calls_per_task": 1.098,
543
+ "extra_agent_latency_s": 9.69
544
+ },
545
+ {
546
+ "threshold": 0.3,
547
+ "accuracy": 0.9145,
548
+ "relative_cost": 0.3229,
549
+ "silent_failure_rate": 0.055,
550
+ "calls_per_task": 1.108,
551
+ "extra_agent_latency_s": 10.73
552
+ },
553
+ {
554
+ "threshold": 0.4,
555
+ "accuracy": 0.9145,
556
+ "relative_cost": 0.3551,
557
+ "silent_failure_rate": 0.0509,
558
+ "calls_per_task": 1.114,
559
+ "extra_agent_latency_s": 11.42
560
+ },
561
+ {
562
+ "threshold": 0.5,
563
+ "accuracy": 0.9165,
564
+ "relative_cost": 0.3865,
565
+ "silent_failure_rate": 0.0448,
566
+ "calls_per_task": 1.122,
567
+ "extra_agent_latency_s": 12.08
568
+ },
569
+ {
570
+ "threshold": 0.6,
571
+ "accuracy": 0.9165,
572
+ "relative_cost": 0.4313,
573
+ "silent_failure_rate": 0.0387,
574
+ "calls_per_task": 1.13,
575
+ "extra_agent_latency_s": 13.14
576
+ },
577
+ {
578
+ "threshold": 0.7,
579
+ "accuracy": 0.9185,
580
+ "relative_cost": 0.4538,
581
+ "silent_failure_rate": 0.0285,
582
+ "calls_per_task": 1.147,
583
+ "extra_agent_latency_s": 14.32
584
+ },
585
+ {
586
+ "threshold": 0.8,
587
+ "accuracy": 0.9104,
588
+ "relative_cost": 0.5055,
589
+ "silent_failure_rate": 0.0285,
590
+ "calls_per_task": 1.196,
591
+ "extra_agent_latency_s": 16.37
592
+ },
593
+ {
594
+ "threshold": 0.85,
595
+ "accuracy": 0.9084,
596
+ "relative_cost": 0.5408,
597
+ "silent_failure_rate": 0.0224,
598
+ "calls_per_task": 1.238,
599
+ "extra_agent_latency_s": 17.9
600
+ },
601
+ {
602
+ "threshold": 0.9,
603
+ "accuracy": 0.9022,
604
+ "relative_cost": 0.6093,
605
+ "silent_failure_rate": 0.0163,
606
+ "calls_per_task": 1.316,
607
+ "extra_agent_latency_s": 20.84
608
+ },
609
+ {
610
+ "threshold": 0.95,
611
+ "accuracy": 0.8697,
612
+ "relative_cost": 0.8913,
613
+ "silent_failure_rate": 0.0143,
614
+ "calls_per_task": 1.654,
615
+ "extra_agent_latency_s": 34.46
616
+ },
617
+ {
618
+ "threshold": 0.98,
619
+ "accuracy": 0.8615,
620
+ "relative_cost": 1.2413,
621
+ "silent_failure_rate": 0.0,
622
+ "calls_per_task": 2.393,
623
+ "extra_agent_latency_s": 55.17
624
+ }
625
+ ],
626
+ "two_rung": {
627
+ "new-domains (aqua, csqa)": {
628
+ "n_tasks": 142,
629
+ "always:glm-5.3-flash": {
630
+ "accuracy": 0.8521,
631
+ "cost_vs_always_mid": 0.2999,
632
+ "silent_failure_rate": 0.0
633
+ },
634
+ "always:qwen3.8-27b": {
635
+ "accuracy": 0.8873,
636
+ "cost_vs_always_mid": 1.0,
637
+ "silent_failure_rate": 0.0
638
+ },
639
+ "oracle": {
640
+ "accuracy": 0.9225,
641
+ "cost_vs_always_mid": 0.7111,
642
+ "silent_failure_rate": 0.0
643
+ },
644
+ "self-consistency": {
645
+ "accuracy": 0.8873,
646
+ "cost_vs_always_mid": 1.0475,
647
+ "silent_failure_rate": 0.0704
648
+ },
649
+ "jedi@0.5": {
650
+ "accuracy": 0.8873,
651
+ "cost_vs_always_mid": 0.6761,
652
+ "silent_failure_rate": 0.0845
653
+ },
654
+ "jedi@0.7": {
655
+ "accuracy": 0.8732,
656
+ "cost_vs_always_mid": 0.7191,
657
+ "silent_failure_rate": 0.0845
658
+ },
659
+ "jedi@0.9": {
660
+ "accuracy": 0.8873,
661
+ "cost_vs_always_mid": 0.8953,
662
+ "silent_failure_rate": 0.0493
663
+ }
664
+ },
665
+ "ood: bbh": {
666
+ "n_tasks": 810,
667
+ "always:glm-5.3-flash": {
668
+ "accuracy": 0.9012,
669
+ "cost_vs_always_mid": 0.1436,
670
+ "silent_failure_rate": 0.0
671
+ },
672
+ "always:qwen3.8-27b": {
673
+ "accuracy": 0.9062,
674
+ "cost_vs_always_mid": 1.0,
675
+ "silent_failure_rate": 0.0
676
+ },
677
+ "oracle": {
678
+ "accuracy": 0.9457,
679
+ "cost_vs_always_mid": 0.3743,
680
+ "silent_failure_rate": 0.0
681
+ },
682
+ "self-consistency": {
683
+ "accuracy": 0.9136,
684
+ "cost_vs_always_mid": 0.5322,
685
+ "silent_failure_rate": 0.0605
686
+ },
687
+ "jedi@0.5": {
688
+ "accuracy": 0.9049,
689
+ "cost_vs_always_mid": 0.178,
690
+ "silent_failure_rate": 0.0938
691
+ },
692
+ "jedi@0.7": {
693
+ "accuracy": 0.9111,
694
+ "cost_vs_always_mid": 0.2665,
695
+ "silent_failure_rate": 0.0778
696
+ },
697
+ "jedi@0.9": {
698
+ "accuracy": 0.9198,
699
+ "cost_vs_always_mid": 0.6854,
700
+ "silent_failure_rate": 0.0222
701
+ }
702
+ }
703
+ },
704
+ "overhead": {
705
+ "jedi_ms_per_check_batch1": 55.6,
706
+ "jedi_input_tokens_mean": 451,
707
+ "jedi_output_tokens": 1
708
+ }
709
+ }
evaluate.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage 4: evaluate Jedi as a verifier and as the gate of an escalation ladder.
2
+
3
+ Jedi's confidence is P(accept) read from its first output token (accept vs escalate), Platt-calibrated on the val
4
+ split. Reports, on held-out data:
5
+ * verifier quality per split / agent: AUROC, false-accept rate (wrong answers it lets through), ECE, Brier;
6
+ * the ladder glm-5.3-flash -> qwen3.8-27b -> nemotron-ultra, with Jedi checking rungs 1 and 2 and the top rung's
7
+ answer always accepted, swept over the confidence threshold (accuracy, relative cost, silent-failure rate);
8
+ * baselines on exactly the same tasks: every single agent, Shaktimaan v1.0 routing, a format check (escalate only
9
+ truncated / unparseable answers), self-consistency (escalate when two cheap samples disagree) and the oracle
10
+ ladder (a perfect verifier);
11
+ * overhead: Jedi's own latency per check and the extra agent latency on escalated tasks.
12
+ Answers for glm / qwen are fresh samples (sample 0). Nemotron-ultra's outcome and token count come from the graded
13
+ record in the Shaktimaan-routing dataset (same task ids), as do agents Shaktimaan routes to that were not re-run.
14
+ Relative cost = sum over called agents of (agent price x completion tokens), divided by always-nemotron-ultra.
15
+ """
16
+ import json, sys, time
17
+ from collections import defaultdict
18
+ import numpy as np
19
+ import torch
20
+ from datasets import load_dataset, load_from_disk
21
+ from transformers import AutoModelForCausalLM, AutoTokenizer
22
+ from common import LADDER, POOL, JEDI_SYSTEM, STRONGEST
23
+
24
+ path = sys.argv[1] if len(sys.argv) > 1 else "jedi-0.5b"
25
+ tok = AutoTokenizer.from_pretrained(path)
26
+ tok.padding_side = "left"
27
+ model = AutoModelForCausalLM.from_pretrained(path, dtype=torch.bfloat16).cuda().eval()
28
+ ds = load_from_disk("data/jedi_judge")
29
+ ACC, ESC = (tok.encode(w, add_special_tokens=False)[0] for w in ("accept", "escalate"))
30
+ assert ACC != ESC
31
+ CHEAP, MID = LADDER[0], LADDER[1]
32
+
33
+
34
+ def logit_gap(views, bs=16):
35
+ """accept-vs-escalate logit gap at Jedi's first output token, for each judge input."""
36
+ out = []
37
+ order = np.argsort([len(v) for v in views]) # length-sorted batches: far less padding
38
+ for i in range(0, len(views), bs):
39
+ idx = order[i:i + bs]
40
+ msgs = [[{"role": "system", "content": JEDI_SYSTEM}, {"role": "user", "content": views[j]}] for j in idx]
41
+ enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", padding=True,
42
+ return_dict=True).to("cuda")
43
+ with torch.no_grad():
44
+ last = model(**enc).logits[:, -1, :].float()
45
+ out += list(zip(idx, (last[:, ACC] - last[:, ESC]).tolist()))
46
+ gap = np.zeros(len(views))
47
+ for j, g in out:
48
+ gap[j] = g
49
+ return gap
50
+
51
+
52
+ def fit_platt(z, y):
53
+ a, b = torch.ones(1, requires_grad=True), torch.zeros(1, requires_grad=True)
54
+ zt, yt = torch.tensor(z, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)
55
+ opt = torch.optim.LBFGS([a, b], max_iter=200)
56
+
57
+ def closure():
58
+ opt.zero_grad()
59
+ loss = torch.nn.functional.binary_cross_entropy_with_logits(a * zt + b, yt)
60
+ loss.backward()
61
+ return loss
62
+ opt.step(closure)
63
+ return a.item(), b.item()
64
+
65
+
66
+ def sigmoid(x):
67
+ return 1 / (1 + np.exp(-x))
68
+
69
+
70
+ def auroc(p, y):
71
+ p, y = np.asarray(p), np.asarray(y, bool)
72
+ if y.all() or not y.any():
73
+ return None
74
+ ranks = np.argsort(np.argsort(p)) + 1
75
+ return float((ranks[y].sum() - y.sum() * (y.sum() + 1) / 2) / (y.sum() * (~y).sum()))
76
+
77
+
78
+ def ece(p, y, bins=10):
79
+ p, y = np.asarray(p), np.asarray(y, float)
80
+ edges = np.linspace(0, 1, bins + 1)
81
+ total, table = 0.0, []
82
+ for lo, hi in zip(edges[:-1], edges[1:]):
83
+ m = (p >= lo) & ((p < hi) | (hi == 1))
84
+ if m.any():
85
+ total += m.mean() * abs(p[m].mean() - y[m].mean())
86
+ table.append(dict(bin=f"{lo:.1f}-{hi:.1f}", n=int(m.sum()), confidence=round(float(p[m].mean()), 3),
87
+ accuracy=round(float(y[m].mean()), 3)))
88
+ return float(total), table
89
+
90
+
91
+ def verifier_metrics(p, y, tau=0.5):
92
+ p, y = np.asarray(p), np.asarray(y, bool)
93
+ e, _ = ece(p, y)
94
+ return dict(n=int(len(y)), answers_wrong=round(float(1 - y.mean()), 4), auroc=round(auroc(p, y) or 0, 4),
95
+ false_accept_rate=round(float(((p >= tau) & ~y).sum() / max(1, (~y).sum())), 4),
96
+ false_escalate_rate=round(float(((p < tau) & y).sum() / max(1, y.sum())), 4),
97
+ ece=round(e, 4), brier=round(float(((p - y) ** 2).mean()), 4))
98
+
99
+
100
+ # ---------- score every held-out answer ----------
101
+ z_val = logit_gap(ds["val"]["judge_input"])
102
+ a, b = fit_platt(z_val, ds["val"]["correct"])
103
+ cal = dict(a=a, b=b, accept_token=ACC, escalate_token=ESC)
104
+ P = {}
105
+ report = {"calibration": cal, "verifier": {}}
106
+ for split in ("val", "test", "ood"):
107
+ d = ds[split]
108
+ z = z_val if split == "val" else logit_gap(d["judge_input"])
109
+ p_raw, p = sigmoid(z), sigmoid(a * z + b)
110
+ y = np.array(d["correct"])
111
+ rep = {"all": verifier_metrics(p, y), "uncalibrated_ece": round(ece(p_raw, y)[0], 4)}
112
+ for ag in sorted(set(d["agent"])):
113
+ m = np.array(d["agent"]) == ag
114
+ rep[ag] = verifier_metrics(p[m], y[m])
115
+ for src in sorted(set(d["source"])):
116
+ m = np.array(d["source"]) == src
117
+ rep[f"source:{src}"] = verifier_metrics(p[m], y[m])
118
+ rep["reliability"] = ece(p, y)[1]
119
+ report["verifier"][split] = rep
120
+ for r, pi in zip(d.select_columns(["task_id", "agent", "sample", "correct", "completion_tokens", "latency",
121
+ "finish", "pred", "kind"]), p):
122
+ P[r["task_id"], r["agent"], r["sample"]] = dict(r, p=float(pi), split=split)
123
+ print(split, json.dumps(rep["all"]), flush=True)
124
+
125
+ # ---------- ladder ----------
126
+ shak = {r["id"]: r for sp in ("train", "test") for r in load_dataset("podstack/Shaktimaan-routing", split=sp)}
127
+
128
+
129
+ def norm_pred(s):
130
+ return (s or "").strip().strip(".").lower()
131
+
132
+
133
+ def format_ok(ans):
134
+ return ans["finish"] != "length" and bool(ans["pred"].strip())
135
+
136
+
137
+ def run_ladder(tasks, rungs, accept, final_record=None):
138
+ """tasks: task ids. rungs: agent names; accept(task_id, agent) -> (bool, extra_cost). The last rung is always
139
+ accepted; if final_record is given it supplies the last rung's (correct, tokens)."""
140
+ correct, cost, silent, calls, extra_latency = 0, 0.0, 0, 0, 0.0
141
+ for t in tasks:
142
+ for i, ag in enumerate(rungs):
143
+ last = i == len(rungs) - 1
144
+ if last and final_record:
145
+ c, toks, lat = final_record(t)
146
+ else:
147
+ ans = P[t, ag, 0]
148
+ c, toks, lat = ans["correct"], ans["completion_tokens"], ans["latency"]
149
+ cost += POOL[ag]["cost"] * toks
150
+ calls += 1
151
+ if i:
152
+ extra_latency += lat
153
+ if last:
154
+ correct += c
155
+ break
156
+ ok, extra = accept(t, ag)
157
+ cost += extra
158
+ if ok:
159
+ correct += c
160
+ silent += not c
161
+ break
162
+ n = len(tasks)
163
+ return dict(accuracy=correct / n, cost=cost, silent_failure_rate=silent / n, calls_per_task=calls / n,
164
+ extra_agent_latency_s=extra_latency / n)
165
+
166
+
167
+ def ultra_record(t):
168
+ r = json.loads(shak[t]["agent_results"])[STRONGEST]
169
+ return r["correct"], r["completion_tokens"], r["latency"]
170
+
171
+
172
+ def jedi(tau):
173
+ return lambda t, ag: (P[t, ag, 0]["p"] >= tau, 0.0)
174
+
175
+
176
+ def fmt(t, ag):
177
+ return format_ok(P[t, ag, 0]), 0.0
178
+
179
+
180
+ def self_consistency(t, ag):
181
+ s0, s1 = P[t, ag, 0], P.get((t, ag, 1))
182
+ if s1 is None or s0["kind"] == "code":
183
+ return format_ok(s0), 0.0
184
+ return format_ok(s0) and norm_pred(s0["pred"]) == norm_pred(s1["pred"]), POOL[ag]["cost"] * s1["completion_tokens"]
185
+
186
+
187
+ def oracle(t, ag):
188
+ return P[t, ag, 0]["correct"], 0.0
189
+
190
+
191
+ def ladder_tasks(split, sources):
192
+ return sorted({t for (t, _, _), v in P.items() if v["split"] == split and t.split("-")[0] in sources
193
+ and (t, CHEAP, 0) in P and (t, MID, 0) in P})
194
+
195
+
196
+ test_tasks = [t for t in ladder_tasks("test", {"gsm8k", "math", "mmlupro", "arc", "mbpp", "humaneval"}) if t in shak]
197
+ ultra_cost = sum(POOL[STRONGEST]["cost"] * ultra_record(t)[1] for t in test_tasks)
198
+
199
+
200
+ def rel(res):
201
+ return {"accuracy": round(res["accuracy"], 4), "relative_cost": round(res["cost"] / ultra_cost, 4),
202
+ "silent_failure_rate": round(res["silent_failure_rate"], 4),
203
+ "calls_per_task": round(res["calls_per_task"], 3),
204
+ "extra_agent_latency_s": round(res["extra_agent_latency_s"], 2)}
205
+
206
+
207
+ policies = {
208
+ f"always:{CHEAP}": rel(run_ladder(test_tasks, [CHEAP], None)),
209
+ f"always:{MID}": rel(run_ladder(test_tasks, [MID], None)),
210
+ f"always:{STRONGEST}": rel(run_ladder(test_tasks, [STRONGEST], None, ultra_record)),
211
+ "ladder:format-check": rel(run_ladder(test_tasks, LADDER, fmt, ultra_record)),
212
+ "ladder:self-consistency": rel(run_ladder(test_tasks, LADDER, self_consistency, ultra_record)),
213
+ "ladder:oracle": rel(run_ladder(test_tasks, LADDER, oracle, ultra_record)),
214
+ }
215
+
216
+ # Shaktimaan v1.0 on the same tasks: route with the published router, then score its choice.
217
+ rtok = AutoTokenizer.from_pretrained("podstack/Shaktimaan")
218
+ rtok.padding_side = "left"
219
+ router = AutoModelForCausalLM.from_pretrained("podstack/Shaktimaan", dtype=torch.bfloat16).cuda().eval()
220
+ routes = []
221
+ for i in range(0, len(test_tasks), 32):
222
+ msgs = [shak[t]["messages"][:2] for t in test_tasks[i:i + 32]]
223
+ enc = rtok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", padding=True,
224
+ return_dict=True).to("cuda")
225
+ with torch.no_grad():
226
+ out = router.generate(**enc, max_new_tokens=96, do_sample=False)
227
+ routes += rtok.batch_decode(out[:, enc["input_ids"].shape[1]:], skip_special_tokens=True)
228
+ del router
229
+ s_correct, s_cost = 0, 0.0
230
+ for t, rt in zip(test_tasks, routes):
231
+ try:
232
+ agents = [x for x in json.loads(rt)["agents"] if x in POOL] or [STRONGEST]
233
+ except Exception: # noqa: BLE001
234
+ agents = [STRONGEST]
235
+ rec = json.loads(shak[t]["agent_results"])
236
+ for ag in agents: # fresh sample where one exists, otherwise the recorded run
237
+ fresh = P.get((t, ag, 0))
238
+ s_cost += POOL[ag]["cost"] * (fresh["completion_tokens"] if fresh else rec[ag]["completion_tokens"])
239
+ fin = P.get((t, agents[-1], 0)) # ensembles are scored as the aggregating agent's own answer (conservative)
240
+ s_correct += fin["correct"] if fin else rec[agents[-1]]["correct"]
241
+ policies["shaktimaan-v1.0"] = {"accuracy": round(s_correct / len(test_tasks), 4),
242
+ "relative_cost": round(s_cost / ultra_cost, 4)}
243
+
244
+ curve = []
245
+ for tau in [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 0.98]:
246
+ curve.append(dict(threshold=tau, **rel(run_ladder(test_tasks, LADDER, jedi(tau), ultra_record))))
247
+
248
+ # two-rung ladder (no Ultra record) on the new domains and on unseen BIG-Bench Hard
249
+ two_rung = {}
250
+ for name, split, srcs in [("new-domains (aqua, csqa)", "test", {"aqua", "csqa"}), ("ood: bbh", "ood", {"bbh"})]:
251
+ ids = ladder_tasks(split, srcs)
252
+ base = sum(POOL[MID]["cost"] * P[t, MID, 0]["completion_tokens"] for t in ids)
253
+ r = lambda res: {"accuracy": round(res["accuracy"], 4), "cost_vs_always_mid": round(res["cost"] / base, 4),
254
+ "silent_failure_rate": round(res["silent_failure_rate"], 4)}
255
+ two_rung[name] = {"n_tasks": len(ids),
256
+ f"always:{CHEAP}": r(run_ladder(ids, [CHEAP], None)),
257
+ f"always:{MID}": r(run_ladder(ids, [MID], None)),
258
+ "oracle": r(run_ladder(ids, [CHEAP, MID], oracle)),
259
+ "self-consistency": r(run_ladder(ids, [CHEAP, MID], self_consistency)),
260
+ **{f"jedi@{tau}": r(run_ladder(ids, [CHEAP, MID], jedi(tau))) for tau in (0.5, 0.7, 0.9)}}
261
+
262
+ # overhead: one check at a time, as a live ladder would issue it
263
+ views = ds["test"]["judge_input"][:200]
264
+ torch.cuda.synchronize()
265
+ t0 = time.time()
266
+ for v in views:
267
+ logit_gap([v], bs=1)
268
+ torch.cuda.synchronize()
269
+ n_tok = np.mean([len(tok(tok.apply_chat_template([{"role": "system", "content": JEDI_SYSTEM},
270
+ {"role": "user", "content": v}], add_generation_prompt=True,
271
+ tokenize=False)).input_ids) for v in views])
272
+
273
+ report.update({"ladder": LADDER, "n_ladder_tasks": len(test_tasks), "policies": policies, "jedi_curve": curve,
274
+ "two_rung": two_rung,
275
+ "overhead": {"jedi_ms_per_check_batch1": round((time.time() - t0) / len(views) * 1000, 1),
276
+ "jedi_input_tokens_mean": round(float(n_tok)), "jedi_output_tokens": 1}})
277
+ print(json.dumps({k: v for k, v in report.items() if k != "verifier"}, indent=2))
278
+ json.dump(report, open(f"{path}/eval_results.json", "w"), indent=2)
279
+ json.dump(cal, open(f"{path}/calibration.json", "w"), indent=2)
gen.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage 1: collect graded (task, agent answer) pairs - the raw material for Jedi.
2
+
3
+ Tasks: the text and code sources Shaktimaan was built on (same ids: GSM8K, MATH, MMLU-Pro, ARC, MBPP, HumanEval) plus
4
+ new domains for coverage (AQuA-RAT, CommonsenseQA) and BIG-Bench Hard, which is kept out of training entirely as an
5
+ out-of-domain test. Every agent in $AGENTS answers every task it can take, $SAMPLES times (temperature 0.6), and the
6
+ full visible answer is stored with an automatic grade (exact / numeric / math-normalised match; code is graded by
7
+ running its tests in a no-network Docker sandbox).
8
+
9
+ Resumable: (task, agent, sample) triples already in data/answers.jsonl without an API error are skipped.
10
+ BORDERLINE=1 restricts the run to tasks whose existing answers disagree (some graded correct, some wrong) - used for
11
+ a second pass with more samples there, where the hard-to-judge answers are.
12
+ """
13
+ import asyncio, json, os, random, sys, time
14
+ import httpx
15
+ from datasets import get_dataset_config_names, load_dataset
16
+ from tqdm import tqdm
17
+ from common import (CODE_SYSTEM, POOL, SOLVER_SYSTEM, capable, endpoints, est_tokens, extract_code, extract_final,
18
+ grade, last_boxed, max_out, strip_think, truncate)
19
+
20
+ OUT = "data/answers.jsonl"
21
+ AGENTS = os.environ.get("AGENTS", "glm-5.3-flash,qwen3.8-27b,kimi-vl").split(",")
22
+ SAMPLES = {a: int(n) for a, n in (x.split("=") for x in os.environ.get(
23
+ "SAMPLES", "glm-5.3-flash=2,qwen3.8-27b=1,kimi-vl=1").split(","))}
24
+ SIZES = {"gsm8k": 1000, "math": 1000, "mmlu_pro": 1200, "arc": 800, "mbpp": 700, "aqua": 800, "csqa": 600,
25
+ "bbh": 30} # bbh: per sub-task (27 sub-tasks)
26
+ random.seed(0)
27
+
28
+
29
+ def load_tasks():
30
+ tasks = []
31
+ # --- sources shared with Shaktimaan (identical loading, so ids line up with the Shaktimaan-routing dataset) ---
32
+ ds = load_dataset("openai/gsm8k", "main", split="train").shuffle(seed=0).select(range(SIZES["gsm8k"]))
33
+ for i, r in enumerate(ds):
34
+ tasks.append(dict(id=f"gsm8k-{i}", source="gsm8k", domain="math", kind="num",
35
+ prompt=r["question"], gold=r["answer"].split("####")[-1].strip()))
36
+ rows = []
37
+ for cfg in ["algebra", "counting_and_probability", "geometry", "intermediate_algebra",
38
+ "number_theory", "prealgebra", "precalculus"]:
39
+ rows += list(load_dataset("EleutherAI/hendrycks_math", cfg, split="train"))
40
+ rows = [r for r in rows if last_boxed(r["solution"])]
41
+ random.shuffle(rows)
42
+ for i, r in enumerate(rows[:SIZES["math"]]):
43
+ tasks.append(dict(id=f"math-{i}", source="math", domain="math", kind="math",
44
+ prompt=r["problem"], gold=last_boxed(r["solution"])))
45
+ ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test").shuffle(seed=0).select(range(SIZES["mmlu_pro"]))
46
+ for i, r in enumerate(ds):
47
+ opts = "\n".join(f"{chr(65 + k)}. {o}" for k, o in enumerate(r["options"]))
48
+ tasks.append(dict(id=f"mmlupro-{i}", source="mmlu_pro", domain=r["category"], kind="mc",
49
+ prompt=f"{r['question']}\n\n{opts}", gold=r["answer"]))
50
+ ds = load_dataset("allenai/ai2_arc", "ARC-Challenge", split="train").shuffle(seed=0)
51
+ for i, r in enumerate(ds.select(range(min(SIZES["arc"], len(ds))))):
52
+ labels = r["choices"]["label"]
53
+ opts = "\n".join(f"{chr(65 + k)}. {t}" for k, t in enumerate(r["choices"]["text"]))
54
+ tasks.append(dict(id=f"arc-{i}", source="arc", domain="science", kind="mc",
55
+ prompt=f"{r['question']}\n\n{opts}", gold=chr(65 + labels.index(r["answerKey"]))))
56
+ mbpp = []
57
+ for split in ["train", "test", "validation", "prompt"]:
58
+ mbpp += list(load_dataset("google-research-datasets/mbpp", "full", split=split))
59
+ random.Random(2).shuffle(mbpp)
60
+ for r in mbpp[:SIZES["mbpp"]]:
61
+ tasks.append(dict(id=f"mbpp-{r['task_id']}", source="mbpp", domain="code", kind="code",
62
+ prompt=f"{r['text']}\nYour code should pass these tests:\n" + "\n".join(r["test_list"]),
63
+ gold=(r["test_setup_code"] or "") + "\n" + "\n".join(r["test_list"])))
64
+ for r in load_dataset("openai/openai_humaneval", split="test"):
65
+ tasks.append(dict(id=f"humaneval-{r['task_id'].split('/')[-1]}", source="humaneval", domain="code",
66
+ kind="code", prompt=f"Complete the following Python function:\n```python\n{r['prompt']}```",
67
+ gold=f"{r['test']}\ncheck({r['entry_point']})"))
68
+ # --- new domains ---
69
+ ds = load_dataset("deepmind/aqua_rat", "raw", split="train").shuffle(seed=0).select(range(SIZES["aqua"]))
70
+ for i, r in enumerate(ds):
71
+ opts = "\n".join(o.replace(")", ". ", 1) for o in r["options"])
72
+ tasks.append(dict(id=f"aqua-{i}", source="aqua", domain="math", kind="mc",
73
+ prompt=f"{r['question']}\n\n{opts}", gold=r["correct"]))
74
+ ds = load_dataset("tau/commonsense_qa", split="train").shuffle(seed=0).select(range(SIZES["csqa"]))
75
+ for r in ds:
76
+ opts = "\n".join(f"{l}. {t}" for l, t in zip(r["choices"]["label"], r["choices"]["text"]))
77
+ tasks.append(dict(id=f"csqa-{r['id']}", source="csqa", domain="commonsense", kind="mc",
78
+ prompt=f"{r['question']}\n\n{opts}", gold=r["answerKey"]))
79
+ # --- out-of-domain test only ---
80
+ for cfg in get_dataset_config_names("lukaemon/bbh"):
81
+ ds = load_dataset("lukaemon/bbh", cfg, split="test").shuffle(seed=0)
82
+ for i, r in enumerate(ds.select(range(min(SIZES["bbh"], len(ds))))):
83
+ tasks.append(dict(id=f"bbh-{cfg}-{i}", source="bbh", domain=f"bbh:{cfg}", kind="exact",
84
+ prompt=r["input"], gold=r["target"]))
85
+ for t in tasks:
86
+ t["est_tokens"] = est_tokens(t["prompt"])
87
+ return tasks
88
+
89
+
90
+ async def call(client, url, model, sem, messages, max_tokens):
91
+ """POST a chat completion. HTTP 400 (e.g. context overflow) is returned immediately as `rejected`."""
92
+ async with sem:
93
+ t0, err = time.time(), None
94
+ for attempt in range(4):
95
+ try:
96
+ r = await client.post(url, json={"model": model, "max_tokens": max_tokens, "temperature": 0.6,
97
+ "messages": messages})
98
+ if r.status_code == 400:
99
+ return dict(text="", rejected=r.text[:200], prompt_tokens=0, completion_tokens=0,
100
+ latency=time.time() - t0)
101
+ r.raise_for_status()
102
+ j = r.json()
103
+ return dict(text=j["choices"][0]["message"].get("content") or "",
104
+ finish=j["choices"][0].get("finish_reason"),
105
+ prompt_tokens=j["usage"]["prompt_tokens"],
106
+ completion_tokens=j["usage"]["completion_tokens"], latency=time.time() - t0)
107
+ except Exception as e: # noqa: BLE001
108
+ err = repr(e)[:200]
109
+ await asyncio.sleep(2 ** attempt)
110
+ return dict(text="", error=err, prompt_tokens=0, completion_tokens=0, latency=time.time() - t0)
111
+
112
+
113
+ SANDBOX = asyncio.Semaphore(24)
114
+
115
+
116
+ async def run_tests(code: str, tests: str) -> bool:
117
+ """Run model code + tests in a throwaway container: no network, 512 MB RAM, 1 CPU, 15 s."""
118
+ if not code.strip():
119
+ return False
120
+ async with SANDBOX:
121
+ proc = await asyncio.create_subprocess_exec(
122
+ "docker", "run", "--rm", "-i", "--network", "none", "--memory", "512m", "--cpus", "1",
123
+ "--pids-limit", "64", "--read-only", "--tmpfs", "/tmp:size=64m", "python:3.12-slim",
124
+ "timeout", "15", "python", "-", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.DEVNULL,
125
+ stderr=asyncio.subprocess.DEVNULL)
126
+ try:
127
+ await asyncio.wait_for(proc.communicate((code + "\n\n" + tests).encode()), timeout=60)
128
+ except asyncio.TimeoutError:
129
+ proc.kill()
130
+ return False
131
+ return proc.returncode == 0
132
+
133
+
134
+ async def answer(client, urls, sems, task, agent, sample):
135
+ key = dict(task_id=task["id"], agent=agent, sample=sample)
136
+ if not capable(agent, task["prompt"], False):
137
+ return dict(key, skipped="context")
138
+ system = CODE_SYSTEM if task["kind"] == "code" else SOLVER_SYSTEM
139
+ o = await call(client, urls[agent], POOL[agent]["model"], sems[agent],
140
+ [{"role": "system", "content": system}, {"role": "user", "content": task["prompt"]}],
141
+ max_out(agent, task["est_tokens"]))
142
+ if task["kind"] == "code":
143
+ pred = extract_code(o["text"])
144
+ correct = await run_tests(pred, task["gold"])
145
+ else:
146
+ pred = extract_final(o["text"])
147
+ correct = grade(task["kind"], pred, task["gold"])
148
+ return dict(key, text=truncate(strip_think(o["text"]), 6000, 6000), pred=pred[:300], correct=correct,
149
+ finish=o.get("finish"), error=o.get("error"), skipped="rejected" if o.get("rejected") else None,
150
+ **{k: o[k] for k in ("prompt_tokens", "completion_tokens", "latency")})
151
+
152
+
153
+ async def alive(client, urls, agent):
154
+ try:
155
+ r = await client.post(urls[agent], timeout=120, json={
156
+ "model": POOL[agent]["model"], "max_tokens": 4, "messages": [{"role": "user", "content": "hi"}]})
157
+ return r.status_code == 200
158
+ except Exception: # noqa: BLE001
159
+ return False
160
+
161
+
162
+ async def main():
163
+ os.makedirs("data", exist_ok=True)
164
+ tasks = load_tasks()
165
+ with open("data/tasks.jsonl", "w") as f:
166
+ for t in tasks:
167
+ f.write(json.dumps(t) + "\n")
168
+ done, grades = set(), {}
169
+ if os.path.exists(OUT):
170
+ for r in map(json.loads, open(OUT)):
171
+ if not r.get("error"):
172
+ done.add((r["task_id"], r["agent"], r["sample"]))
173
+ if not r.get("skipped"):
174
+ grades.setdefault(r["task_id"], set()).add(bool(r["correct"]))
175
+ if os.environ.get("BORDERLINE"):
176
+ tasks = [t for t in tasks if len(grades.get(t["id"], ())) == 2 and t["source"] != "bbh"]
177
+ urls = {n: f"{base.rstrip('/')}/chat/completions" for n, base in endpoints().items()}
178
+ sems = {n: asyncio.Semaphore(int(os.environ.get(f"CONC_{n}", POOL[n]["concurrency"]))) for n in POOL}
179
+ limits = httpx.Limits(max_connections=400, max_keepalive_connections=400)
180
+ async with httpx.AsyncClient(timeout=httpx.Timeout(1800), limits=limits) as client:
181
+ up = [a for a, ok in zip(AGENTS, await asyncio.gather(*[alive(client, urls, a) for a in AGENTS])) if ok]
182
+ print(f"{len(tasks)} tasks; agents up: {up}; down: {sorted(set(AGENTS) - set(up))}", flush=True)
183
+ jobs = [(t, a, s) for t in tasks for a in up for s in range(SAMPLES.get(a, 1))
184
+ if (t["id"], a, s) not in done]
185
+ print(f"{len(done)} answers done, {len(jobs)} to run", flush=True)
186
+ with open(OUT, "a") as f:
187
+ futs = [answer(client, urls, sems, t, a, s) for t, a, s in jobs]
188
+ for fut in tqdm(asyncio.as_completed(futs), total=len(futs), mininterval=60, file=sys.stdout):
189
+ f.write(json.dumps(await fut) + "\n")
190
+ f.flush()
191
+
192
+
193
+ if __name__ == "__main__":
194
+ asyncio.run(main())
generation_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 151643,
3
+ "do_sample": true,
4
+ "eos_token_id": [
5
+ 151645,
6
+ 151643
7
+ ],
8
+ "pad_token_id": 151643,
9
+ "repetition_penalty": 1.1,
10
+ "temperature": 0.7,
11
+ "top_k": 20,
12
+ "top_p": 0.8,
13
+ "transformers_version": "5.19.0"
14
+ }
jedi.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Jedi runtime: answer with the cheapest agent, let the 0.5B verifier check it, escalate up the ladder if needed.
2
+
3
+ Usage:
4
+ JEDI_ENDPOINTS='{"glm-5.3-flash": "http://host:8000/v1", ...}' python jedi.py [--threshold 0.7] [--code] "task"
5
+
6
+ The threshold is the single cost-vs-quality dial: raise it for accuracy (more escalations), lower it for savings.
7
+ Prints the final answer; the per-rung trace goes to stderr as JSON lines like
8
+ {"agent": "glm-5.3-flash", "verdict": "escalate", "confidence": 0.87, "escalate_to": "qwen3.8-27b"}
9
+ """
10
+ import argparse, json, math, os, re, sys
11
+ import httpx
12
+ import torch
13
+ from transformers import AutoModelForCausalLM, AutoTokenizer
14
+ from common import CODE_SYSTEM, LADDER, POOL, JEDI_SYSTEM, SOLVER_SYSTEM, endpoints, est_tokens, judge_view, max_out
15
+
16
+ MODEL = os.environ.get("JEDI_MODEL", "podstack/Jedi")
17
+
18
+
19
+ class Jedi:
20
+ def __init__(self, path=MODEL):
21
+ self.tok = AutoTokenizer.from_pretrained(path)
22
+ self.model = AutoModelForCausalLM.from_pretrained(path, dtype=torch.bfloat16).eval()
23
+ if torch.cuda.is_available():
24
+ self.model.cuda()
25
+ try:
26
+ from huggingface_hub import hf_hub_download
27
+ cal_file = os.path.join(path, "calibration.json") if os.path.isdir(path) else \
28
+ hf_hub_download(path, "calibration.json")
29
+ cal = json.load(open(cal_file))
30
+ except Exception: # noqa: BLE001
31
+ cal = {"a": 1.0, "b": 0.0}
32
+ self.a, self.b = cal["a"], cal["b"]
33
+ self.ids = [self.tok.encode(w, add_special_tokens=False)[0] for w in ("accept", "escalate")]
34
+
35
+ def p_correct(self, task, answer, agent, kind, finish=None):
36
+ """Calibrated probability that `answer` (written by `agent`) is correct."""
37
+ enc = self.tok.apply_chat_template(
38
+ [{"role": "system", "content": JEDI_SYSTEM},
39
+ {"role": "user", "content": judge_view(task, answer, agent, kind, finish)}],
40
+ add_generation_prompt=True, return_tensors="pt", return_dict=True).to(self.model.device)
41
+ with torch.no_grad():
42
+ logits = self.model(**enc).logits[0, -1].float()
43
+ gap = (logits[self.ids[0]] - logits[self.ids[1]]).item()
44
+ return 1 / (1 + math.exp(-(self.a * gap + self.b)))
45
+
46
+
47
+ def ask(agent, task, code):
48
+ base = endpoints()[agent].rstrip("/")
49
+ msgs = [{"role": "system", "content": CODE_SYSTEM if code else SOLVER_SYSTEM}, {"role": "user", "content": task}]
50
+ r = httpx.post(f"{base}/chat/completions", timeout=1800, json={
51
+ "model": POOL[agent]["model"], "messages": msgs, "max_tokens": max_out(agent, est_tokens(task))})
52
+ r.raise_for_status()
53
+ c = r.json()["choices"][0]
54
+ return c["message"].get("content") or "", c.get("finish_reason")
55
+
56
+
57
+ def infer_kind(task, code):
58
+ """Task type shown to Jedi (its training data used: code, mc, num, math, exact)."""
59
+ if code:
60
+ return "code"
61
+ return "mc" if re.search(r"^\(?[A-J][.)]\s", task, flags=re.M) else "math"
62
+
63
+
64
+ def solve(task, threshold=0.7, code=False, kind=None, judge=None):
65
+ """Run the ladder. Returns (final answer text, trace)."""
66
+ judge = judge or Jedi()
67
+ kind = kind or infer_kind(task, code)
68
+ rungs = [a for a in LADDER if a in endpoints()]
69
+ trace, text = [], ""
70
+ for i, agent in enumerate(rungs):
71
+ try:
72
+ text, finish = ask(agent, task, code)
73
+ except httpx.HTTPError as e:
74
+ if i == 0:
75
+ raise
76
+ trace.append({"agent": agent, "verdict": "unavailable", "error": repr(e)[:120]})
77
+ return text, trace # keep the best answer so far
78
+ if i == len(rungs) - 1:
79
+ trace.append({"agent": agent, "verdict": "final", "confidence": None, "escalate_to": None})
80
+ return text, trace
81
+ p = judge.p_correct(task, text, agent, kind, finish)
82
+ verdict = "accept" if p >= threshold else "escalate"
83
+ trace.append({"agent": agent, "verdict": verdict, "confidence": round(p if verdict == "accept" else 1 - p, 3),
84
+ "escalate_to": None if verdict == "accept" else rungs[i + 1]})
85
+ if verdict == "accept":
86
+ return text, trace
87
+
88
+
89
+ if __name__ == "__main__":
90
+ ap = argparse.ArgumentParser()
91
+ ap.add_argument("task", nargs="+")
92
+ ap.add_argument("--threshold", type=float, default=0.7, help="min P(correct) to accept an answer")
93
+ ap.add_argument("--code", action="store_true", help="ask agents for a Python code solution")
94
+ ap.add_argument("--kind", choices=["math", "num", "mc", "code", "exact"], help="task type (default: inferred)")
95
+ args = ap.parse_args()
96
+ answer, trace = solve(" ".join(args.task), args.threshold, args.code, args.kind)
97
+ for step in trace:
98
+ print(json.dumps(step), file=sys.stderr)
99
+ print(answer)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2319fb5813fe7baf027727e5ac7be4f46785e9835ec669317baf1c57496ab2d3
3
+ size 1976163472
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fd169731d2cbde95e10bf356d66d5997fd885dd8dbb6fb4684da3f23b2585d8
3
+ size 11421892
tokenizer_config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<|im_start|>",
10
+ "<|im_end|>",
11
+ "<|object_ref_start|>",
12
+ "<|object_ref_end|>",
13
+ "<|box_start|>",
14
+ "<|box_end|>",
15
+ "<|quad_start|>",
16
+ "<|quad_end|>",
17
+ "<|vision_start|>",
18
+ "<|vision_end|>",
19
+ "<|vision_pad|>",
20
+ "<|image_pad|>",
21
+ "<|video_pad|>"
22
+ ],
23
+ "is_local": false,
24
+ "local_files_only": false,
25
+ "model_max_length": 131072,
26
+ "pad_token": "<|endoftext|>",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null
30
+ }
train.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage 3: full fine-tune of Qwen2.5-0.5B-Instruct into Jedi (loss on the one-word verdict only)."""
2
+ import os
3
+ from datasets import load_from_disk
4
+ from trl import SFTConfig, SFTTrainer
5
+
6
+ BASE = os.environ.get("BASE", "Qwen/Qwen2.5-0.5B-Instruct")
7
+ ds = load_from_disk("data/jedi_judge").select_columns(["prompt", "completion"])
8
+
9
+ cfg = SFTConfig(
10
+ output_dir="ckpt", num_train_epochs=2, per_device_train_batch_size=8, gradient_accumulation_steps=4,
11
+ learning_rate=2e-5, lr_scheduler_type="cosine", warmup_steps=30, weight_decay=0.0, bf16=True,
12
+ max_length=2048, completion_only_loss=True, logging_steps=20, eval_strategy="epoch", save_strategy="epoch",
13
+ save_total_limit=1, report_to=[], gradient_checkpointing=True, dataloader_num_workers=4,
14
+ )
15
+ trainer = SFTTrainer(model=BASE, args=cfg, train_dataset=ds["train"], eval_dataset=ds["val"])
16
+ trainer.train()
17
+ trainer.save_model("jedi-0.5b")
18
+ trainer.processing_class.save_pretrained("jedi-0.5b")