Skip to content

Commit 5d1fdc1

Browse files
committed
Add xagent labels to router training
1 parent fc11d5f commit 5d1fdc1

8 files changed

Lines changed: 232 additions & 25 deletions

File tree

AGENTS.md

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ model will usually win raw score while losing the cost objective.
3434

3535
## Current Data Strategy
3636

37-
The production difficulty model is trained on **three datasets combined**
38-
(377,997 rows / ~14,364 prompts / 283 subjects):
37+
The production difficulty model is trained on **four datasets combined**
38+
(378,397 rows / ~14,463 prompts / 287 subjects):
3939

4040
```text
4141
NPULH/LLMRouterBench (350k stream sample) 37 models x ~13,775 prompts, single-turn
@@ -45,20 +45,23 @@ agent-psychometrics Terminal-Bench 2.0 89 tasks x 112 subjects, terminal ag
4545
data/agentic/terminalbench (--dataset agentic:agentic/terminalbench)
4646
agent-psychometrics SWE-bench Verified 500 tasks x 134 subjects, coding agent
4747
data/agentic/swebench_verified (task text joined from princeton-nlp/SWE-bench_Verified)
48+
Xorbits/xagent-xrouter-labels 100 prompts x 4 OpenRouter models, real xagent
49+
--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full
4850
```
4951

50-
Only the 37 llmrouterbench models have benchmark profiles, so they alone feed
51-
the capability/combine logistic; the agentic subjects (agent+scaffold combos)
52-
feed the difficulty axis only. agent-psychometrics swebench_pro (730x14) and
53-
gso (102x15) are loadable but NOT wired in (they ship no local task text and
54-
need an external join).
52+
Only models with benchmark profiles feed the capability/combine logistic: the
53+
37 llmrouterbench models plus profiled xagent OpenRouter candidates. The
54+
agent-psychometrics subjects (agent+scaffold combos) feed the difficulty axis
55+
only. agent-psychometrics swebench_pro (730x14) and gso (102x15) are loadable
56+
but NOT wired in (they ship no local task text and need an external join).
5557

5658
- agent-psychometrics matrices load via `agentic.py` and the CLI `agentic:`
5759
dataset kind. **terminalbench** (89x112, local `tasks.jsonl`) and
5860
**swebench_verified** (500x134, task text joined from
5961
`princeton-nlp/SWE-bench_Verified`) are wired in; see "Agentic training data".
6062
- All sources feed the difficulty axis; only the profiled llmrouterbench models
61-
feed the capability/combine logistic (agentic subjects have no profile).
63+
plus profiled xagent OpenRouter candidates feed the capability/combine
64+
logistic (agentic subjects have no profile).
6265
- RouterBench (`withmartian/routerbench`) is kept as a smaller legacy baseline.
6366
- `swebench_pro` (730x14), `gso` (102x15) ship no local task text and need an
6467
external join (not wired yet).
@@ -242,7 +245,8 @@ PYTHONPATH=src python3 -m xrouter_llm.cli train-irt \
242245
--dataset llmrouterbench:data/raw/llmrouterbench_stream_sample_350k \
243246
--dataset agentic:agentic/terminalbench \
244247
--dataset agentic:agentic/swebench_verified \
245-
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json \
248+
--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full \
249+
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json,src/xrouter_llm/resources/config/models \
246250
--output artifacts/models/irt_router_350k.joblib
247251
```
248252

@@ -270,7 +274,8 @@ PYTHONPATH=src python3 -m xrouter_llm.cli train-irt \
270274
--dataset llmrouterbench:data/raw/llmrouterbench_stream_sample_350k \
271275
--dataset agentic:agentic/terminalbench \
272276
--dataset agentic:agentic/swebench_verified \
273-
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json \
277+
--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full \
278+
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json,src/xrouter_llm/resources/config/models \
274279
--output artifacts/models/irt_router_350k.joblib
275280
```
276281

@@ -291,7 +296,8 @@ PYTHONPATH=src python3 -m xrouter_llm.cli train-irt \
291296
--dataset llmrouterbench:data/raw/llmrouterbench_stream_sample_350k \
292297
--dataset agentic:agentic/terminalbench \
293298
--dataset agentic:agentic/swebench_verified \
294-
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json \
299+
--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full \
300+
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json,src/xrouter_llm/resources/config/models \
295301
--output artifacts/models/irt_router_350k.joblib
296302
```
297303

@@ -301,6 +307,8 @@ PYTHONPATH=src python3 -m xrouter_llm.cli train-irt \
301307
joined from `princeton-nlp/SWE-bench_Verified` `problem_statement` into a local
302308
`data/agentic/swebench_verified/tasks.jsonl` (regenerate on a fresh checkout
303309
via `datasets.load_dataset("princeton-nlp/SWE-bench_Verified", split="test")`).
310+
- **xagent labels** are loaded from `Xorbits/xagent-xrouter-labels` via
311+
`xagent_labels.py` and the CLI `xagent-labels:` dataset kind.
304312
- **swebench_pro** (730x14), **gso** (102x15) ship no local task text and need
305313
an external join -- not wired yet.
306314

@@ -312,12 +320,10 @@ agentic subjects have no
312320
benchmark profiles, so they feed ONLY the difficulty axis; the capability/combine
313321
logistic still fits on profiled models.
314322

315-
Limitation (verified): real xagent prompts (e.g. Chinese business + image-gen
316-
agentic tasks) are NOT covered by SWE-bench/Terminal-Bench either, so they stay
317-
out-of-distribution and get a near-max (clamped) difficulty. Difficulty is
318-
clamped to the training range so P never collapses, but the only way to make it
319-
accurate for a specific task mix is that deployment's own logged
320-
prompts + outcomes.
323+
Limitation (verified): the public xagent labels are only a 100-prompt seed. They
324+
slightly improve the public benchmark headline under a controlled base test set,
325+
but real deployment accuracy for a specific task mix still needs that
326+
deployment's own logged prompts + outcomes.
321327

322328
## Evaluation Rules
323329

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33

44
# xrouter-llm
55

6-
<img src="./assets/xrouter-llm-hero-clean.png" alt="xrouter-llm: 52.4% lower cost and +1.7 pts completion on our tested dataset" />
6+
<img src="./assets/xrouter-llm-hero-clean.png" alt="xrouter-llm: 53.2% lower cost and +1.9 pts completion on our tested dataset" />
77

88
</div>
99

1010
Stop sending every prompt to your most expensive LLM.
1111

1212
`xrouter-llm` is a prompt-aware LLM **routing-decision** service: it predicts
1313
which models can complete a prompt, then chooses the cheapest model that clears
14-
the bar. On our tested dataset, it cuts realized cost by **52.4%** while
15-
improving completion by **+1.7 pts**.
14+
the bar. On our tested dataset, it cuts realized cost by **53.2%** while
15+
improving completion by **+1.9 pts**.
1616

1717
It answers "which model should serve this prompt?" and records the choice — it
1818
does NOT call the underlying LLMs.
@@ -116,10 +116,12 @@ The production difficulty model is trained on **multiple datasets combined**
116116
| `NPULH/LLMRouterBench` (350k stream sample) | single-turn QA / code / math (22 tasks) | 37 models x ~13.8k prompts ||
117117
| agent-psychometrics — Terminal-Bench 2.0 | terminal agent | 89 tasks x 112 subjects |`--dataset agentic:agentic/terminalbench` |
118118
| agent-psychometrics — SWE-bench Verified | coding agent | 500 tasks x 134 subjects | ✅ task text joined from `princeton-nlp/SWE-bench_Verified` |
119+
| `Xorbits/xagent-xrouter-labels` | real xagent internal prompts | 100 prompts x 4 OpenRouter models |`--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full` |
119120
| agent-psychometrics — SWE-bench Pro / GSO | coding agent | 730x14 / 102x15 | ⛔ ship no local task text, external join needed |
120121

121122
The current artifact trains on LLMRouterBench 350k **+ Terminal-Bench +
122-
SWE-bench Verified** (377,997 rows / ~14,364 prompts / 283 subjects). The
123+
SWE-bench Verified + xagent labels** (378,397 rows / ~14,463 prompts /
124+
287 subjects). The
123125
agentic matrices come from
124126
[agent-psychometrics](https://github.com/dariakryvosheieva/agent-psychometrics)
125127
(MIT) via `agentic.py`. In `IRTRouter`, only the 37 profiled llmrouterbench
@@ -137,7 +139,8 @@ xrouter-llm train-irt \
137139
--dataset llmrouterbench:data/raw/llmrouterbench_stream_sample_350k \
138140
--dataset agentic:agentic/terminalbench \
139141
--dataset agentic:agentic/swebench_verified \
140-
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json \
142+
--dataset xagent-labels:Xorbits/xagent-xrouter-labels:full \
143+
--benchmark-profiles artifacts/profiles/llmrouterbench_350k_profiles_priority_collected.json,src/xrouter_llm/resources/config/models \
141144
--output artifacts/models/irt_router_350k.joblib
142145
```
143146

assets/xrouter-llm-hero-clean.png

-75.7 KB
Loading

src/xrouter_llm/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
RouteDecision,
5959
UtilityBreakdown,
6060
)
61+
from xrouter_llm.xagent_labels import load_xagent_labels, load_xagent_openrouter_labels
6162

6263
__all__ = [
6364
"BenchmarkRow",
@@ -105,6 +106,8 @@
105106
"load_llmrouterbench",
106107
"load_routerbench_pickle",
107108
"load_jsonl",
109+
"load_xagent_labels",
110+
"load_xagent_openrouter_labels",
108111
"sample_llmrouterbench",
109112
"split_by_prompt",
110113
]

src/xrouter_llm/cli.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def main(argv: list[str] | None = None) -> int:
6565
sweep_parser.add_argument("--input", default=None)
6666
sweep_parser.add_argument(
6767
"--format",
68-
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench"],
68+
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench", "xagent-labels"],
6969
default="jsonl",
7070
)
7171
_add_sweep_args(sweep_parser)
@@ -83,7 +83,7 @@ def main(argv: list[str] | None = None) -> int:
8383
holdout_parser.add_argument("--input", default=None)
8484
holdout_parser.add_argument(
8585
"--format",
86-
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench"],
86+
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench", "xagent-labels"],
8787
default="jsonl",
8888
)
8989
holdout_parser.add_argument(
@@ -104,7 +104,7 @@ def main(argv: list[str] | None = None) -> int:
104104
train_irt_parser.add_argument("--input", default=None)
105105
train_irt_parser.add_argument(
106106
"--format",
107-
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench"],
107+
choices=["jsonl", "csv", "routerbench-pkl", "llmrouterbench", "xagent-labels"],
108108
default="llmrouterbench",
109109
)
110110
train_irt_parser.add_argument("--benchmark-profiles", default=default_models_dir())
@@ -416,6 +416,14 @@ def _load_dataset(kind: str, path: str, *, args: argparse.Namespace) -> list[obj
416416
max_prompts=args.max_prompts,
417417
random_state=args.random_state,
418418
)
419+
if kind == "xagent-labels":
420+
from xrouter_llm.xagent_labels import load_xagent_openrouter_labels
421+
422+
return limit_rows_by_prompt(
423+
load_xagent_openrouter_labels(path),
424+
max_prompts=args.max_prompts,
425+
random_state=args.random_state,
426+
)
419427
raise ValueError(f"Unsupported dataset kind {kind!r}")
420428

421429

4.76 KB
Binary file not shown.

src/xrouter_llm/xagent_labels.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
from typing import Any, Iterable
6+
7+
from huggingface_hub import hf_hub_download
8+
9+
from xrouter_llm.types import BenchmarkRow
10+
11+
12+
DEFAULT_XAGENT_LABEL_REPO = "Xorbits/xagent-xrouter-labels"
13+
14+
XAGENT_LABEL_FILES = {
15+
"full": "xagent_openrouter_official_candidates_100_4models.public.jsonl",
16+
"train80": "xagent_openrouter_official_candidates_100_4models_train80.public.jsonl",
17+
"holdout20": "xagent_openrouter_official_candidates_100_4models_holdout20.public.jsonl",
18+
"sample": "xagent_openrouter_official_sample.public.jsonl",
19+
}
20+
21+
22+
def load_xagent_openrouter_labels(
23+
source: str | Path = DEFAULT_XAGENT_LABEL_REPO,
24+
*,
25+
split: str = "full",
26+
) -> list[BenchmarkRow]:
27+
"""Load scrubbed xagent routing labels from a local JSONL file or HF dataset.
28+
29+
The public HF dataset currently contains plain JSONL files. Loading through
30+
``huggingface_hub`` keeps the dependency surface small and avoids relying on
31+
a generated datasets schema.
32+
"""
33+
34+
path = Path(source).expanduser()
35+
if path.exists():
36+
return _load_xagent_jsonl(path)
37+
38+
repo_id, resolved_split = _parse_hf_source(str(source), split=split)
39+
filename = XAGENT_LABEL_FILES.get(resolved_split, resolved_split)
40+
downloaded = hf_hub_download(repo_id, filename, repo_type="dataset")
41+
return _load_xagent_jsonl(Path(downloaded))
42+
43+
44+
def _parse_hf_source(source: str, *, split: str) -> tuple[str, str]:
45+
if ":" not in source:
46+
return source, split
47+
48+
repo_id, maybe_split = source.rsplit(":", 1)
49+
if "/" not in maybe_split:
50+
return repo_id, maybe_split
51+
return source, split
52+
53+
54+
def _load_xagent_jsonl(path: Path) -> list[BenchmarkRow]:
55+
output: list[BenchmarkRow] = []
56+
for line_number, record in _iter_jsonl(path):
57+
score = _score(record, line_number=line_number)
58+
prompt = str(record.get("prompt") or "")
59+
model_id = str(record.get("candidate_model") or "")
60+
if not prompt:
61+
raise ValueError(f"Missing prompt in xagent label line {line_number}")
62+
if not model_id:
63+
raise ValueError(f"Missing candidate_model in xagent label line {line_number}")
64+
65+
prompt_sha = record.get("prompt_sha256")
66+
prompt_id = f"xagent:{prompt_sha}" if prompt_sha else _fallback_prompt_id(record)
67+
output.append(
68+
BenchmarkRow(
69+
prompt_id=prompt_id,
70+
prompt=prompt,
71+
model_id=model_id,
72+
score=score,
73+
cost_usd=_usage_cost(record.get("candidate_usage")),
74+
task=f"xagent:{record.get('category') or 'unknown'}",
75+
)
76+
)
77+
if not output:
78+
raise ValueError(f"No xagent labels loaded from {path}")
79+
return output
80+
81+
82+
load_xagent_labels = load_xagent_openrouter_labels
83+
84+
85+
def _iter_jsonl(path: Path) -> Iterable[tuple[int, dict[str, Any]]]:
86+
with path.open("r", encoding="utf-8") as file:
87+
for line_number, line in enumerate(file, start=1):
88+
line = line.strip()
89+
if not line:
90+
continue
91+
try:
92+
value = json.loads(line)
93+
except json.JSONDecodeError as exc:
94+
raise ValueError(f"Invalid xagent JSONL at line {line_number}: {exc}") from exc
95+
if not isinstance(value, dict):
96+
raise ValueError(f"Invalid xagent JSONL at line {line_number}: expected object")
97+
yield line_number, value
98+
99+
100+
def _score(record: dict[str, Any], *, line_number: int) -> float:
101+
judge = record.get("judge")
102+
if not isinstance(judge, dict) or judge.get("score") is None:
103+
raise ValueError(f"Missing judge.score in xagent label line {line_number}")
104+
score = float(judge["score"])
105+
if not 0.0 <= score <= 1.0:
106+
raise ValueError(f"judge.score must be in [0, 1] at line {line_number}: {score}")
107+
return score
108+
109+
110+
def _fallback_prompt_id(record: dict[str, Any]) -> str:
111+
event_id = record.get("event_id", "unknown")
112+
end_event_id = record.get("end_event_id", "unknown")
113+
category = record.get("category", "unknown")
114+
return f"xagent:{event_id}:{end_event_id}:{category}"
115+
116+
117+
def _usage_cost(usage: object) -> float | None:
118+
if not isinstance(usage, dict):
119+
return None
120+
value = usage.get("cost")
121+
if value is None:
122+
return None
123+
return float(value)

tests/test_xagent_labels.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
3+
from xrouter_llm.xagent_labels import load_xagent_openrouter_labels
4+
5+
6+
def test_load_xagent_labels_from_local_jsonl(tmp_path) -> None:
7+
path = tmp_path / "xagent.public.jsonl"
8+
path.write_text(
9+
"\n".join(
10+
json.dumps(record)
11+
for record in [
12+
{
13+
"prompt_sha256": "abc123",
14+
"prompt": "Current user request:\nhello",
15+
"candidate_model": "google/gemini-2.5-flash-lite",
16+
"candidate_usage": {"cost": 0.001},
17+
"category": "skill",
18+
"judge": {"score": 0.75, "can_complete": True},
19+
},
20+
{
21+
"prompt_sha256": "abc123",
22+
"prompt": "Current user request:\nhello",
23+
"candidate_model": "deepseek/deepseek-v4-flash",
24+
"candidate_usage": {"cost": 0.0005},
25+
"category": "skill",
26+
"judge": {"score": 0.25, "can_complete": False},
27+
},
28+
]
29+
),
30+
encoding="utf-8",
31+
)
32+
33+
rows = load_xagent_openrouter_labels(path)
34+
35+
assert len(rows) == 2
36+
assert {row.prompt_id for row in rows} == {"xagent:abc123"}
37+
assert {row.model_id for row in rows} == {
38+
"deepseek/deepseek-v4-flash",
39+
"google/gemini-2.5-flash-lite",
40+
}
41+
assert [row.score for row in rows] == [0.75, 0.25]
42+
assert rows[0].cost_usd == 0.001
43+
assert rows[0].task == "xagent:skill"
44+
45+
46+
def test_load_xagent_labels_rejects_unlabeled_candidates(tmp_path) -> None:
47+
path = tmp_path / "xagent.public.jsonl"
48+
path.write_text(
49+
json.dumps(
50+
{
51+
"prompt_sha256": "abc123",
52+
"prompt": "prompt",
53+
"category": "skill",
54+
}
55+
),
56+
encoding="utf-8",
57+
)
58+
59+
try:
60+
load_xagent_openrouter_labels(path)
61+
except ValueError as exc:
62+
assert "Missing judge.score" in str(exc)
63+
else:
64+
raise AssertionError("expected missing judge.score to fail")

0 commit comments

Comments
 (0)