narcolepticchicken commited on
Commit
efd2832
·
verified ·
1 Parent(s): 153bc0e

Delete router_models/bert_5class/*, router_models/bert_router/*, router_models/baarbundle.pkl, router_models/productionbundle.pkl, router_models/routerbundle*.pkl, router_models/*_success.json, router_models/v6_eval*, training/train_router_v[1-9]*.py, training/train_router_full.py, training/train_bert_5class*.py, training/train_baar_router.py, eval/eval_bert_part*.py, eval/eval_bert_router.py, eval/run_bert_eval*.py, eval/bert_results.json, eval/*v[8-9]*.json, eval/sw*.json

Browse files
Files changed (40) hide show
  1. eval/benchmark_v9_final.json +0 -73
  2. eval/benchmark_v9_results.json +0 -32
  3. eval/bert_results.json +0 -4
  4. eval/eval_bert_part2.py +0 -40
  5. eval/eval_bert_part3.py +0 -140
  6. eval/eval_bert_part4.py +0 -61
  7. eval/eval_bert_partB.py +0 -68
  8. eval/eval_bert_partC.py +0 -95
  9. eval/eval_bert_partD.py +0 -81
  10. eval/eval_bert_router.py +0 -61
  11. eval/run_bert_eval.py +0 -47
  12. eval/run_bert_eval_full.py +0 -66
  13. eval/run_bert_eval_launcher.py +0 -23
  14. eval/swe_bench_results.json +0 -27
  15. eval/swe_v10_results.json +0 -38
  16. eval/v8_results.json +0 -69
  17. router_models/bert_5class/config.json +0 -42
  18. router_models/bert_5class/metadata.json +0 -47
  19. router_models/bert_5class/model.safetensors +0 -3
  20. router_models/bert_5class/tokenizer.json +0 -0
  21. router_models/bert_5class/tokenizer_config.json +0 -15
  22. router_models/bert_router/config.json +0 -28
  23. router_models/bert_router/model.safetensors +0 -3
  24. router_models/bert_router/tokenizer.json +0 -0
  25. router_models/bert_router/tokenizer_config.json +0 -15
  26. router_models/tier_1_success.json +0 -0
  27. router_models/tier_2_success.json +0 -0
  28. router_models/tier_3_success.json +0 -0
  29. router_models/tier_4_success.json +0 -0
  30. router_models/tier_5_success.json +0 -0
  31. router_models/v6_eval_results.json +0 -304
  32. training/train_baar_router.py +0 -470
  33. training/train_bert_5class.py +0 -187
  34. training/train_bert_5class_v2.py +0 -291
  35. training/train_bert_5class_v3.py +0 -292
  36. training/train_router_full.py +0 -433
  37. training/train_router_v1.py +0 -433
  38. training/train_router_v2.py +0 -510
  39. training/train_router_v3.py +0 -262
  40. training/train_router_v4_production.py +0 -241
eval/benchmark_v9_final.json DELETED
@@ -1,73 +0,0 @@
1
- {
2
- "frontier": {
3
- "success": 0.8996666666666666,
4
- "cost": 1.0
5
- },
6
- "v8": {
7
- "success": 0.8373333333333334,
8
- "cost": 0.9151666666666726
9
- },
10
- "v9_e2.0_lc0.05": {
11
- "success": 0.8996666666666666,
12
- "cost": 0.9785500000000054,
13
- "escalated": 238,
14
- "ent_thr": 2.0,
15
- "lc_thr": 0.05
16
- },
17
- "v9_e2.0_lc0.1": {
18
- "success": 0.8996666666666666,
19
- "cost": 0.9785500000000054,
20
- "escalated": 238,
21
- "ent_thr": 2.0,
22
- "lc_thr": 0.1
23
- },
24
- "v9_e2.0_lc0.15": {
25
- "success": 0.8996666666666666,
26
- "cost": 0.9785500000000054,
27
- "escalated": 238,
28
- "ent_thr": 2.0,
29
- "lc_thr": 0.15
30
- },
31
- "v9_e2.5_lc0.05": {
32
- "success": 0.8576666666666667,
33
- "cost": 0.9261833333333388,
34
- "escalated": 94,
35
- "ent_thr": 2.5,
36
- "lc_thr": 0.05
37
- },
38
- "v9_e2.5_lc0.1": {
39
- "success": 0.8576666666666667,
40
- "cost": 0.9261833333333388,
41
- "escalated": 94,
42
- "ent_thr": 2.5,
43
- "lc_thr": 0.1
44
- },
45
- "v9_e2.5_lc0.15": {
46
- "success": 0.8576666666666667,
47
- "cost": 0.9261833333333388,
48
- "escalated": 94,
49
- "ent_thr": 2.5,
50
- "lc_thr": 0.15
51
- },
52
- "v9_e3.0_lc0.05": {
53
- "success": 0.836,
54
- "cost": 0.9132500000000056,
55
- "escalated": 51,
56
- "ent_thr": 3.0,
57
- "lc_thr": 0.05
58
- },
59
- "v9_e3.0_lc0.1": {
60
- "success": 0.834,
61
- "cost": 0.9130833333333389,
62
- "escalated": 49,
63
- "ent_thr": 3.0,
64
- "lc_thr": 0.1
65
- },
66
- "v9_e3.0_lc0.15": {
67
- "success": 0.834,
68
- "cost": 0.9130833333333389,
69
- "escalated": 49,
70
- "ent_thr": 3.0,
71
- "lc_thr": 0.15
72
- }
73
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/benchmark_v9_results.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "always_frontier": {
3
- "success": 0.901,
4
- "avg_cost": 1.0,
5
- "unsafe": 0.0
6
- },
7
- "always_cheap": {
8
- "success": 0.29133333333333333,
9
- "avg_cost": 0.049999999999999954,
10
- "unsafe": 0.7203333333333334
11
- },
12
- "heuristic": {
13
- "success": 0.834,
14
- "avg_cost": 0.8830500000000058,
15
- "unsafe": 0.11466666666666667
16
- },
17
- "v8_router": {
18
- "success": 0.8406666666666667,
19
- "avg_cost": 0.8864666666666725,
20
- "unsafe": 0.0
21
- },
22
- "v9_feedback": {
23
- "success": 0.86,
24
- "avg_cost": 0.9651333333333396,
25
- "unsafe": 0.0
26
- },
27
- "oracle": {
28
- "success": 0.9986666666666667,
29
- "avg_cost": 0.4130333333333279,
30
- "unsafe": 0.0
31
- }
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/bert_results.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "eval_accuracy": 0.9446835987946621,
3
- "eval_f1": 0.971529854879805
4
- }
 
 
 
 
 
eval/eval_bert_part2.py DELETED
@@ -1,40 +0,0 @@
1
-
2
- # ── Part 2: Load data and BERT model ──
3
-
4
- from datasets import load_dataset
5
- from transformers import AutoTokenizer, AutoModelForSequenceClassification
6
- import torch
7
-
8
- print("\n[1] Loading SWE-Router data...")
9
- traces = defaultdict(dict)
10
- for model in MODELS:
11
- try:
12
- ds = load_dataset(f'SWE-Router/swebench-verified-{model}', split='test')
13
- for row in ds:
14
- iid = row['instance_id']
15
- traces[iid][model] = {
16
- 'resolved': row['resolved'],
17
- 'cost': float(row['instance_cost']),
18
- 'problem': row['problem_statement'],
19
- }
20
- print(f" {model}: loaded")
21
- except Exception as e:
22
- print(f" {model}: FAILED - {e}")
23
-
24
- print(f" Total tasks: {len(traces)}")
25
-
26
- print("\n[2] Loading BERT router...")
27
- REPO = "narcolepticchicken/agent-cost-optimizer"
28
- tokenizer = AutoTokenizer.from_pretrained(f"{REPO}", subfolder="router_models/bert_router")
29
- bert_model = AutoModelForSequenceClassification.from_pretrained(f"{REPO}", subfolder="router_models/bert_router")
30
- bert_model.eval()
31
- print(f" BERT model loaded, num_labels={bert_model.config.num_labels}")
32
-
33
- print("\n[3] Loading v11 XGBoost router...")
34
- from huggingface_hub import hf_hub_download
35
- v11_path = hf_hub_download(REPO, "router_models/router_bundle_v11.pkl")
36
- v11_bundle = pickle.load(open(v11_path, "rb"))
37
- v11_tier_clfs = {int(k):v for k,v in v11_bundle["tier_clfs"].items()}
38
- v11_tier_calibs = {int(k):v for k,v in v11_bundle["tier_calibrators"].items()}
39
- v11_feat_keys = v11_bundle["feat_keys"]
40
- print(f" v11 loaded, features={len(v11_feat_keys)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_part3.py DELETED
@@ -1,140 +0,0 @@
1
-
2
- # ── Part 3: Run evaluations ──
3
-
4
- def route_bert(problem_text):
5
- """Route using BERT classifier."""
6
- inputs = tokenizer(problem_text, truncation=True, max_length=512, return_tensors="pt")
7
- with torch.no_grad():
8
- logits = bert_model(**inputs).logits
9
- pred_class = torch.argmax(logits, dim=-1).item()
10
- tier = pred_class + 1 # classes are 0-4 → tiers 1-5
11
- probs = torch.softmax(logits, dim=-1)[0]
12
- confidence = float(probs[pred_class])
13
- return tier, confidence
14
-
15
- def route_v11(problem_text):
16
- """Route using v11 XGBoost + isotonic calibration."""
17
- feats = extract_features(problem_text)
18
- feat_vec = np.array([float(feats.get(k, 0.0)) for k in v11_feat_keys], dtype=np.float32).reshape(1,-1)
19
- tier_probs = {}
20
- for t in range(1, 6):
21
- p_raw = v11_tier_clfs[t].predict_proba(feat_vec)[0, 1]
22
- p_cal = float(v11_tier_calibs[t].transform([p_raw])[0])
23
- tier_probs[t] = p_cal
24
- # Find cheapest tier with P(success) >= 0.65
25
- for t in range(1, 6):
26
- if tier_probs[t] >= 0.65:
27
- return t, tier_probs[t], tier_probs
28
- return 5, tier_probs[5], tier_probs
29
-
30
- TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,
31
- "legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
32
-
33
- def classify_task(text):
34
- r = text.lower()
35
- if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
36
- if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
37
- if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
38
- if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
39
- if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
40
- if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
41
- if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
42
- return "unknown_ambiguous"
43
-
44
- policies = defaultdict(lambda: {"success":0,"cost":0.0,"n":0})
45
-
46
- print("\n[4] Evaluating all policies on SWE-Router...")
47
- for iid, model_results in traces.items():
48
- problem = next(iter(model_results.values()))['problem']
49
- task_type = classify_task(problem)
50
- floor = TASK_FLOOR.get(task_type, 2)
51
-
52
- # Oracle
53
- resolved = [(m, r) for m, r in model_results.items() if r['resolved']]
54
- if resolved:
55
- cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
56
- policies['oracle']['success'] += 1
57
- policies['oracle']['cost'] += cheapest[1]['cost']
58
- else:
59
- policies['oracle']['cost'] += min(r['cost'] for r in model_results.values())
60
- policies['oracle']['n'] += 1
61
-
62
- # Always frontier
63
- f_model = 'claude-opus-4.7'
64
- if f_model in model_results:
65
- policies['frontier']['success'] += int(model_results[f_model]['resolved'])
66
- policies['frontier']['cost'] += model_results[f_model]['cost']
67
- policies['frontier']['n'] += 1
68
-
69
- # BERT router
70
- bert_tier, bert_conf = route_bert(problem)
71
- bert_tier = max(bert_tier, floor) # enforce safety floor
72
- m_bert = TIER_TO_SWE.get(bert_tier, 'claude-opus-4.7')
73
- if m_bert in model_results:
74
- policies['bert']['success'] += int(model_results[m_bert]['resolved'])
75
- policies['bert']['cost'] += model_results[m_bert]['cost']
76
- else:
77
- policies['bert']['success'] += int(model_results.get('claude-opus-4.7',{}).get('resolved',0))
78
- policies['bert']['cost'] += model_results.get('claude-opus-4.7',{}).get('cost',0.3)
79
- policies['bert']['n'] += 1
80
-
81
- # v11 XGBoost
82
- v11_tier, v11_conf, v11_probs = route_v11(problem)
83
- v11_tier = max(v11_tier, floor) # enforce safety floor
84
- m_v11 = TIER_TO_SWE.get(v11_tier, 'claude-opus-4.7')
85
- if m_v11 in model_results:
86
- policies['v11_xgboost']['success'] += int(model_results[m_v11]['resolved'])
87
- policies['v11_xgboost']['cost'] += model_results[m_v11]['cost']
88
- else:
89
- policies['v11_xgboost']['success'] += int(model_results.get('claude-opus-4.7',{}).get('resolved',0))
90
- policies['v11_xgboost']['cost'] += model_results.get('claude-opus-4.7',{}).get('cost',0.3)
91
- policies['v11_xgboost']['n'] += 1
92
-
93
- # BERT + feedback (escalate on failure)
94
- if m_bert in model_results and model_results[m_bert]['resolved']:
95
- policies['bert_feedback']['success'] += 1
96
- policies['bert_feedback']['cost'] += model_results[m_bert]['cost']
97
- else:
98
- # Escalate one tier
99
- up_tier = min(bert_tier + 1, 5)
100
- m_up = TIER_TO_SWE.get(up_tier, 'claude-opus-4.7')
101
- if m_up in model_results and model_results[m_up]['resolved']:
102
- policies['bert_feedback']['success'] += 1
103
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
104
- policies['bert_feedback']['cost'] += model_results[m_up]['cost']
105
- else:
106
- # Last resort: frontier
107
- if f_model in model_results and model_results[f_model]['resolved']:
108
- policies['bert_feedback']['success'] += 1
109
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
110
- policies['bert_feedback']['cost'] += model_results[f_model]['cost']
111
- else:
112
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
113
- policies['bert_feedback']['n'] += 1
114
-
115
- # v11 + feedback (escalate on failure)
116
- if m_v11 in model_results and model_results[m_v11]['resolved']:
117
- policies['v11_feedback']['success'] += 1
118
- policies['v11_feedback']['cost'] += model_results[m_v11]['cost']
119
- else:
120
- up_tier = min(v11_tier + 1, 5)
121
- m_up = TIER_TO_SWE.get(up_tier, 'claude-opus-4.7')
122
- if m_up in model_results and model_results[m_up]['resolved']:
123
- policies['v11_feedback']['success'] += 1
124
- policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
125
- policies['v11_feedback']['cost'] += model_results[m_up]['cost']
126
- else:
127
- if f_model in model_results and model_results[f_model]['resolved']:
128
- policies['v11_feedback']['success'] += 1
129
- policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
130
- policies['v11_feedback']['cost'] += model_results[f_model]['cost']
131
- else:
132
- policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
133
- policies['v11_feedback']['n'] += 1
134
-
135
- # Always cheap
136
- c_model = 'deepseek-v4-flash'
137
- if c_model in model_results:
138
- policies['always_cheap']['success'] += int(model_results[c_model]['resolved'])
139
- policies['always_cheap']['cost'] += model_results[c_model]['cost']
140
- policies['always_cheap']['n'] += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_part4.py DELETED
@@ -1,61 +0,0 @@
1
-
2
- # ── Part 4: Print results ──
3
-
4
- print(f"\n\n{'='*70}")
5
- print("BERT vs XGBoost ROUTER COMPARISON ON SWE-BENCH")
6
- print(f"{'='*70}")
7
-
8
- fr = policies['frontier']
9
- fr_cost = fr['cost'] / fr['n']
10
- fr_succ = fr['success'] / fr['n']
11
-
12
- print(f"\n{'Policy':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
13
- print("-"*52)
14
- order = ['oracle','bert_feedback','v11_feedback','bert','v11_xgboost','frontier','always_cheap']
15
- for name in order:
16
- if name not in policies:
17
- continue
18
- r = policies[name]
19
- sr = r['success']/r['n'] if r['n'] > 0 else 0
20
- ac = r['cost']/r['n'] if r['n'] > 0 else 0
21
- cr = (1 - ac/fr_cost)*100 if fr_cost > 0 else 0
22
- print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
23
-
24
- # BERT tier distribution
25
- print(f"\n\nBERT tier distribution:")
26
- bert_tiers = defaultdict(int)
27
- for iid, model_results in traces.items():
28
- problem = next(iter(model_results.values()))['problem']
29
- t, c = route_bert(problem)
30
- bert_tiers[t] += 1
31
- for t in sorted(bert_tiers):
32
- print(f" Tier {t}: {bert_tiers[t]}")
33
-
34
- # Quality gap analysis
35
- print(f"\n\nQuality gap vs frontier:")
36
- for name in ['bert','bert_feedback','v11_xgboost','v11_feedback']:
37
- r = policies[name]
38
- sr = r['success']/r['n'] if r['n'] > 0 else 0
39
- gap = (sr - fr_succ) * 100
40
- print(f" {name}: {gap:+.1f}pp vs frontier")
41
-
42
- # Save results
43
- results = {}
44
- for name, r in policies.items():
45
- sr = r['success']/r['n'] if r['n'] > 0 else 0
46
- ac = r['cost']/r['n'] if r['n'] > 0 else 0
47
- cr = (1 - ac/fr_cost)*100 if fr_cost > 0 else 0
48
- results[name] = {"success": round(sr, 4), "avg_cost": round(ac, 4), "costRed": round(cr, 1)}
49
-
50
- # Upload results to Hub
51
- from huggingface_hub import HfApi
52
- api = HfApi()
53
- import tempfile, json
54
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
55
- json.dump(results, f, indent=2)
56
- api.upload_file(path_or_fileobj=f.name, path_in_repo="eval/bert_vs_xgboost_results.json",
57
- repo_id="narcolepticchicken/agent-cost-optimizer", repo_type="model")
58
- os.unlink(f.name)
59
-
60
- print(f"\nResults saved to eval/bert_vs_xgboost_results.json")
61
- print("DONE!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_partB.py DELETED
@@ -1,68 +0,0 @@
1
-
2
- # ── Load models ──
3
- print("\n[2] Loading BERT router from Hub...")
4
- from transformers import AutoTokenizer, AutoModelForSequenceClassification
5
- import torch
6
-
7
- REPO = "narcolepticchicken/agent-cost-optimizer"
8
- tokenizer = AutoTokenizer.from_pretrained(REPO, subfolder="router_models/bert_router")
9
- bert_model = AutoModelForSequenceClassification.from_pretrained(REPO, subfolder="router_models/bert_router")
10
- bert_model.eval()
11
- print(f" BERT loaded, num_labels={bert_model.config.num_labels}")
12
-
13
- # BERT is binary (success/fail) — we'll use it as a per-tier success predictor
14
- # by prepending "Tier X:" to the input text
15
-
16
- print("\n[3] Loading v10 XGBoost router...")
17
- from huggingface_hub import hf_hub_download
18
- import pickle
19
- v10_path = hf_hub_download(REPO, "router_models/router_bundle_v10_fixed.pkl")
20
- v10_bundle = pickle.load(open(v10_path, "rb"))
21
- print(f" v10 bundle keys: {list(v10_bundle.keys())}")
22
-
23
- # v10 may have different structure — inspect
24
- if "tier_clfs" in v10_bundle:
25
- v10_tier_clfs = {int(k):v for k,v in v10_bundle["tier_clfs"].items()}
26
- v10_tier_calibs = {int(k):v for k,v in v10_bundle["tier_calibrators"].items()}
27
- v10_feat_keys = v10_bundle["feat_keys"]
28
- print(f" v10 loaded, features={len(v10_feat_keys)}")
29
- HAS_V10 = True
30
- else:
31
- HAS_V10 = False
32
- print(f" v10 bundle structure: {type(v10_bundle)}")
33
-
34
- # ── Routing functions ──
35
- def route_bert(problem_text):
36
- """BERT binary classifier: predict success probability at each tier.
37
- Route to cheapest tier where P(success) > 0.5."""
38
- tier_probs = {}
39
- for tier in range(1, 6):
40
- prompt = f"[Tier {tier}] {problem_text}"
41
- inputs = tokenizer(prompt, truncation=True, max_length=512, return_tensors="pt")
42
- with torch.no_grad():
43
- logits = bert_model(**inputs).logits
44
- probs = torch.softmax(logits, dim=-1)[0]
45
- # Binary: class 1 = success
46
- tier_probs[tier] = float(probs[1]) if bert_model.config.num_labels == 2 else float(probs[tier-1])
47
-
48
- # Route to cheapest tier with P(success) >= 0.5
49
- for t in range(1, 6):
50
- if tier_probs[t] >= 0.5:
51
- return t, tier_probs[t], tier_probs
52
- return 5, tier_probs[5], tier_probs
53
-
54
- def route_v10(problem_text):
55
- """v10 XGBoost cascade router."""
56
- if not HAS_V10:
57
- return 4, 0.5, {t:0.5 for t in range(1,6)}
58
- feats = extract_features(problem_text)
59
- feat_vec = np.array([float(feats.get(k, 0.0)) for k in v10_feat_keys], dtype=np.float32).reshape(1,-1)
60
- tier_probs = {}
61
- for t in range(1, 6):
62
- p_raw = v10_tier_clfs[t].predict_proba(feat_vec)[0, 1]
63
- p_cal = float(v10_tier_calibs[t].transform([p_raw])[0])
64
- tier_probs[t] = p_cal
65
- for t in range(1, 6):
66
- if tier_probs[t] >= 0.65:
67
- return t, tier_probs[t], tier_probs
68
- return 5, tier_probs[5], tier_probs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_partC.py DELETED
@@ -1,95 +0,0 @@
1
-
2
- # ── Evaluate ──
3
- policies = defaultdict(lambda: {"success":0,"cost":0.0,"n":0})
4
-
5
- print("\n[4] Evaluating all policies...")
6
- for idx, (iid, model_results) in enumerate(traces.items()):
7
- if idx % 100 == 0:
8
- print(f" Progress: {idx}/{len(traces)}")
9
- problem = next(iter(model_results.values()))['problem']
10
- task_type = classify_task(problem)
11
- floor = TASK_FLOOR.get(task_type, 2)
12
- f_model = 'claude-opus-4.7'
13
-
14
- # Oracle
15
- resolved = [(m, r) for m, r in model_results.items() if r['resolved']]
16
- if resolved:
17
- cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
18
- policies['oracle']['success'] += 1
19
- policies['oracle']['cost'] += cheapest[1]['cost']
20
- else:
21
- policies['oracle']['cost'] += min(r['cost'] for r in model_results.values())
22
- policies['oracle']['n'] += 1
23
-
24
- # Always frontier
25
- if f_model in model_results:
26
- policies['frontier']['success'] += int(model_results[f_model]['resolved'])
27
- policies['frontier']['cost'] += model_results[f_model]['cost']
28
- policies['frontier']['n'] += 1
29
-
30
- # BERT (per-tier success prediction with cascade)
31
- bert_tier, bert_conf, bert_probs = route_bert(problem)
32
- bert_tier = max(bert_tier, floor)
33
- m_bert = TIER_TO_SWE.get(bert_tier, f_model)
34
- if m_bert in model_results:
35
- policies['bert']['success'] += int(model_results[m_bert]['resolved'])
36
- policies['bert']['cost'] += model_results[m_bert]['cost']
37
- else:
38
- policies['bert']['success'] += int(model_results.get(f_model,{}).get('resolved',0))
39
- policies['bert']['cost'] += model_results.get(f_model,{}).get('cost',0.3)
40
- policies['bert']['n'] += 1
41
-
42
- # v10 XGBoost
43
- v10_tier, v10_conf, v10_probs = route_v10(problem)
44
- v10_tier = max(v10_tier, floor)
45
- m_v10 = TIER_TO_SWE.get(v10_tier, f_model)
46
- if m_v10 in model_results:
47
- policies['v10_xgboost']['success'] += int(model_results[m_v10]['resolved'])
48
- policies['v10_xgboost']['cost'] += model_results[m_v10]['cost']
49
- else:
50
- policies['v10_xgboost']['success'] += int(model_results.get(f_model,{}).get('resolved',0))
51
- policies['v10_xgboost']['cost'] += model_results.get(f_model,{}).get('cost',0.3)
52
- policies['v10_xgboost']['n'] += 1
53
-
54
- # BERT + feedback (escalate on failure)
55
- if m_bert in model_results and model_results[m_bert]['resolved']:
56
- policies['bert_feedback']['success'] += 1
57
- policies['bert_feedback']['cost'] += model_results[m_bert]['cost']
58
- else:
59
- up_tier = min(bert_tier + 1, 5)
60
- m_up = TIER_TO_SWE.get(up_tier, f_model)
61
- if m_up in model_results and model_results[m_up]['resolved']:
62
- policies['bert_feedback']['success'] += 1
63
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01) + model_results[m_up]['cost']
64
- elif f_model in model_results and model_results[f_model]['resolved']:
65
- policies['bert_feedback']['success'] += 1
66
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01) + model_results[f_model]['cost']
67
- else:
68
- policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
69
- policies['bert_feedback']['n'] += 1
70
-
71
- # v10 + feedback
72
- if m_v10 in model_results and model_results[m_v10]['resolved']:
73
- policies['v10_feedback']['success'] += 1
74
- policies['v10_feedback']['cost'] += model_results[m_v10]['cost']
75
- else:
76
- up_tier = min(v10_tier + 1, 5)
77
- m_up = TIER_TO_SWE.get(up_tier, f_model)
78
- if m_up in model_results and model_results[m_up]['resolved']:
79
- policies['v10_feedback']['success'] += 1
80
- policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01) + model_results[m_up]['cost']
81
- elif f_model in model_results and model_results[f_model]['resolved']:
82
- policies['v10_feedback']['success'] += 1
83
- policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01) + model_results[f_model]['cost']
84
- else:
85
- policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01)
86
- policies['v10_feedback']['n'] += 1
87
-
88
- # Always cheap
89
- c_model = 'deepseek-v4-flash'
90
- if c_model in model_results:
91
- policies['always_cheap']['success'] += int(model_results[c_model]['resolved'])
92
- policies['always_cheap']['cost'] += model_results[c_model]['cost']
93
- policies['always_cheap']['n'] += 1
94
-
95
- print(f" Progress: {len(traces)}/{len(traces)} - DONE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_partD.py DELETED
@@ -1,81 +0,0 @@
1
-
2
- # ── Results ──
3
- fr = policies['frontier']
4
- fr_cost = fr['cost'] / fr['n']
5
- fr_succ = fr['success'] / fr['n']
6
-
7
- print(f"\n\n{'='*70}")
8
- print("BERT vs XGBoost ROUTER COMPARISON ON SWE-BENCH")
9
- print(f"{'='*70}")
10
- print(f"\n{'Policy':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
11
- print("-"*52)
12
- for name in ['oracle','bert_feedback','v11_feedback','bert','v11_xgboost','frontier','always_cheap']:
13
- # Map v11 names to v10 if v11 not available
14
- actual_name = name
15
- if name == 'v11_xgboost' and 'v11_xgboost' not in policies and 'v10_xgboost' in policies:
16
- actual_name = 'v10_xgboost'
17
- if name == 'v11_feedback' and 'v11_feedback' not in policies and 'v10_feedback' in policies:
18
- actual_name = 'v10_feedback'
19
- if actual_name not in policies:
20
- continue
21
- r = policies[actual_name]
22
- sr = r['success']/r['n'] if r['n'] > 0 else 0
23
- ac = r['cost']/r['n'] if r['n'] > 0 else 0
24
- cr = (1-ac/fr_cost)*100 if fr_cost > 0 else 0
25
- print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
26
-
27
- print(f"\nQuality gap vs frontier:")
28
- for name in ['bert','bert_feedback','v11_xgboost','v11_feedback']:
29
- actual_name = name
30
- if name == 'v11_xgboost' and 'v11_xgboost' not in policies and 'v10_xgboost' in policies:
31
- actual_name = 'v10_xgboost'
32
- if name == 'v11_feedback' and 'v11_feedback' not in policies and 'v10_feedback' in policies:
33
- actual_name = 'v10_feedback'
34
- if actual_name not in policies:
35
- continue
36
- r = policies[actual_name]
37
- sr = r['success']/r['n'] if r['n'] > 0 else 0
38
- gap = (sr - fr_succ) * 100
39
- print(f" {name}: {gap:+.1f}pp vs frontier")
40
-
41
- # BERT tier distribution
42
- print(f"\nBERT tier distribution (first 100 tasks):")
43
- bert_tiers = defaultdict(int)
44
- bert_probs_dist = defaultdict(list)
45
- for iid, model_results in list(traces.items())[:100]:
46
- problem = next(iter(model_results.values()))['problem']
47
- t, conf, probs = route_bert(problem)
48
- bert_tiers[t] += 1
49
- for tier, p in probs.items():
50
- bert_probs_dist[tier].append(p)
51
- print(f" Tier routing counts:")
52
- for t in sorted(bert_tiers):
53
- print(f" Tier {t}: {bert_tiers[t]}")
54
- print(f" Per-tier P(success) stats:")
55
- for t in sorted(bert_probs_dist):
56
- ps = bert_probs_dist[t]
57
- print(f" Tier {t}: mean={np.mean(ps):.3f}, std={np.std(ps):.3f}, min={np.min(ps):.3f}, max={np.max(ps):.3f}")
58
-
59
- # Save results to local file and try to upload to Hub
60
- results = {}
61
- for name, r in policies.items():
62
- sr = r['success']/r['n'] if r['n'] > 0 else 0
63
- ac = r['cost']/r['n'] if r['n'] > 0 else 0
64
- cr = (1-ac/fr_cost)*100 if fr_cost > 0 else 0
65
- results[name] = {"success": round(sr, 4), "avg_cost": round(ac, 4), "costRed": round(cr, 1)}
66
-
67
- import tempfile, json
68
- try:
69
- from huggingface_hub import HfApi
70
- api = HfApi()
71
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
72
- json.dump(results, f, indent=2)
73
- api.upload_file(path_or_fileobj=f.name, path_in_repo="eval/bert_vs_xgboost_results.json",
74
- repo_id="narcolepticchicken/agent-cost-optimizer", repo_type="model")
75
- os.unlink(f.name)
76
- print(f"\nResults uploaded to eval/bert_vs_xgboost_results.json on Hub")
77
- except Exception as e:
78
- print(f"\nCould not upload to Hub: {e}")
79
- print(f"Results JSON:\n{json.dumps(results, indent=2)}")
80
-
81
- print("\nDONE!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/eval_bert_router.py DELETED
@@ -1,61 +0,0 @@
1
- """Evaluate BERT router on SWE-Router and compare with v11 XGBoost."""
2
- import json, os, sys, random, pickle, math
3
- import numpy as np
4
- from collections import defaultdict
5
-
6
- # ── Constants ──
7
- MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
8
- 'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
9
-
10
- MODEL_TIER = {
11
- 'deepseek-v4-flash': 1, 'gpt-5-nano': 1,
12
- 'gpt-5-mini': 2, 'deepseek-v3.2': 2,
13
- 'gemini-2.5-pro': 3,
14
- 'claude-opus-4.7': 4, 'gpt-5.2': 4,
15
- 'gemini-3-pro': 5,
16
- }
17
- TIER_COST = {1:0.01, 2:0.05, 3:0.15, 4:0.30, 5:0.50}
18
- TIER_TO_SWE = {
19
- 1: 'deepseek-v4-flash', 2: 'gpt-5-mini',
20
- 3: 'gemini-2.5-pro', 4: 'claude-opus-4.7', 5: 'gemini-3-pro',
21
- }
22
-
23
- # ── Feature extraction (same as training) ──
24
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
25
- "compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
26
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
27
- RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
28
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
29
- CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
30
- SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
31
- LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
32
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
33
-
34
- def extract_features(problem_text):
35
- r = problem_text.lower()
36
- feats = {
37
- 'req_len': len(problem_text), 'num_words': len(problem_text.split()),
38
- 'has_code': int(any(k in r for k in CODE_KW)),
39
- 'n_code': sum(1 for k in CODE_KW if k in r),
40
- 'has_legal': int(any(k in r for k in LEGAL_KW)),
41
- 'has_research': int(any(k in r for k in RESEARCH_KW)),
42
- 'has_tool': int(any(k in r for k in TOOL_KW)),
43
- 'has_critical': int(any(k in r for k in CRITICAL_KW)),
44
- 'has_simple': int(any(k in r for k in SIMPLE_KW)),
45
- 'has_long': int(any(k in r for k in LONG_KW)),
46
- 'has_math': int(any(k in r for k in MATH_KW)),
47
- 'has_error_msg': int('error' in r or 'traceback' in r or 'exception' in r),
48
- 'has_file_path': int('/' in r),
49
- 'n_lines': problem_text.count('\n') + 1,
50
- 'has_version': int('version' in r or 'update' in r or 'upgrade' in r),
51
- 'has_add': int('add' in r or 'new' in r or 'create' in r),
52
- 'has_fix': int('fix' in r or 'bug' in r or 'issue' in r or 'broken' in r),
53
- 'has_change': int('change' in r or 'modify' in r or 'update' in r),
54
- 'has_remove': int('remove' in r or 'delete' in r or 'drop' in r),
55
- 'has_test': int('test' in r or 'spec' in r or 'assert' in r),
56
- 'has_doc': int('doc' in r or 'readme' in r or 'comment' in r),
57
- }
58
- return feats
59
-
60
- print("BERT Router Evaluation on SWE-Router")
61
- print("="*60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/run_bert_eval.py DELETED
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Evaluate BERT router on SWE-Router and compare with v11 XGBoost.
3
- Combines eval_bert_router.py + eval_bert_part2.py + eval_bert_part3.py + eval_bert_part4.py
4
- """
5
- import json, os, sys, random, pickle, math
6
- import numpy as np
7
- from collections import defaultdict
8
-
9
- MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
10
- 'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
11
- MODEL_TIER = {
12
- 'deepseek-v4-flash': 1, 'gpt-5-nano': 1,
13
- 'gpt-5-mini': 2, 'deepseek-v3.2': 2,
14
- 'gemini-2.5-pro': 3,
15
- 'claude-opus-4.7': 4, 'gpt-5.2': 4,
16
- 'gemini-3-pro': 5,
17
- }
18
- TIER_COST = {1:0.01, 2:0.05, 3:0.15, 4:0.30, 5:0.50}
19
- TIER_TO_SWE = {1:'deepseek-v4-flash',2:'gpt-5-mini',3:'gemini-2.5-pro',4:'claude-opus-4.7',5:'gemini-3-pro'}
20
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test","compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
21
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
22
- RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
23
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
24
- CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
25
- SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
26
- LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
27
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
28
-
29
- def extract_features(pt):
30
- r = pt.lower()
31
- return {'req_len':len(pt),'num_words':len(pt.split()),'has_code':int(any(k in r for k in CODE_KW)),'n_code':sum(1 for k in CODE_KW if k in r),'has_legal':int(any(k in r for k in LEGAL_KW)),'has_research':int(any(k in r for k in RESEARCH_KW)),'has_tool':int(any(k in r for k in TOOL_KW)),'has_critical':int(any(k in r for k in CRITICAL_KW)),'has_simple':int(any(k in r for k in SIMPLE_KW)),'has_long':int(any(k in r for k in LONG_KW)),'has_math':int(any(k in r for k in MATH_KW)),'has_error_msg':int('error' in r or 'traceback' in r),'has_file_path':int('/' in r),'n_lines':pt.count('\n')+1,'has_version':int('version' in r or 'update' in r),'has_add':int('add' in r or 'new' in r),'has_fix':int('fix' in r or 'bug' in r),'has_change':int('change' in r or 'modify' in r),'has_remove':int('remove' in r),'has_test':int('test' in r or 'spec' in r),'has_doc':int('doc' in r)}
32
-
33
- TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,"legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
34
-
35
- def classify_task(text):
36
- r = text.lower()
37
- if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
38
- if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
39
- if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
40
- if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
41
- if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
42
- if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
43
- if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
44
- return "unknown_ambiguous"
45
-
46
- print("BERT Router Evaluation on SWE-Router")
47
- print("="*60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/run_bert_eval_full.py DELETED
@@ -1,66 +0,0 @@
1
- """Evaluate BERT router on SWE-Router and compare with v11 XGBoost."""
2
- import json, os, sys, random, pickle, math
3
- import numpy as np
4
- from collections import defaultdict
5
-
6
- MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
7
- 'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
8
- MODEL_TIER = {'deepseek-v4-flash':1,'gpt-5-nano':1,'gpt-5-mini':2,'deepseek-v3.2':2,
9
- 'gemini-2.5-pro':3,'claude-opus-4.7':4,'gpt-5.2':4,'gemini-3-pro':5}
10
- TIER_COST = {1:0.01,2:0.05,3:0.15,4:0.30,5:0.50}
11
- TIER_TO_SWE = {1:'deepseek-v4-flash',2:'gpt-5-mini',3:'gemini-2.5-pro',4:'claude-opus-4.7',5:'gemini-3-pro'}
12
- TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,
13
- "legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
14
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
15
- "compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
16
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
17
- RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
18
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
19
- CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
20
- SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
21
- LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
22
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
23
-
24
- def extract_features(pt):
25
- r = pt.lower()
26
- return {'req_len':len(pt),'num_words':len(pt.split()),
27
- 'has_code':int(any(k in r for k in CODE_KW)),'n_code':sum(1 for k in CODE_KW if k in r),
28
- 'has_legal':int(any(k in r for k in LEGAL_KW)),'has_research':int(any(k in r for k in RESEARCH_KW)),
29
- 'has_tool':int(any(k in r for k in TOOL_KW)),'has_critical':int(any(k in r for k in CRITICAL_KW)),
30
- 'has_simple':int(any(k in r for k in SIMPLE_KW)),'has_long':int(any(k in r for k in LONG_KW)),
31
- 'has_math':int(any(k in r for k in MATH_KW)),'has_error_msg':int('error' in r or 'traceback' in r),
32
- 'has_file_path':int('/' in r),'n_lines':pt.count('\n')+1,
33
- 'has_version':int('version' in r or 'update' in r),'has_add':int('add' in r or 'new' in r),
34
- 'has_fix':int('fix' in r or 'bug' in r),'has_change':int('change' in r or 'modify' in r),
35
- 'has_remove':int('remove' in r),'has_test':int('test' in r or 'spec' in r),
36
- 'has_doc':int('doc' in r or 'readme' in r)}
37
-
38
- def classify_task(text):
39
- r = text.lower()
40
- if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
41
- if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
42
- if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
43
- if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
44
- if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
45
- if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
46
- if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
47
- return "unknown_ambiguous"
48
-
49
- print("BERT Router Evaluation on SWE-Router")
50
- print("="*60)
51
-
52
- # ── Load SWE-Router data ──
53
- print("\n[1] Loading SWE-Router data...")
54
- from datasets import load_dataset
55
- traces = defaultdict(dict)
56
- for model in MODELS:
57
- try:
58
- ds = load_dataset(f'SWE-Router/swebench-verified-{model}', split='test')
59
- for row in ds:
60
- iid = row['instance_id']
61
- traces[iid][model] = {'resolved': row['resolved'], 'cost': float(row['instance_cost']),
62
- 'problem': row['problem_statement']}
63
- print(f" {model}: loaded")
64
- except Exception as e:
65
- print(f" {model}: FAILED - {e}")
66
- print(f" Total tasks: {len(traces)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/run_bert_eval_launcher.py DELETED
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Download eval script parts from Hub, combine, and run."""
3
- import os, subprocess
4
- from huggingface_hub import hf_hub_download
5
-
6
- REPO = "narcolepticchicken/agent-cost-optimizer"
7
- parts = ["eval/run_bert_eval_full.py", "eval/eval_bert_partB.py", "eval/eval_bert_partC.py", "eval/eval_bert_partD.py"]
8
-
9
- combined = "/app/eval_bert_combined.py"
10
- with open(combined, "w") as out:
11
- for part in parts:
12
- path = hf_hub_download(REPO, part)
13
- with open(path) as f:
14
- out.write(f.read())
15
- out.write("\n\n")
16
-
17
- print(f"Combined script: {combined}")
18
- print(f"Size: {os.path.getsize(combined)} bytes")
19
-
20
- # Run it
21
- import subprocess, sys
22
- result = subprocess.run([sys.executable, combined], capture_output=False)
23
- sys.exit(result.returncode)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/swe_bench_results.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "always_frontier": {
3
- "success": 0.782,
4
- "avg_cost": 0.3166872804999999,
5
- "n": 500
6
- },
7
- "always_cheap": {
8
- "success": 0.632,
9
- "avg_cost": 0.014239461958399993,
10
- "n": 500
11
- },
12
- "aco_v8": {
13
- "success": 0.756,
14
- "avg_cost": 0.29145885426799994,
15
- "n": 500
16
- },
17
- "oracle": {
18
- "success": 0.87,
19
- "avg_cost": 0.05456037742200003,
20
- "n": 500
21
- },
22
- "aco_v9_feedback": {
23
- "success": 0.826,
24
- "avg_cost": 0.4847524783679998,
25
- "n": 500
26
- }
27
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/swe_v10_results.json DELETED
@@ -1,38 +0,0 @@
1
- {
2
- "v10_direct": {
3
- "success": 0.766,
4
- "avg_cost": 0.1878,
5
- "costRed": 40.7
6
- },
7
- "v10_cascade": {
8
- "success": 0.756,
9
- "avg_cost": 0.1767,
10
- "costRed": 44.2
11
- },
12
- "v10_feedback": {
13
- "success": 0.848,
14
- "avg_cost": 0.2014,
15
- "costRed": 36.4
16
- },
17
- "v8_synthetic": {
18
- "success": 0.658,
19
- "avg_cost": 0.3534,
20
- "costRed": -11.6
21
- },
22
- "frontier": {
23
- "success": 0.782,
24
- "avg_cost": 0.3167,
25
- "costRed": 0.0
26
- },
27
- "oracle": {
28
- "success": 0.87,
29
- "avg_cost": 0.0624,
30
- "costRed": 80.3
31
- },
32
- "always_cheap": {
33
- "success": 0.632,
34
- "avg_cost": 0.0142,
35
- "costRed": 95.5
36
- },
37
- "key_finding": "v10 trained on REAL SWE-Router data achieves 36-44% cost reduction vs 8% for synthetic-trained v8. v10_feedback achieves HIGHER success than always-frontier (84.8% vs 78.2%) at 36.4% cost reduction."
38
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eval/v8_results.json DELETED
@@ -1,69 +0,0 @@
1
- {
2
- "always_frontier": {
3
- "success": 0.8935,
4
- "avg_cost": 1.0,
5
- "unsafe_rate": 0.026,
6
- "false_done": 0.0805,
7
- "tier_dist": {
8
- "4": 2000
9
- }
10
- },
11
- "always_cheap": {
12
- "success": 0.238,
13
- "avg_cost": 0.04999999999999823,
14
- "unsafe_rate": 0.762,
15
- "false_done": 0.0,
16
- "tier_dist": {
17
- "1": 2000
18
- }
19
- },
20
- "heuristic_static": {
21
- "success": 0.825,
22
- "avg_cost": 0.929275000000004,
23
- "unsafe_rate": 0.058,
24
- "false_done": 0.117,
25
- "tier_dist": {
26
- "4": 674,
27
- "3": 646,
28
- "5": 443,
29
- "2": 237
30
- }
31
- },
32
- "oracle": {
33
- "success": 1.0,
34
- "avg_cost": 0.46504999999999164,
35
- "unsafe_rate": 0.0,
36
- "false_done": 0.0,
37
- "tier_dist": {
38
- "3": 647,
39
- "1": 476,
40
- "2": 567,
41
- "4": 258,
42
- "5": 52
43
- }
44
- },
45
- "v8_dynamic+ML": {
46
- "success": 0.82,
47
- "avg_cost": 0.922775000000004,
48
- "unsafe_rate": 0.0615,
49
- "false_done": 0.1185,
50
- "tier_dist": {
51
- "3": 754,
52
- "4": 538,
53
- "5": 471,
54
- "2": 237
55
- }
56
- },
57
- "v8_dynamic_only": {
58
- "success": 0.82,
59
- "avg_cost": 0.922775000000004,
60
- "unsafe_rate": 0.0615,
61
- "false_done": 0.1185,
62
- "tier_dist": {
63
- "3": 754,
64
- "4": 538,
65
- "5": 471,
66
- "2": 237
67
- }
68
- }
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/bert_5class/config.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "activation": "gelu",
3
- "architectures": [
4
- "DistilBertForSequenceClassification"
5
- ],
6
- "attention_dropout": 0.1,
7
- "bos_token_id": null,
8
- "dim": 768,
9
- "dropout": 0.1,
10
- "dtype": "float32",
11
- "eos_token_id": null,
12
- "hidden_dim": 3072,
13
- "id2label": {
14
- "0": "LABEL_0",
15
- "1": "LABEL_1",
16
- "2": "LABEL_2",
17
- "3": "LABEL_3",
18
- "4": "LABEL_4"
19
- },
20
- "initializer_range": 0.02,
21
- "label2id": {
22
- "LABEL_0": 0,
23
- "LABEL_1": 1,
24
- "LABEL_2": 2,
25
- "LABEL_3": 3,
26
- "LABEL_4": 4
27
- },
28
- "max_position_embeddings": 512,
29
- "model_type": "distilbert",
30
- "n_heads": 12,
31
- "n_layers": 6,
32
- "pad_token_id": 0,
33
- "problem_type": "single_label_classification",
34
- "qa_dropout": 0.1,
35
- "seq_classif_dropout": 0.2,
36
- "sinusoidal_pos_embds": false,
37
- "tie_weights_": true,
38
- "tie_word_embeddings": true,
39
- "transformers_version": "5.8.0",
40
- "use_cache": false,
41
- "vocab_size": 30522
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/bert_5class/metadata.json DELETED
@@ -1,47 +0,0 @@
1
- {
2
- "model": "distilbert-base-uncased",
3
- "num_labels": 5,
4
- "task": "5-class optimal tier prediction",
5
- "training_dataset": "CARROT-LLM-Routing/SPROUT",
6
- "n_training_samples": 27871,
7
- "correctness_threshold": 0.7,
8
- "tier_mapping": {
9
- "wxai-granite-3-2b-instruct-8k-max-tokens": 1,
10
- "wxai-llama-3-2-1b-instruct": 1,
11
- "wxai-llama-3-2-3b-instruct": 1,
12
- "wxai-llama-3-1-8b-instruct": 2,
13
- "wxai-granite-3-8b-instruct-8k-max-tokens": 2,
14
- "openai-gpt-4o-mini": 2,
15
- "wxai-mixtral-8x7b-instruct-v01": 3,
16
- "aws-titan-text-premier-v1": 3,
17
- "openai-gpt-4o": 4,
18
- "wxai-llama-3-1-70b-instruct": 4,
19
- "wxai-llama-3-3-70b-instruct": 4,
20
- "aws-claude-3-5-sonnet-v1": 4,
21
- "wxai-llama-3-405b-instruct": 5
22
- },
23
- "max_seq_len": 512,
24
- "epochs": 5,
25
- "learning_rate": 2e-05,
26
- "final_metrics": {
27
- "eval_loss": 0.6286362409591675,
28
- "eval_accuracy": 0.7694543106231837,
29
- "eval_off_by_one": 0.9228285437520181,
30
- "eval_acc_tier1": 0.9991586032814472,
31
- "eval_acc_tier2": 0.012605042016806723,
32
- "eval_acc_tier3": 0.0,
33
- "eval_acc_tier4": 0.017391304347826087,
34
- "eval_acc_tier5": 0.0,
35
- "eval_runtime": 12.4974,
36
- "eval_samples_per_second": 247.811,
37
- "eval_steps_per_second": 3.921,
38
- "epoch": 5.0
39
- },
40
- "label_distribution": {
41
- "1": 23972,
42
- "2": 4727,
43
- "4": 1119,
44
- "5": 636,
45
- "3": 514
46
- }
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/bert_5class/model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3865791a1f5061c928e8252f6badb06956b94f91e4f11ba3130e15e2590ff5cf
3
- size 267841796
 
 
 
 
router_models/bert_5class/tokenizer.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/bert_5class/tokenizer_config.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "backend": "tokenizers",
3
- "cls_token": "[CLS]",
4
- "do_lower_case": true,
5
- "is_local": false,
6
- "local_files_only": false,
7
- "mask_token": "[MASK]",
8
- "model_max_length": 512,
9
- "pad_token": "[PAD]",
10
- "sep_token": "[SEP]",
11
- "strip_accents": null,
12
- "tokenize_chinese_chars": true,
13
- "tokenizer_class": "BertTokenizer",
14
- "unk_token": "[UNK]"
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/bert_router/config.json DELETED
@@ -1,28 +0,0 @@
1
- {
2
- "activation": "gelu",
3
- "architectures": [
4
- "DistilBertForSequenceClassification"
5
- ],
6
- "attention_dropout": 0.1,
7
- "bos_token_id": null,
8
- "dim": 768,
9
- "dropout": 0.1,
10
- "dtype": "float32",
11
- "eos_token_id": null,
12
- "hidden_dim": 3072,
13
- "initializer_range": 0.02,
14
- "max_position_embeddings": 512,
15
- "model_type": "distilbert",
16
- "n_heads": 12,
17
- "n_layers": 6,
18
- "pad_token_id": 0,
19
- "problem_type": "single_label_classification",
20
- "qa_dropout": 0.1,
21
- "seq_classif_dropout": 0.2,
22
- "sinusoidal_pos_embds": false,
23
- "tie_weights_": true,
24
- "tie_word_embeddings": true,
25
- "transformers_version": "5.8.0",
26
- "use_cache": false,
27
- "vocab_size": 30522
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/bert_router/model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:21baff75e1ebe25d72bd3c13383eb7d563cf1830c03a37dc81d173706fe473d9
3
- size 267832560
 
 
 
 
router_models/bert_router/tokenizer.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/bert_router/tokenizer_config.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "backend": "tokenizers",
3
- "cls_token": "[CLS]",
4
- "do_lower_case": true,
5
- "is_local": false,
6
- "local_files_only": false,
7
- "mask_token": "[MASK]",
8
- "model_max_length": 512,
9
- "pad_token": "[PAD]",
10
- "sep_token": "[SEP]",
11
- "strip_accents": null,
12
- "tokenize_chinese_chars": true,
13
- "tokenizer_class": "BertTokenizer",
14
- "unk_token": "[UNK]"
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
router_models/tier_1_success.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/tier_2_success.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/tier_3_success.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/tier_4_success.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/tier_5_success.json DELETED
The diff for this file is too large to render. See raw diff
 
router_models/v6_eval_results.json DELETED
@@ -1,304 +0,0 @@
1
- {
2
- "always_frontier": {
3
- "success": 0.8935,
4
- "avg_cost": 1.0,
5
- "unsafe_rate": 0.023,
6
- "false_done": 0.0835,
7
- "tier_dist": {
8
- "4": 2000
9
- },
10
- "escalations": 878,
11
- "downgrades": 420,
12
- "heuristic_only": 702
13
- },
14
- "always_cheap": {
15
- "success": 0.2225,
16
- "avg_cost": 0.04999999999999823,
17
- "unsafe_rate": 0.7775,
18
- "false_done": 0.0,
19
- "tier_dist": {
20
- "1": 2000
21
- },
22
- "escalations": 0,
23
- "downgrades": 2000,
24
- "heuristic_only": 0
25
- },
26
- "heuristic_diff+1": {
27
- "success": 0.8405,
28
- "avg_cost": 0.9271500000000037,
29
- "unsafe_rate": 0.047,
30
- "false_done": 0.1125,
31
- "tier_dist": {
32
- "4": 702,
33
- "3": 651,
34
- "5": 420,
35
- "2": 227
36
- },
37
- "escalations": 0,
38
- "downgrades": 0,
39
- "heuristic_only": 2000
40
- },
41
- "heuristic_floor": {
42
- "success": 0.5875,
43
- "avg_cost": 0.5013749999999922,
44
- "unsafe_rate": 0.28,
45
- "false_done": 0.1325,
46
- "tier_dist": {
47
- "3": 913,
48
- "2": 651,
49
- "4": 209,
50
- "1": 227
51
- },
52
- "escalations": 0,
53
- "downgrades": 2000,
54
- "heuristic_only": 0
55
- },
56
- "oracle": {
57
- "success": 0.998,
58
- "avg_cost": 0.47694999999999094,
59
- "unsafe_rate": 0.0,
60
- "false_done": 0.002,
61
- "tier_dist": {
62
- "3": 644,
63
- "5": 46,
64
- "2": 571,
65
- "4": 294,
66
- "1": 445
67
- },
68
- "escalations": 94,
69
- "downgrades": 1486,
70
- "heuristic_only": 420
71
- },
72
- "hybrid_s0.25_d0.70": {
73
- "success": 0.8175,
74
- "avg_cost": 0.8221500000000023,
75
- "unsafe_rate": 0.0595,
76
- "false_done": 0.123,
77
- "tier_dist": {
78
- "4": 1122,
79
- "3": 651,
80
- "2": 227
81
- },
82
- "escalations": 0,
83
- "downgrades": 420,
84
- "heuristic_only": 1580
85
- },
86
- "hybrid_s0.25_d0.75": {
87
- "success": 0.8175,
88
- "avg_cost": 0.8221500000000023,
89
- "unsafe_rate": 0.0595,
90
- "false_done": 0.123,
91
- "tier_dist": {
92
- "4": 1122,
93
- "3": 651,
94
- "2": 227
95
- },
96
- "escalations": 0,
97
- "downgrades": 420,
98
- "heuristic_only": 1580
99
- },
100
- "hybrid_s0.25_d0.80": {
101
- "success": 0.8225,
102
- "avg_cost": 0.8416500000000027,
103
- "unsafe_rate": 0.058,
104
- "false_done": 0.1195,
105
- "tier_dist": {
106
- "4": 1044,
107
- "3": 651,
108
- "5": 78,
109
- "2": 227
110
- },
111
- "escalations": 0,
112
- "downgrades": 342,
113
- "heuristic_only": 1658
114
- },
115
- "hybrid_s0.25_d0.85": {
116
- "success": 0.838,
117
- "avg_cost": 0.9084000000000035,
118
- "unsafe_rate": 0.048,
119
- "false_done": 0.114,
120
- "tier_dist": {
121
- "4": 777,
122
- "3": 651,
123
- "5": 345,
124
- "2": 227
125
- },
126
- "escalations": 0,
127
- "downgrades": 75,
128
- "heuristic_only": 1925
129
- },
130
- "hybrid_s0.30_d0.70": {
131
- "success": 0.8175,
132
- "avg_cost": 0.8221500000000023,
133
- "unsafe_rate": 0.0595,
134
- "false_done": 0.123,
135
- "tier_dist": {
136
- "4": 1122,
137
- "3": 651,
138
- "2": 227
139
- },
140
- "escalations": 0,
141
- "downgrades": 420,
142
- "heuristic_only": 1580
143
- },
144
- "hybrid_s0.30_d0.75": {
145
- "success": 0.8175,
146
- "avg_cost": 0.8221500000000023,
147
- "unsafe_rate": 0.0595,
148
- "false_done": 0.123,
149
- "tier_dist": {
150
- "4": 1122,
151
- "3": 651,
152
- "2": 227
153
- },
154
- "escalations": 0,
155
- "downgrades": 420,
156
- "heuristic_only": 1580
157
- },
158
- "hybrid_s0.30_d0.80": {
159
- "success": 0.8225,
160
- "avg_cost": 0.8416500000000027,
161
- "unsafe_rate": 0.058,
162
- "false_done": 0.1195,
163
- "tier_dist": {
164
- "4": 1044,
165
- "3": 651,
166
- "5": 78,
167
- "2": 227
168
- },
169
- "escalations": 0,
170
- "downgrades": 342,
171
- "heuristic_only": 1658
172
- },
173
- "hybrid_s0.30_d0.85": {
174
- "success": 0.838,
175
- "avg_cost": 0.9084000000000035,
176
- "unsafe_rate": 0.048,
177
- "false_done": 0.114,
178
- "tier_dist": {
179
- "4": 777,
180
- "3": 651,
181
- "5": 345,
182
- "2": 227
183
- },
184
- "escalations": 0,
185
- "downgrades": 75,
186
- "heuristic_only": 1925
187
- },
188
- "hybrid_s0.35_d0.70": {
189
- "success": 0.8175,
190
- "avg_cost": 0.8221500000000023,
191
- "unsafe_rate": 0.0595,
192
- "false_done": 0.123,
193
- "tier_dist": {
194
- "4": 1122,
195
- "3": 651,
196
- "2": 227
197
- },
198
- "escalations": 0,
199
- "downgrades": 420,
200
- "heuristic_only": 1580
201
- },
202
- "hybrid_s0.35_d0.75": {
203
- "success": 0.8175,
204
- "avg_cost": 0.8221500000000023,
205
- "unsafe_rate": 0.0595,
206
- "false_done": 0.123,
207
- "tier_dist": {
208
- "4": 1122,
209
- "3": 651,
210
- "2": 227
211
- },
212
- "escalations": 0,
213
- "downgrades": 420,
214
- "heuristic_only": 1580
215
- },
216
- "hybrid_s0.35_d0.80": {
217
- "success": 0.8225,
218
- "avg_cost": 0.8416500000000027,
219
- "unsafe_rate": 0.058,
220
- "false_done": 0.1195,
221
- "tier_dist": {
222
- "4": 1044,
223
- "3": 651,
224
- "5": 78,
225
- "2": 227
226
- },
227
- "escalations": 0,
228
- "downgrades": 342,
229
- "heuristic_only": 1658
230
- },
231
- "hybrid_s0.35_d0.85": {
232
- "success": 0.838,
233
- "avg_cost": 0.9084000000000035,
234
- "unsafe_rate": 0.048,
235
- "false_done": 0.114,
236
- "tier_dist": {
237
- "4": 777,
238
- "3": 651,
239
- "5": 345,
240
- "2": 227
241
- },
242
- "escalations": 0,
243
- "downgrades": 75,
244
- "heuristic_only": 1925
245
- },
246
- "hybrid_s0.40_d0.70": {
247
- "success": 0.8175,
248
- "avg_cost": 0.8221500000000023,
249
- "unsafe_rate": 0.0595,
250
- "false_done": 0.123,
251
- "tier_dist": {
252
- "4": 1122,
253
- "3": 651,
254
- "2": 227
255
- },
256
- "escalations": 0,
257
- "downgrades": 420,
258
- "heuristic_only": 1580
259
- },
260
- "hybrid_s0.40_d0.75": {
261
- "success": 0.8175,
262
- "avg_cost": 0.8221500000000023,
263
- "unsafe_rate": 0.0595,
264
- "false_done": 0.123,
265
- "tier_dist": {
266
- "4": 1122,
267
- "3": 651,
268
- "2": 227
269
- },
270
- "escalations": 0,
271
- "downgrades": 420,
272
- "heuristic_only": 1580
273
- },
274
- "hybrid_s0.40_d0.80": {
275
- "success": 0.8225,
276
- "avg_cost": 0.8416500000000027,
277
- "unsafe_rate": 0.058,
278
- "false_done": 0.1195,
279
- "tier_dist": {
280
- "4": 1044,
281
- "3": 651,
282
- "5": 78,
283
- "2": 227
284
- },
285
- "escalations": 0,
286
- "downgrades": 342,
287
- "heuristic_only": 1658
288
- },
289
- "hybrid_s0.40_d0.85": {
290
- "success": 0.838,
291
- "avg_cost": 0.9084000000000035,
292
- "unsafe_rate": 0.048,
293
- "false_done": 0.114,
294
- "tier_dist": {
295
- "4": 777,
296
- "3": 651,
297
- "5": 345,
298
- "2": 227
299
- },
300
- "escalations": 0,
301
- "downgrades": 75,
302
- "heuristic_only": 1925
303
- }
304
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_baar_router.py DELETED
@@ -1,470 +0,0 @@
1
- #!/usr/bin/env python3
2
- """BAAR-Style Profiling Router for ACO.
3
-
4
- Inspired by BAAR (Shnitzer et al., 2025): "Budget-Aware Adaptive Routing."
5
- The core idea: profile with a cheap model first, use its output as a feature
6
- for deciding whether to escalate to a stronger model.
7
-
8
- Our approach:
9
- 1. BERT [CLS] embedding of the problem statement → semantic features
10
- 2. Combine with classic keyword features
11
- 3. XGBoost predicts P(success) for each tier
12
- 4. Route to cheapest tier with P(success) >= threshold
13
- 5. Compare: BERT+XGBoost vs keyword-only XGBoost vs frontier vs oracle
14
-
15
- The key innovation over v10: BERT features capture semantic difficulty
16
- that keyword features miss (e.g., "fix the segfault in the async event loop"
17
- vs "add a comment to the README" — keywords see "segfault" and "async" and
18
- think it's hard, but the BERT embedding captures the holistic semantics).
19
- """
20
- import json, os, sys, pickle, random
21
- import numpy as np
22
- from collections import defaultdict
23
- from datasets import load_dataset
24
- from transformers import AutoTokenizer, AutoModel
25
- import torch
26
-
27
- # ── Config ──────────────────────────────────────────────────────
28
- REPO = "narcolepticchicken/agent-cost-optimizer"
29
- TIER_COST = {1: 0.01, 2: 0.05, 3: 0.15, 4: 0.30, 5: 0.50}
30
- TIER_TO_SWE = {
31
- 1: "deepseek-v4-flash", 2: "gpt-5-mini",
32
- 3: "gemini-2.5-pro", 4: "claude-opus-4.7", 5: "gemini-3-pro",
33
- }
34
- MODEL_TIER = {
35
- "deepseek-v4-flash": 1, "gpt-5-nano": 1,
36
- "gpt-5-mini": 2, "deepseek-v3.2": 2,
37
- "gemini-2.5-pro": 3,
38
- "claude-opus-4.7": 4, "gpt-5.2": 4,
39
- "gemini-3-pro": 5,
40
- }
41
- MODELS = list(MODEL_TIER.keys())
42
-
43
- print("=" * 70)
44
- print("BAAR-STYLE PROFILING ROUTER: BERT + XGBoost")
45
- print("=" * 70)
46
-
47
- # ── 1. Extract BERT [CLS] features ──────────────────────────────
48
- print("\n[1] Loading BERT for feature extraction...")
49
-
50
- # First try the trained 5-class model (has domain knowledge from SPROUT)
51
- try:
52
- tokenizer = AutoTokenizer.from_pretrained(
53
- REPO, subfolder="router_models/bert_5class"
54
- )
55
- bert_model = AutoModel.from_pretrained(
56
- REPO, subfolder="router_models/bert_5class"
57
- )
58
- print(" ✓ Loaded trained BERT 5-class from Hub (SPROUT-fine-tuned)")
59
- except Exception as e:
60
- print(f" Trained BERT failed: {e}")
61
- tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
62
- bert_model = AutoModel.from_pretrained("distilbert-base-uncased")
63
- print(" Using base distilbert (not fine-tuned)")
64
-
65
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
66
- bert_model = bert_model.to(device)
67
- bert_model.eval()
68
-
69
- # ── 2. Load SWE-Router ──────────────────────────────────────────
70
- print("\n[2] Loading SWE-Router execution data...")
71
- traces = defaultdict(dict)
72
- for model_name in MODELS:
73
- ds = load_dataset(f"SWE-Router/swebench-verified-{model_name}", split="test")
74
- for row in ds:
75
- iid = row["instance_id"]
76
- traces[iid][model_name] = {
77
- "resolved": row["resolved"],
78
- "cost": float(row["instance_cost"]),
79
- "api_calls": int(row["api_calls"]),
80
- "problem": row["problem_statement"],
81
- }
82
- print(f" {model_name}: loaded")
83
- print(f" Total tasks: {len(traces)}")
84
-
85
- # ── 3. Build combined features ──────────────────────────────────
86
- print("\n[3] Extracting BERT [CLS] embeddings + keyword features...")
87
-
88
- # Keyword features (same as v10)
89
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
90
- "compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
91
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
92
- RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
93
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
94
- CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
95
- SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
96
- LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
97
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
98
-
99
- BERT_DIM = 768 # distilbert-base-uncased
100
-
101
- def extract_features(problem_text):
102
- """Extract combined BERT + keyword features."""
103
- r = problem_text.lower()
104
-
105
- # Classic keyword features
106
- kw_feats = [
107
- len(problem_text), # req_len
108
- len(problem_text.split()), # num_words
109
- int(any(k in r for k in CODE_KW)), # has_code
110
- sum(1 for k in CODE_KW if k in r), # n_code
111
- int(any(k in r for k in LEGAL_KW)),
112
- int(any(k in r for k in RESEARCH_KW)),
113
- int(any(k in r for k in TOOL_KW)),
114
- int(any(k in r for k in CRITICAL_KW)),
115
- int(any(k in r for k in SIMPLE_KW)),
116
- int(any(k in r for k in LONG_KW)),
117
- int(any(k in r for k in MATH_KW)),
118
- int("error" in r or "traceback" in r or "exception" in r),
119
- int("/" in r and "." in problem_text), # has_file_path
120
- problem_text.count("\n") + 1, # n_lines
121
- int("version" in r or "update" in r or "upgrade" in r),
122
- int("add" in r or "new" in r or "create" in r),
123
- int("fix" in r or "bug" in r or "issue" in r or "broken" in r),
124
- int("change" in r or "modify" in r or "update" in r),
125
- int("remove" in r or "delete" in r or "drop" in r),
126
- int("test" in r or "spec" in r or "assert" in r),
127
- int("doc" in r or "readme" in r or "comment" in r),
128
- int("see also" in r or "related" in r),
129
- int("steps to reproduce" in r or "reproduce" in r),
130
- ]
131
-
132
- # BERT [CLS] embedding
133
- enc = tokenizer(
134
- problem_text[:2000], truncation=True, max_length=512,
135
- return_tensors="pt",
136
- )
137
- enc = {k: v.to(device) for k, v in enc.items()}
138
- with torch.no_grad():
139
- outputs = bert_model(**enc)
140
- cls_embedding = outputs.last_hidden_state[:, 0, :] # [1, 768]
141
-
142
- # Combine
143
- full = kw_feats + cls_embedding[0].cpu().numpy().tolist()
144
- return full
145
-
146
- # Build training data
147
- X_feats = []
148
- y_tiers = {t: [] for t in range(1, 6)}
149
- y_optimal = []
150
- feat_names = (
151
- ["req_len","num_words","has_code","n_code","has_legal","has_research","has_tool",
152
- "has_critical","has_simple","has_long","has_math","has_error_msg","has_file_path",
153
- "n_lines","has_version","has_add","has_fix","has_change","has_remove","has_test",
154
- "has_doc","has_see_also","has_steps"] +
155
- [f"bert_{i}" for i in range(BERT_DIM)]
156
- )
157
-
158
- for iid, model_results in traces.items():
159
- problem = next(iter(model_results.values()))["problem"]
160
- feats = extract_features(problem)
161
- X_feats.append(feats)
162
-
163
- # Per-tier success ground truth
164
- tier_success = {}
165
- for model_name, result in model_results.items():
166
- tier = MODEL_TIER[model_name]
167
- if tier not in tier_success:
168
- tier_success[tier] = False
169
- if result["resolved"]:
170
- tier_success[tier] = True
171
-
172
- for t in range(1, 6):
173
- y_tiers[t].append(int(tier_success.get(t, False)))
174
-
175
- # Optimal tier = cheapest that succeeded
176
- optimal = 5
177
- for t in range(1, 6):
178
- if tier_success.get(t, False):
179
- optimal = t
180
- break
181
- y_optimal.append(optimal)
182
-
183
- X = np.array(X_feats, dtype=np.float32)
184
-
185
- print(f" Feature matrix: {X.shape} ({len(feat_names)} features)")
186
- print(f" BERT features: {BERT_DIM} dims + {len(feat_names)-BERT_DIM} keyword feats")
187
- print(f" Per-tier success rates:")
188
- for t in range(1, 6):
189
- s = sum(y_tiers[t])
190
- print(f" Tier {t}: {s}/{len(y_tiers[t])} = {s/len(y_tiers[t])*100:.1f}%")
191
- print(f" Optimal tier distribution:")
192
- opt_dist = defaultdict(int)
193
- for t in y_optimal:
194
- opt_dist[t] += 1
195
- for t in sorted(opt_dist):
196
- print(f" Tier {t}: {opt_dist[t]} ({opt_dist[t]/len(y_optimal)*100:.1f}%)")
197
-
198
- # ── 4. Train XGBoost with BERT features ─────────────────────────
199
- print("\n[4] Training BAAR-style XGBoost + BERT router...")
200
-
201
- from xgboost import XGBClassifier
202
- from sklearn.calibration import IsotonicRegression
203
- from sklearn.model_selection import cross_val_score
204
- import warnings
205
- warnings.filterwarnings("ignore")
206
-
207
- tier_clfs = {}
208
- tier_calibs = {}
209
- tier_cv = {}
210
-
211
- for t in range(1, 6):
212
- y = np.array(y_tiers[t])
213
- n_pos = y.sum()
214
- n_neg = len(y) - n_pos
215
- spw = max(1, n_neg / max(n_pos, 1))
216
-
217
- clf = XGBClassifier(
218
- n_estimators=200, max_depth=6, learning_rate=0.05,
219
- subsample=0.8, colsample_bytree=0.8,
220
- scale_pos_weight=spw,
221
- eval_metric="logloss",
222
- random_state=42,
223
- verbosity=0,
224
- )
225
-
226
- try:
227
- scores = cross_val_score(clf, X, y, cv=5, scoring="f1")
228
- tier_cv[t] = float(scores.mean())
229
- except:
230
- tier_cv[t] = 0.0
231
-
232
- clf.fit(X, y)
233
-
234
- # Calibrate probabilities
235
- p_raw = clf.predict_proba(X)[:, 1]
236
- cal = IsotonicRegression(out_of_bounds="clip")
237
- cal.fit(p_raw, y)
238
-
239
- tier_clfs[t] = clf
240
- tier_calibs[t] = cal
241
-
242
- p_cal = cal.transform(p_raw)
243
- brier = np.mean((p_cal - y) ** 2)
244
-
245
- print(f" Tier {t}: n_pos={n_pos}, CV_f1={tier_cv[t]:.3f}, Brier={brier:.4f}")
246
-
247
- # Direct optimal-tier predictor
248
- opt_clf = XGBClassifier(
249
- n_estimators=300, max_depth=6, learning_rate=0.05,
250
- subsample=0.8, colsample_bytree=0.8,
251
- eval_metric="mlogloss",
252
- random_state=42, num_class=5, verbosity=0,
253
- )
254
- opt_clf.fit(X, np.array(y_optimal) - 1)
255
- opt_pred = opt_clf.predict(X) + 1
256
- opt_acc = np.mean(opt_pred == np.array(y_optimal))
257
- print(f" Direct optimal-tier accuracy: {opt_acc:.3f}")
258
-
259
- # Also train a keyword-only baseline for ablation
260
- print("\n[4b] Training keyword-only baseline (no BERT)...")
261
- kw_X = X[:, :23] # First 23 are keyword features
262
-
263
- kw_tier_clfs = {}
264
- kw_tier_calibs = {}
265
- kw_tier_cv = {}
266
-
267
- for t in range(1, 6):
268
- y = np.array(y_tiers[t])
269
- n_pos = y.sum()
270
- n_neg = len(y) - n_pos
271
- spw = max(1, n_neg / max(n_pos, 1))
272
-
273
- clf = XGBClassifier(
274
- n_estimators=200, max_depth=6, learning_rate=0.05,
275
- subsample=0.8, colsample_bytree=0.8,
276
- scale_pos_weight=spw,
277
- eval_metric="logloss",
278
- random_state=42,
279
- verbosity=0,
280
- )
281
-
282
- try:
283
- scores = cross_val_score(clf, kw_X, y, cv=5, scoring="f1")
284
- kw_tier_cv[t] = float(scores.mean())
285
- except:
286
- kw_tier_cv[t] = 0.0
287
-
288
- clf.fit(kw_X, y)
289
- p_raw = clf.predict_proba(kw_X)[:, 1]
290
- cal = IsotonicRegression(out_of_bounds="clip")
291
- cal.fit(p_raw, y)
292
-
293
- kw_tier_clfs[t] = clf
294
- kw_tier_calibs[t] = cal
295
-
296
- # ── 5. Evaluate all policies ─────────────────────────────────────
297
- print("\n[5] Evaluating routing policies...")
298
-
299
- policies = defaultdict(lambda: {"success": 0, "cost": 0.0, "n": 0})
300
-
301
- for iid, model_results in traces.items():
302
- problem = next(iter(model_results.values()))["problem"]
303
- feats = extract_features(problem)
304
- feat_vec = np.array(feats, dtype=np.float32).reshape(1, -1)
305
- kw_vec = np.array(feats[:23], dtype=np.float32).reshape(1, -1)
306
-
307
- # ─── BAAR (BERT + XGBoost) ───
308
- tier_probs_baar = {}
309
- for t in range(1, 6):
310
- p_raw = tier_clfs[t].predict_proba(feat_vec)[0, 1]
311
- p_cal = float(tier_calibs[t].transform([p_raw])[0])
312
- tier_probs_baar[t] = p_cal
313
-
314
- baar_tier = 5
315
- for t in range(1, 6):
316
- if tier_probs_baar[t] >= 0.5:
317
- baar_tier = t
318
- break
319
- baar_model = TIER_TO_SWE.get(baar_tier, "claude-opus-4.7")
320
- if baar_model in model_results:
321
- policies["baar"]["success"] += int(model_results[baar_model]["resolved"])
322
- policies["baar"]["cost"] += model_results[baar_model]["cost"]
323
- policies["baar"]["n"] += 1
324
-
325
- # ─── BAAR + feedback (escalate on failure) ───
326
- try_tier = baar_tier
327
- total_cost = 0.0
328
- success = False
329
- while try_tier <= 5:
330
- tm = TIER_TO_SWE.get(try_tier, "claude-opus-4.7")
331
- if tm in model_results:
332
- total_cost += model_results[tm]["cost"]
333
- if model_results[tm]["resolved"]:
334
- success = True
335
- break
336
- try_tier += 1
337
- policies["baar_feedback"]["success"] += int(success)
338
- policies["baar_feedback"]["cost"] += total_cost
339
- policies["baar_feedback"]["n"] += 1
340
-
341
- # ─── Keyword-only ───
342
- kw_tier = 5
343
- for t in range(1, 6):
344
- p_raw = kw_tier_clfs[t].predict_proba(kw_vec)[0, 1]
345
- p_cal = float(kw_tier_calibs[t].transform([p_raw])[0])
346
- if p_cal >= 0.5:
347
- kw_tier = t
348
- break
349
- kw_model = TIER_TO_SWE.get(kw_tier, "claude-opus-4.7")
350
- if kw_model in model_results:
351
- policies["keyword_only"]["success"] += int(model_results[kw_model]["resolved"])
352
- policies["keyword_only"]["cost"] += model_results[kw_model]["cost"]
353
- policies["keyword_only"]["n"] += 1
354
-
355
- # ─── Frontiers ───
356
- f_model = "claude-opus-4.7"
357
- policies["frontier"]["success"] += int(model_results[f_model]["resolved"])
358
- policies["frontier"]["cost"] += model_results[f_model]["cost"]
359
- policies["frontier"]["n"] += 1
360
-
361
- c_model = "deepseek-v4-flash"
362
- if c_model in model_results:
363
- policies["always_cheap"]["success"] += int(model_results[c_model]["resolved"])
364
- policies["always_cheap"]["cost"] += model_results[c_model]["cost"]
365
- policies["always_cheap"]["n"] += 1
366
-
367
- # ─── Oracle ───
368
- resolved = [(m, r) for m, r in model_results.items() if r["resolved"]]
369
- if resolved:
370
- cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
371
- policies["oracle"]["success"] += 1
372
- policies["oracle"]["cost"] += cheapest[1]["cost"]
373
- policies["oracle"]["n"] += 1
374
-
375
- # ── 6. Print results ────────────────────────────────────────────
376
- fr_cost = policies["frontier"]["cost"] / max(policies["frontier"]["n"], 1)
377
-
378
- print(f"\n{'='*70}")
379
- print("BAAR-STYLE ROUTER RESULTS ON SWE-BENCH (500 tasks)")
380
- print(f"{'='*70}")
381
- print(f"\n{'Policy':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
382
- print("-" * 55)
383
-
384
- for name in ["oracle", "baar_feedback", "baar", "keyword_only", "frontier", "always_cheap"]:
385
- r = policies[name]
386
- sr = r["success"] / max(r["n"], 1)
387
- ac = r["cost"] / max(r["n"], 1)
388
- cr = (1 - ac / max(fr_cost, 0.0001)) * 100
389
- print(f" {name:<23} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
390
-
391
- # Compare CV scores
392
- print(f"\n{'='*70}")
393
- print("CROSS-VALIDATION F1 COMPARISON")
394
- print(f"{'='*70}")
395
- print(f"\n{'Tier':<8} {'Keyword-only':>15} {'BAAR (BERT+KW)':>18} {'Improvement':>14}")
396
- print("-" * 55)
397
- for t in range(1, 6):
398
- kw_cv = kw_tier_cv.get(t, 0)
399
- baar_cv = tier_cv.get(t, 0)
400
- imp = baar_cv - kw_cv
401
- print(f" Tier {t:<4} {kw_cv:>15.3f} {baar_cv:>18.3f} {imp:>13.3f}")
402
-
403
- # Save results
404
- results = {
405
- "router": "BAAR-style (BERT [CLS] + XGBoost)",
406
- "n_tasks": len(traces),
407
- "n_features": X.shape[1],
408
- "n_bert_dims": BERT_DIM,
409
- "n_keyword_feats": 23,
410
- "policies": {
411
- name: {
412
- "success": r["success"],
413
- "success_rate": round(r["success"] / max(r["n"], 1), 4),
414
- "avg_cost": round(r["cost"] / max(r["n"], 1), 4),
415
- "cost_reduction_pct": round(
416
- (1 - (r["cost"] / max(r["n"], 1)) / max(fr_cost, 0.0001)) * 100, 1
417
- ),
418
- }
419
- for name, r in policies.items()
420
- },
421
- "cv_f1_comparison": {
422
- "keyword_only": {str(k): v for k, v in kw_tier_cv.items()},
423
- "baar_bert_kw": {str(k): v for k, v in tier_cv.items()},
424
- },
425
- "opt_tier_accuracy": float(opt_acc),
426
- }
427
-
428
- with open("/tmp/baar_results.json", "w") as f:
429
- json.dump(results, f, indent=2)
430
-
431
- # Upload to Hub
432
- from huggingface_hub import HfApi
433
- api = HfApi()
434
- api.upload_file(
435
- path_or_fileobj="/tmp/baar_results.json",
436
- path_in_repo="eval/baar_results.json",
437
- repo_id=REPO,
438
- repo_type="model",
439
- )
440
- print(f"\n ✓ Results uploaded to Hub: eval/baar_results.json")
441
-
442
- # Save model bundle
443
- bundle = {
444
- "tier_clfs": {str(k): v for k, v in tier_clfs.items()},
445
- "tier_calibrators": {str(k): v for k, v in tier_calibs.items()},
446
- "opt_clf": opt_clf,
447
- "kw_tier_clfs": {str(k): v for k, v in kw_tier_clfs.items()},
448
- "kw_tier_calibrators": {str(k): v for k, v in kw_tier_calibs.items()},
449
- "feat_names": feat_names,
450
- "bert_dim": BERT_DIM,
451
- "n_keyword_feats": 23,
452
- "version": "baar_v1",
453
- "description": "BAAR-style router: BERT [CLS] embeddings + keyword features → XGBoost tier predictor",
454
- "tier_cost": TIER_COST,
455
- }
456
-
457
- with open("/tmp/baar_bundle.pkl", "wb") as f:
458
- pickle.dump(bundle, f)
459
-
460
- api.upload_file(
461
- path_or_fileobj="/tmp/baar_bundle.pkl",
462
- path_in_repo="router_models/baar_bundle.pkl",
463
- repo_id=REPO,
464
- repo_type="model",
465
- )
466
- print(f" ✓ BAAR bundle uploaded: router_models/baar_bundle.pkl")
467
-
468
- print(f"\n{'='*70}")
469
- print("DONE! BAAR-style router built and evaluated.")
470
- print("=" * 70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_bert_5class.py DELETED
@@ -1,187 +0,0 @@
1
- """Retrain BERT as a 5-class tier router.
2
-
3
- Uses SPROUT data to predict optimal tier (1-5) directly.
4
- """
5
- import os, json, random
6
- import numpy as np
7
- from datasets import load_dataset
8
- from transformers import (
9
- AutoTokenizer, AutoModelForSequenceClassification,
10
- TrainingArguments, Trainer, DataCollatorWithPadding,
11
- )
12
- import torch
13
-
14
- REPO = "narcolepticchicken/agent-cost-optimizer"
15
- print("BERT 5-Class Tier Router Training")
16
- print("="*60)
17
-
18
- # ── Load SPROUT ──
19
- print("\n[1] Loading SPROUT dataset...")
20
- ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train", trust_remote_code=True)
21
- print(f" Total rows: {len(ds)}")
22
- print(f" Columns: {ds.column_names}")
23
-
24
- # ── Model tier mapping ──
25
- # SPROUT models → tiers (same as v11 training)
26
- MODEL_TIER_MAP = {}
27
- TIER_MODELS = {
28
- 1: ["gemma-2-2b-it","phi-3-mini-128k-instruct","qwen2.5-3b-instruct",
29
- "llama-3.2-3b-instruct","deepseek-v3.2"],
30
- 2: ["gemma-2-9b-it","mistral-7b-instruct-v0.3","qwen2.5-7b-instruct",
31
- "llama-3.1-8b-instruct","gpt-5-nano","gpt-5-mini"],
32
- 3: ["qwen2.5-32b-instruct","mixtral-8x7b-instruct-v0.1",
33
- "gemma-2-27b-it","gemini-2.5-pro"],
34
- 4: ["claude-opus-4.7","gpt-5.2","llama-3.1-70b-instruct",
35
- "qwen2.5-72b-instruct"],
36
- 5: ["gemini-3-pro","deepseek-v4-flash"],
37
- }
38
- for tier, models in TIER_MODELS.items():
39
- for m in models:
40
- MODEL_TIER_MAP[m.lower()] = tier
41
-
42
- # ── Build training data ──
43
- print("\n[2] Building training data...")
44
- texts = []
45
- labels = []
46
- skipped = 0
47
-
48
- for row in ds:
49
- # Find the cheapest tier that succeeded
50
- best_tier = 5 # default
51
- found = False
52
-
53
- # Try to find model results in the row
54
- for tier in range(1, 6):
55
- for m in TIER_MODELS.get(tier, []):
56
- m_lower = m.lower()
57
- # Check various possible column names
58
- for col in ds.column_names:
59
- if m_lower in col.lower():
60
- val = row.get(col)
61
- if isinstance(val, (int, float)) and val > 0:
62
- best_tier = tier
63
- found = True
64
- break
65
- if found:
66
- break
67
- if found:
68
- break
69
-
70
- # Get the prompt/question text
71
- prompt = ""
72
- for col in ["prompt", "question", "input", "query", "problem_statement", "instruction"]:
73
- if col in ds.column_names:
74
- prompt = str(row[col])
75
- break
76
-
77
- if not prompt:
78
- # Try first string column
79
- for col in ds.column_names:
80
- if isinstance(row[col], str) and len(row[col]) > 20:
81
- prompt = row[col]
82
- break
83
-
84
- if prompt and len(prompt) > 10:
85
- texts.append(prompt[:2000]) # truncate long texts
86
- labels.append(best_tier - 1) # 0-indexed for classification
87
- else:
88
- skipped += 1
89
-
90
- print(f" Training samples: {len(texts)}")
91
- print(f" Skipped: {skipped}")
92
- print(f" Label distribution:")
93
- from collections import Counter
94
- label_dist = Counter(labels)
95
- for label in sorted(label_dist):
96
- print(f" Tier {label+1}: {label_dist[label]} ({label_dist[label]/len(labels)*100:.1f}%)")
97
-
98
- # ── Tokenize ──
99
- print("\n[3] Tokenizing...")
100
- tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
101
-
102
- encodings = tokenizer(texts, truncation=True, max_length=512, padding=False)
103
-
104
- class TierDataset(torch.utils.data.Dataset):
105
- def __init__(self, encodings, labels):
106
- self.encodings = encodings
107
- self.labels = labels
108
- def __getitem__(self, idx):
109
- item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
110
- item["labels"] = torch.tensor(self.labels[idx])
111
- return item
112
- def __len__(self):
113
- return len(self.labels)
114
-
115
- # Split
116
- split = int(0.9 * len(texts))
117
- train_ds = TierDataset(
118
- {k: v[:split] for k, v in encodings.items()},
119
- labels[:split],
120
- )
121
- eval_ds = TierDataset(
122
- {k: v[split:] for k, v in encodings.items()},
123
- labels[split:],
124
- )
125
- print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
126
-
127
- # ── Train ──
128
- print("\n[4] Training 5-class BERT router...")
129
- model = AutoModelForSequenceClassification.from_pretrained(
130
- "distilbert-base-uncased", num_labels=5
131
- )
132
-
133
- training_args = TrainingArguments(
134
- output_dir="/tmp/bert_5class",
135
- num_train_epochs=3,
136
- per_device_train_batch_size=32,
137
- per_device_eval_batch_size=64,
138
- learning_rate=2e-5,
139
- weight_decay=0.01,
140
- eval_strategy="epoch",
141
- save_strategy="epoch",
142
- load_best_model_at_end=True,
143
- metric_for_best_model="eval_accuracy",
144
- logging_steps=50,
145
- disable_tqdm=True,
146
- )
147
-
148
- def compute_metrics(eval_pred):
149
- logits, labels = eval_pred
150
- preds = np.argmax(logits, axis=-1)
151
- acc = np.mean(preds == labels)
152
- return {"accuracy": acc}
153
-
154
- trainer = Trainer(
155
- model=model,
156
- args=training_args,
157
- train_dataset=train_ds,
158
- eval_dataset=eval_ds,
159
- tokenizer=tokenizer,
160
- data_collator=DataCollatorWithPadding(tokenizer),
161
- compute_metrics=compute_metrics,
162
- )
163
-
164
- trainer.train()
165
-
166
- # ── Save and upload ──
167
- print("\n[5] Saving model...")
168
- save_dir = "/tmp/bert_5class_final"
169
- model.save_pretrained(save_dir)
170
- tokenizer.save_pretrained(save_dir)
171
- print(f" Saved to {save_dir}")
172
-
173
- # Upload to Hub
174
- from huggingface_hub import HfApi
175
- api = HfApi()
176
- for fname in os.listdir(save_dir):
177
- fpath = os.path.join(save_dir, fname)
178
- if os.path.isfile(fpath):
179
- api.upload_file(
180
- path_or_fileobj=fpath,
181
- path_in_repo=f"router_models/bert_5class/{fname}",
182
- repo_id=REPO,
183
- repo_type="model",
184
- )
185
- print(f" Uploaded {fname}")
186
-
187
- print("\nDONE! 5-class BERT router saved to router_models/bert_5class/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_bert_5class_v2.py DELETED
@@ -1,291 +0,0 @@
1
- #!/usr/bin/env python3
2
- """BERT 5-Class Tier Router — V2 with PROPER SPROUT parsing.
3
-
4
- Fixes the V1 bug: SPROUT model columns are dicts like:
5
- {'judge_response': '{"correctness_score": 0.8, "justification": "..."}'}
6
- which need JSON parsing, not naive int/float checks.
7
-
8
- Also writes a robust label extraction that maps SPROUT's 13 models → ACO's 5 cost tiers
9
- and labels each task with the cheapest tier that achieves correctness_score >= THRESHOLD.
10
- """
11
- import json, os, sys
12
- import numpy as np
13
- from collections import Counter
14
- from datasets import load_dataset
15
- import torch
16
- from transformers import (
17
- AutoTokenizer,
18
- AutoModelForSequenceClassification,
19
- TrainingArguments,
20
- Trainer,
21
- DataCollatorWithPadding,
22
- )
23
- from sklearn.model_selection import train_test_split
24
-
25
- REPO = "narcolepticchicken/agent-cost-optimizer"
26
- CORRECTNESS_THRESHOLD = 0.7 # correctness_score >= this = "succeeded"
27
- MAX_SEQ_LEN = 512
28
- BATCH_SIZE = 32
29
- EPOCHS = 5
30
- LR = 2e-5
31
-
32
- print("="*70)
33
- print("BERT 5-CLASS TIER ROUTER — V2 (Fixed SPROUT Parsing)")
34
- print("="*70)
35
-
36
- # ── 1. Map SPROUT models → ACO cost tiers ──────────────────────────
37
- SPROUT_MODEL_TO_TIER = {
38
- # Tier 1: smallest/cheapest
39
- "wxai-granite-3-2b-instruct-8k-max-tokens": 1,
40
- "wxai-llama-3-2-1b-instruct": 1,
41
- "wxai-llama-3-2-3b-instruct": 1,
42
- # Tier 2: small/medium
43
- "wxai-llama-3-1-8b-instruct": 2,
44
- "wxai-granite-3-8b-instruct-8k-max-tokens": 2,
45
- "openai-gpt-4o-mini": 2,
46
- # Tier 3: medium
47
- "wxai-mixtral-8x7b-instruct-v01": 3,
48
- "aws-titan-text-premier-v1": 3,
49
- # Tier 4: strong
50
- "openai-gpt-4o": 4,
51
- "wxai-llama-3-1-70b-instruct": 4,
52
- "wxai-llama-3-3-70b-instruct": 4,
53
- "aws-claude-3-5-sonnet-v1": 4,
54
- # Tier 5: strongest
55
- "wxai-llama-3-405b-instruct": 5,
56
- }
57
-
58
- SPROUT_MODEL_COLS = list(SPROUT_MODEL_TO_TIER.keys())
59
- print(f"\n[1] SPROUT model mapping: {len(SPROUT_MODEL_COLS)} models → 5 ACO tiers")
60
-
61
- # ── 2. Load SPROUT & extract correctness scores ────────────────────
62
- print("\n[2] Loading SPROUT dataset...")
63
- ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train", trust_remote_code=True)
64
- print(f" Total rows: {len(ds)}")
65
-
66
- print("\n[3] Parsing judge_response JSON and extracting correctness_scores...")
67
-
68
- texts = []
69
- labels = []
70
- parse_errors = 0
71
- no_score = 0
72
- all_failed = 0
73
- label_counter = Counter()
74
-
75
- # Track per-model success rates for sanity check
76
- model_success_counts = Counter()
77
- model_total_counts = Counter()
78
-
79
- for idx, row in enumerate(ds):
80
- prompt = str(row.get("prompt") or "")
81
- if not prompt or len(prompt) < 10:
82
- all_failed += 1
83
- continue
84
-
85
- # Find the cheapest tier where any model succeeded
86
- best_tier = 5 # default to frontier
87
- found_success = False
88
-
89
- for model_col in SPROUT_MODEL_COLS:
90
- tier = SPROUT_MODEL_TO_TIER[model_col]
91
-
92
- if model_col not in row:
93
- continue
94
-
95
- val = row[model_col]
96
-
97
- # Handle the nested dict structure
98
- score = None
99
- try:
100
- if isinstance(val, dict):
101
- # {'judge_response': '{"correctness_score": 0.8, ...}'}
102
- jr = val.get("judge_response", "")
103
- if isinstance(jr, str) and jr.strip():
104
- jr_parsed = json.loads(jr)
105
- score = jr_parsed.get("correctness_score", None)
106
- elif isinstance(jr, dict):
107
- # Already parsed JSON
108
- score = jr.get("correctness_score", None)
109
- elif isinstance(val, str):
110
- # Direct JSON string
111
- val_parsed = json.loads(val)
112
- jr = val_parsed.get("judge_response", "")
113
- if isinstance(jr, str) and jr.strip():
114
- jr_parsed = json.loads(jr)
115
- score = jr_parsed.get("correctness_score", None)
116
- elif isinstance(jr, dict):
117
- score = jr.get("correctness_score", None)
118
- else:
119
- score = val_parsed.get("correctness_score", None)
120
- except (json.JSONDecodeError, AttributeError, TypeError) as e:
121
- parse_errors += 1
122
- continue
123
-
124
- if score is None:
125
- no_score += 1
126
- continue
127
-
128
- model_total_counts[model_col] += 1
129
- succeeded = float(score) >= CORRECTNESS_THRESHOLD
130
- if succeeded:
131
- model_success_counts[model_col] += 1
132
- if not found_success or tier < best_tier:
133
- best_tier = tier
134
- found_success = True
135
-
136
- if not found_success:
137
- all_failed += 1
138
- best_tier = 5
139
-
140
- texts.append(prompt[:2000])
141
- labels.append(best_tier - 1) # 0-indexed
142
- label_counter[best_tier] += 1
143
-
144
- print(f"\n[3a] Label extraction complete:")
145
- print(f" Valid samples: {len(texts)}")
146
- print(f" All-failed: {all_failed}")
147
- print(f" Parse errors: {parse_errors}")
148
- print(f" Missing scores: {no_score}")
149
- print(f" Label distribution (tier → 0-idx):")
150
- for tier in range(1, 6):
151
- c = label_counter.get(tier, 0)
152
- print(f" Tier {tier}: {c} ({c/max(len(texts),1)*100:.1f}%)")
153
-
154
- print(f"\n[3b] Per-model success rates (@ threshold {CORRECTNESS_THRESHOLD}):")
155
- for model_col in SPROUT_MODEL_COLS:
156
- total = model_total_counts[model_col]
157
- if total > 0:
158
- succ = model_success_counts[model_col]
159
- print(f" {model_col:<50}: {succ}/{total} = {succ/total*100:.1f}%")
160
-
161
- # ── 4. Tokenize ───────────────────────────────────────────────────
162
- print(f"\n[4] Tokenizing {len(texts)} samples with distilbert-base-uncased...")
163
- tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
164
-
165
- encodings = tokenizer(
166
- texts, truncation=True, max_length=MAX_SEQ_LEN, padding=False
167
- )
168
-
169
- class TierDataset(torch.utils.data.Dataset):
170
- def __init__(self, encodings, labels):
171
- self.encodings = encodings
172
- self.labels = labels
173
- def __getitem__(self, idx):
174
- item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
175
- item["labels"] = torch.tensor(self.labels[idx])
176
- return item
177
- def __len__(self):
178
- return len(self.labels)
179
-
180
- # Split
181
- indices = list(range(len(texts)))
182
- train_idx, eval_idx = train_test_split(indices, test_size=0.1, random_state=42)
183
-
184
- train_enc = {k: [v[i] for i in train_idx] for k, v in encodings.items()}
185
- eval_enc = {k: [v[i] for i in eval_idx] for k, v in encodings.items()}
186
- train_labels = [labels[i] for i in train_idx]
187
- eval_labels = [labels[i] for i in eval_idx]
188
-
189
- train_ds = TierDataset(train_enc, train_labels)
190
- eval_ds = TierDataset(eval_enc, eval_labels)
191
- print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
192
-
193
- # ── 5. Train ──────────────────────────────────────────────────────
194
- print("\n[5] Training 5-class DistilBERT router...")
195
-
196
- model = AutoModelForSequenceClassification.from_pretrained(
197
- "distilbert-base-uncased", num_labels=5
198
- )
199
-
200
- training_args = TrainingArguments(
201
- output_dir="/tmp/bert_5class_v2",
202
- num_train_epochs=EPOCHS,
203
- per_device_train_batch_size=BATCH_SIZE,
204
- per_device_eval_batch_size=BATCH_SIZE * 2,
205
- learning_rate=LR,
206
- weight_decay=0.01,
207
- warmup_ratio=0.1,
208
- eval_strategy="epoch",
209
- save_strategy="epoch",
210
- load_best_model_at_end=True,
211
- metric_for_best_model="eval_accuracy",
212
- logging_steps=100,
213
- logging_first_step=True,
214
- disable_tqdm=True,
215
- report_to="none",
216
- )
217
-
218
- def compute_metrics(eval_pred):
219
- logits, labels = eval_pred
220
- preds = np.argmax(logits, axis=-1)
221
- acc = np.mean(preds == labels).item()
222
-
223
- # Per-class accuracy
224
- per_class = {}
225
- for c in range(5):
226
- mask = labels == c
227
- if mask.sum() > 0:
228
- per_class[f"acc_tier{c+1}"] = np.mean(preds[mask] == labels[mask]).item()
229
-
230
- off_by_one = np.mean(np.abs(preds - labels) <= 1).item()
231
-
232
- return {"accuracy": acc, "off_by_one": off_by_one, **per_class}
233
-
234
- trainer = Trainer(
235
- model=model,
236
- args=training_args,
237
- train_dataset=train_ds,
238
- eval_dataset=eval_ds,
239
- tokenizer=tokenizer,
240
- data_collator=DataCollatorWithPadding(tokenizer),
241
- compute_metrics=compute_metrics,
242
- )
243
-
244
- result = trainer.train()
245
- final_metrics = trainer.evaluate()
246
- print(f"\n Final metrics: {final_metrics}")
247
-
248
- # ── 6. Save and upload ────────────────────────────────────────────
249
- print("\n[6] Saving model...")
250
- save_dir = "/tmp/bert_5class_v2_final"
251
- model.save_pretrained(save_dir)
252
- tokenizer.save_pretrained(save_dir)
253
-
254
- meta = {
255
- "model": "distilbert-base-uncased",
256
- "num_labels": 5,
257
- "task": "5-class optimal tier prediction",
258
- "training_dataset": "CARROT-LLM-Routing/SPROUT",
259
- "n_training_samples": len(train_ds),
260
- "correctness_threshold": CORRECTNESS_THRESHOLD,
261
- "tier_mapping": SPROUT_MODEL_TO_TIER,
262
- "max_seq_len": MAX_SEQ_LEN,
263
- "epochs": EPOCHS,
264
- "learning_rate": LR,
265
- "final_metrics": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
266
- for k, v in final_metrics.items()},
267
- "label_distribution": dict(label_counter),
268
- }
269
- with open(os.path.join(save_dir, "metadata.json"), "w") as f:
270
- json.dump(meta, f, indent=2)
271
- print(f" Saved to {save_dir}")
272
-
273
- # Upload
274
- from huggingface_hub import HfApi
275
-
276
- api = HfApi()
277
- for fname in os.listdir(save_dir):
278
- fpath = os.path.join(save_dir, fname)
279
- if os.path.isfile(fpath):
280
- api.upload_file(
281
- path_or_fileobj=fpath,
282
- path_in_repo=f"router_models/bert_5class/{fname}",
283
- repo_id=REPO,
284
- repo_type="model",
285
- )
286
- print(f" Uploaded {fname}")
287
-
288
- print("\n" + "="*70)
289
- print("DONE! BERT 5-class router saved to router_models/bert_5class/")
290
- print(f"Repo: https://huggingface.co/{REPO}")
291
- print("="*70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_bert_5class_v3.py DELETED
@@ -1,292 +0,0 @@
1
- #!/usr/bin/env python3
2
- """BERT 5-Class Tier Router — V3 with ROBUST SPROUT parsing.
3
-
4
- Fixes V2 issues:
5
- 1. Handle escaped single-quotes in judge_response JSON (Model\'s → Model's)
6
- 2. Add accelerate dependency
7
- 3. More robust fallback parsing for edge cases
8
- """
9
- import json, os, sys, re
10
- import numpy as np
11
- from collections import Counter
12
- from datasets import load_dataset
13
- import torch
14
- from transformers import (
15
- AutoTokenizer,
16
- AutoModelForSequenceClassification,
17
- TrainingArguments,
18
- Trainer,
19
- DataCollatorWithPadding,
20
- )
21
- from sklearn.model_selection import train_test_split
22
-
23
- REPO = "narcolepticchicken/agent-cost-optimizer"
24
- CORRECTNESS_THRESHOLD = 0.7
25
- MAX_SEQ_LEN = 512
26
- BATCH_SIZE = 32
27
- EPOCHS = 5
28
- LR = 2e-5
29
-
30
- print("="*70)
31
- print("BERT 5-CLASS TIER ROUTER — V3 (Robust SPROUT Parsing)")
32
- print("="*70)
33
-
34
- # ── 1. Map SPROUT models → ACO cost tiers ──────────────────────────
35
- SPROUT_MODEL_TO_TIER = {
36
- "wxai-granite-3-2b-instruct-8k-max-tokens": 1,
37
- "wxai-llama-3-2-1b-instruct": 1,
38
- "wxai-llama-3-2-3b-instruct": 1,
39
- "wxai-llama-3-1-8b-instruct": 2,
40
- "wxai-granite-3-8b-instruct-8k-max-tokens": 2,
41
- "openai-gpt-4o-mini": 2,
42
- "wxai-mixtral-8x7b-instruct-v01": 3,
43
- "aws-titan-text-premier-v1": 3,
44
- "openai-gpt-4o": 4,
45
- "wxai-llama-3-1-70b-instruct": 4,
46
- "wxai-llama-3-3-70b-instruct": 4,
47
- "aws-claude-3-5-sonnet-v1": 4,
48
- "wxai-llama-3-405b-instruct": 5,
49
- }
50
- SPROUT_MODEL_COLS = list(SPROUT_MODEL_TO_TIER.keys())
51
-
52
- print(f"\n[1] SPROUT model mapping: {len(SPROUT_MODEL_COLS)} models → 5 ACO tiers")
53
-
54
- # ── 2. Robust JSON parsing helper ────────────────────────────────
55
- def extract_correctness_score(val):
56
- """Robustly extract correctness_score from SPROUT's nested dict/JSON structure."""
57
- if val is None:
58
- return None
59
-
60
- judge_raw = None
61
- if isinstance(val, dict):
62
- judge_raw = val.get("judge_response")
63
- elif isinstance(val, str):
64
- try:
65
- parsed = json.loads(val)
66
- if isinstance(parsed, dict):
67
- judge_raw = parsed.get("judge_response")
68
- else:
69
- return None
70
- except json.JSONDecodeError:
71
- return None
72
- else:
73
- return None
74
-
75
- if judge_raw is None:
76
- return None
77
- if isinstance(judge_raw, (int, float)):
78
- return float(judge_raw)
79
- if isinstance(judge_raw, dict):
80
- score = judge_raw.get("correctness_score")
81
- return float(score) if score is not None else None
82
- if not isinstance(judge_raw, str) or not judge_raw.strip():
83
- return None
84
-
85
- # Normal JSON parse
86
- try:
87
- parsed = json.loads(judge_raw)
88
- score = parsed.get("correctness_score")
89
- return float(score) if score is not None else None
90
- except json.JSONDecodeError:
91
- pass
92
-
93
- # Fix common escape issues
94
- cleaned = judge_raw.replace("\\'", "'")
95
- try:
96
- parsed = json.loads(cleaned)
97
- score = parsed.get("correctness_score")
98
- return float(score) if score is not None else None
99
- except json.JSONDecodeError:
100
- pass
101
-
102
- # Regex fallback
103
- m = re.search(r'"correctness_score"\s*:\s*([\d.]+)', judge_raw)
104
- if m:
105
- try:
106
- return float(m.group(1))
107
- except ValueError:
108
- pass
109
- return None
110
-
111
- # ── 3. Load SPROUT & extract labels ──────────────────────────────
112
- print("\n[2] Loading SPROUT dataset...")
113
- ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train")
114
- print(f" Total rows: {len(ds)}")
115
-
116
- print("\n[3] Extracting labels with robust parsing...")
117
-
118
- texts = []
119
- labels = []
120
- parse_errors = 0
121
- all_failed = 0
122
- label_counter = Counter()
123
- model_success_counts = Counter()
124
- model_total_counts = Counter()
125
-
126
- for idx, row in enumerate(ds):
127
- prompt = str(row.get("prompt") or "")
128
- if not prompt or len(prompt) < 10:
129
- all_failed += 1
130
- continue
131
-
132
- best_tier = 5
133
- found_success = False
134
-
135
- for model_col in SPROUT_MODEL_COLS:
136
- tier = SPROUT_MODEL_TO_TIER[model_col]
137
- score = extract_correctness_score(row.get(model_col))
138
-
139
- if score is None:
140
- parse_errors += 1
141
- continue
142
-
143
- model_total_counts[model_col] += 1
144
- if score >= CORRECTNESS_THRESHOLD:
145
- model_success_counts[model_col] += 1
146
- if not found_success or tier < best_tier:
147
- best_tier = tier
148
- found_success = True
149
-
150
- if not found_success:
151
- all_failed += 1
152
- best_tier = 5
153
-
154
- texts.append(prompt[:2000])
155
- labels.append(best_tier - 1)
156
- label_counter[best_tier] += 1
157
-
158
- print(f"\n[3a] Label extraction complete:")
159
- print(f" Valid samples: {len(texts)}")
160
- print(f" All-failed: {all_failed}")
161
- print(f" Parse errors: {parse_errors}")
162
- print(f" Label distribution:")
163
- for tier in range(1, 6):
164
- c = label_counter.get(tier, 0)
165
- pct = c / max(len(texts), 1) * 100
166
- print(f" Tier {tier}: {c} ({pct:.1f}%)")
167
-
168
- print(f"\n[3b] Per-model success rates (@ threshold {CORRECTNESS_THRESHOLD}):")
169
- for model_col in SPROUT_MODEL_COLS:
170
- total = model_total_counts[model_col]
171
- if total > 0:
172
- succ = model_success_counts[model_col]
173
- print(f" {model_col:<50}: {succ}/{total} = {succ/total*100:.1f}%")
174
-
175
- # ── 4. Tokenize ───────────────────────────────────────────────────
176
- print(f"\n[4] Tokenizing {len(texts)} samples...")
177
- tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
178
- encodings = tokenizer(texts, truncation=True, max_length=MAX_SEQ_LEN, padding=False)
179
-
180
- class TierDataset(torch.utils.data.Dataset):
181
- def __init__(self, encodings, labels):
182
- self.encodings = encodings
183
- self.labels = labels
184
- def __getitem__(self, idx):
185
- item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
186
- item["labels"] = torch.tensor(self.labels[idx])
187
- return item
188
- def __len__(self):
189
- return len(self.labels)
190
-
191
- indices = list(range(len(texts)))
192
- train_idx, eval_idx = train_test_split(indices, test_size=0.1, random_state=42)
193
- train_enc = {k: [v[i] for i in train_idx] for k, v in encodings.items()}
194
- eval_enc = {k: [v[i] for i in eval_idx] for k, v in encodings.items()}
195
-
196
- train_ds = TierDataset(train_enc, [labels[i] for i in train_idx])
197
- eval_ds = TierDataset(eval_enc, [labels[i] for i in eval_idx])
198
- print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
199
-
200
- # ── 5. Train ──────────────────────────────────────────────────────
201
- print("\n[5] Training 5-class DistilBERT router...")
202
-
203
- model = AutoModelForSequenceClassification.from_pretrained(
204
- "distilbert-base-uncased", num_labels=5
205
- )
206
-
207
- training_args = TrainingArguments(
208
- output_dir="/tmp/bert_5class_v3",
209
- num_train_epochs=EPOCHS,
210
- per_device_train_batch_size=BATCH_SIZE,
211
- per_device_eval_batch_size=BATCH_SIZE * 2,
212
- learning_rate=LR,
213
- weight_decay=0.01,
214
- warmup_steps=500,
215
- eval_strategy="epoch",
216
- save_strategy="epoch",
217
- load_best_model_at_end=True,
218
- metric_for_best_model="eval_accuracy",
219
- logging_steps=100,
220
- logging_first_step=True,
221
- disable_tqdm=True,
222
- report_to="none",
223
- )
224
-
225
- def compute_metrics(eval_pred):
226
- logits, labels = eval_pred
227
- preds = np.argmax(logits, axis=-1)
228
- acc = np.mean(preds == labels).item()
229
- per_class = {}
230
- for c in range(5):
231
- mask = labels == c
232
- if mask.sum() > 0:
233
- per_class[f"acc_tier{c+1}"] = np.mean(preds[mask] == labels[mask]).item()
234
- off_by_one = np.mean(np.abs(preds - labels) <= 1).item()
235
- return {"accuracy": acc, "off_by_one": off_by_one, **per_class}
236
-
237
- trainer = Trainer(
238
- model=model,
239
- args=training_args,
240
- train_dataset=train_ds,
241
- eval_dataset=eval_ds,
242
- processing_class=tokenizer,
243
- data_collator=DataCollatorWithPadding(tokenizer),
244
- compute_metrics=compute_metrics,
245
- )
246
-
247
- result = trainer.train()
248
- final_metrics = trainer.evaluate()
249
- print(f"\n Final metrics: {final_metrics}")
250
-
251
- # ── 6. Save and upload ────────────────────────────────────────────
252
- print("\n[6] Saving model...")
253
- save_dir = "/tmp/bert_5class_v3_final"
254
- model.save_pretrained(save_dir)
255
- tokenizer.save_pretrained(save_dir)
256
-
257
- meta = {
258
- "model": "distilbert-base-uncased",
259
- "num_labels": 5,
260
- "task": "5-class optimal tier prediction",
261
- "training_dataset": "CARROT-LLM-Routing/SPROUT",
262
- "n_training_samples": len(train_ds),
263
- "correctness_threshold": CORRECTNESS_THRESHOLD,
264
- "tier_mapping": SPROUT_MODEL_TO_TIER,
265
- "max_seq_len": MAX_SEQ_LEN,
266
- "epochs": EPOCHS,
267
- "learning_rate": LR,
268
- "final_metrics": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
269
- for k, v in final_metrics.items()},
270
- "label_distribution": dict(label_counter),
271
- }
272
- with open(os.path.join(save_dir, "metadata.json"), "w") as f:
273
- json.dump(meta, f, indent=2)
274
- print(f" Saved to {save_dir}")
275
-
276
- from huggingface_hub import HfApi
277
- api = HfApi()
278
- for fname in os.listdir(save_dir):
279
- fpath = os.path.join(save_dir, fname)
280
- if os.path.isfile(fpath):
281
- api.upload_file(
282
- path_or_fileobj=fpath,
283
- path_in_repo=f"router_models/bert_5class/{fname}",
284
- repo_id=REPO,
285
- repo_type="model",
286
- )
287
- print(f" Uploaded {fname}")
288
-
289
- print("\n" + "="*70)
290
- print("DONE! BERT 5-class router saved to router_models/bert_5class/")
291
- print(f"Repo: https://huggingface.co/{REPO}")
292
- print("="*70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_router_full.py DELETED
@@ -1,433 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Train a learned model router for Agent Cost Optimizer."""
3
- import json, os, sys, random, pickle, uuid
4
- import numpy as np
5
- from datetime import datetime
6
- from collections import defaultdict
7
- from typing import Dict, List, Tuple, Any, Optional
8
-
9
- TASK_TYPES = ["quick_answer","coding","research","document_drafting",
10
- "legal_regulated","tool_heavy","retrieval_heavy",
11
- "long_horizon","unknown_ambiguous"]
12
- TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
13
-
14
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
15
- "implement","test","compile","runtime","class","module","async","thread"]
16
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
17
- RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
18
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
19
- LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
20
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
21
-
22
- TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
23
- TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
24
-
25
- TASK_TEMPLATES = {
26
- "quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
27
- "What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
28
- "What is the speed of light?","List the primary colors.","What is GDP?"],
29
- "coding":["Write a Python function to reverse a linked list.",
30
- "Fix the bug in this React component.","Refactor auth module to JWT.",
31
- "Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
32
- "Add unit tests for the payment module.","Optimize this SQL query.",
33
- "Create a REST API for user management.","Implement binary search in Rust."],
34
- "research":["Research latest transformer advances.",
35
- "Find sources comparing LoRA and full FT.",
36
- "Investigate data center climate impact.",
37
- "Survey privacy-preserving ML techniques.",
38
- "Compare reinforcement learning algorithms for robotics."],
39
- "document_drafting":["Draft project proposal for ML pipeline.",
40
- "Write email to team about deployment.","Create technical report on performance."],
41
- "legal_regulated":["Review this contract for liability clauses.",
42
- "Check GDPR compliance for data pipeline.","Draft privacy policy section.",
43
- "Verify regulatory compliance for medical device software."],
44
- "tool_heavy":["Search open issues and create summary.",
45
- "Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
46
- "retrieval_heavy":["Answer based on 50-page document.",
47
- "Find all payment processing mentions.","Retrieve relevant cases for legal query."],
48
- "long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
49
- "Redesign data architecture end-to-end.","Migrate monolith to microservices."],
50
- "unknown_ambiguous":["Help me with this thing.",
51
- "I need something about the server.","Can you look into that issue?"],
52
- }
53
-
54
- def tsp(tier, diff):
55
- return TIER_STR[tier] ** (diff * 0.6)
56
-
57
- def extract_features(request, task_type, difficulty=3):
58
- r = request.lower()
59
- f = {
60
- "req_len": len(request),
61
- "num_words": len(request.split()),
62
- "has_code": int(any(k in r for k in CODE_KW)),
63
- "n_code": sum(1 for k in CODE_KW if k in r),
64
- "has_legal": int(any(k in r for k in LEGAL_KW)),
65
- "n_legal": sum(1 for k in LEGAL_KW if k in r),
66
- "has_research": int(any(k in r for k in RESEARCH_KW)),
67
- "n_research": sum(1 for k in RESEARCH_KW if k in r),
68
- "has_tool": int(any(k in r for k in TOOL_KW)),
69
- "n_tool": sum(1 for k in TOOL_KW if k in r),
70
- "has_long": int(any(k in r for k in LONG_KW)),
71
- "has_math": int(any(k in r for k in MATH_KW)),
72
- "tt_idx": TT2IDX.get(task_type, 8),
73
- "difficulty": difficulty,
74
- }
75
- for tt in TASK_TYPES:
76
- f[f"tt_{tt}"] = int(task_type == tt)
77
- return f
78
-
79
- def gen_trace(idx, rng):
80
- tt = rng.choice(list(TASK_TEMPLATES.keys()))
81
- diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
82
- "research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
83
- tier_out = {}
84
- for t in range(1,6):
85
- tier_out[t] = rng.random() < tsp(t, diff)
86
- opt = 5
87
- for t in range(1,6):
88
- if tier_out[t]:
89
- opt = t
90
- break
91
- if diff <= 2:
92
- actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
93
- elif diff == 3:
94
- actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
95
- elif diff == 4:
96
- actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
97
- else:
98
- actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
99
- outcome = "success" if tier_out[actual] else "failure"
100
- req = rng.choice(TASK_TEMPLATES[tt])
101
- feats = extract_features(req, tt, diff)
102
- return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
103
- "tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
104
-
105
- print("="*80)
106
- print("AGENT COST OPTIMIZER - TRAINED ROUTER TRAINING")
107
- print("="*80)
108
-
109
- # ─── Generate Training Data ────────────────────────────────────────
110
- print("\n[1] Generating 50K training traces...")
111
- rng = random.Random(42)
112
- traces = [gen_trace(i, rng) for i in range(50000)]
113
- print(f" Generated {len(traces)} traces")
114
-
115
- opt_dist = defaultdict(int)
116
- for t in traces:
117
- opt_dist[t["opt"]] += 1
118
- for k in sorted(opt_dist):
119
- print(f" opt_tier={k}: {opt_dist[k]} ({opt_dist[k]/len(traces)*100:.1f}%)")
120
-
121
- # ─── Build Feature Matrix ──────────────────────────────────────────
122
- print("\n[2] Building feature matrix...")
123
-
124
- def feats_to_vec(feats):
125
- """Convert feature dict to fixed-order numpy array."""
126
- keys = sorted(feats.keys())
127
- return np.array([float(feats[k]) for k in keys], dtype=np.float32)
128
-
129
- # Get feature key order from first trace
130
- FEAT_KEYS = sorted(traces[0]["feats"].keys())
131
- NUM_FEATURES = len(FEAT_KEYS)
132
- print(f" Feature count: {NUM_FEATURES}")
133
- print(f" Features: {FEAT_KEYS}")
134
-
135
- def feats_to_vec_safe(feats):
136
- return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
137
-
138
- X_all = np.array([feats_to_vec_safe(t["feats"]) for t in traces])
139
- y_opt = np.array([t["opt"] for t in traces])
140
- y_actual = np.array([t["actual"] for t in traces])
141
- y_outcome = np.array([1 if t["outcome"]=="success" else 0 for t in traces])
142
-
143
- # ─── Per-Tier Success Classifiers ───────────────────────────────────
144
- print("\n[3] Training per-tier P(success|query) classifiers...")
145
- from sklearn.model_selection import train_test_split
146
- from sklearn.metrics import classification_report, accuracy_score, f1_score
147
-
148
- # For each tier, create binary label: did this tier succeed?
149
- per_tier_labels = {}
150
- for tier in range(1, 6):
151
- labels = []
152
- for t in traces:
153
- labels.append(1 if t["tier_out"].get(tier, False) else 0)
154
- per_tier_labels[tier] = np.array(labels)
155
- succ_rate = per_tier_labels[tier].mean()
156
- print(f" Tier {tier}: success rate = {succ_rate:.3f}")
157
-
158
- # Split train/test
159
- X_train, X_test, idx_train, idx_test = train_test_split(
160
- X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
161
- )
162
-
163
- print(f"\n Train: {len(X_train)}, Test: {len(X_test)}")
164
-
165
- # ─── XGBoost Per-Tier Classifiers ───────────────────────────────────
166
- print("\n[4] Training XGBoost per-tier classifiers...")
167
- import xgboost as xgb
168
-
169
- tier_classifiers = {}
170
- for tier in range(1, 6):
171
- y_train_tier = per_tier_labels[tier][idx_train]
172
- y_test_tier = per_tier_labels[tier][idx_test]
173
-
174
- clf = xgb.XGBClassifier(
175
- n_estimators=100,
176
- max_depth=4,
177
- learning_rate=0.1,
178
- subsample=0.8,
179
- colsample_bytree=0.8,
180
- objective="binary:logistic",
181
- eval_metric="logloss",
182
- random_state=42,
183
- verbosity=0,
184
- )
185
- clf.fit(X_train, y_train_tier)
186
-
187
- y_pred = clf.predict(X_test)
188
- y_prob = clf.predict_proba(X_test)[:, 1]
189
-
190
- acc = accuracy_score(y_test_tier, y_pred)
191
- f1 = f1_score(y_test_tier, y_pred, zero_division=0)
192
-
193
- tier_classifiers[tier] = clf
194
- print(f" Tier {tier}: accuracy={acc:.3f}, f1={f1:.3f}")
195
-
196
- # ─── CARROT-Style Router Decision ────────────────────────────────────
197
- print("\n[5] Building CARROT-style router...")
198
-
199
- def route_carrot(features_vec, tier_clfs, mu=0.7):
200
- """Route to argmin_tier [mu*(1-P_success_tier) + (1-mu)*cost_tier].
201
-
202
- mu controls quality-vs-cost tradeoff:
203
- mu=1.0: maximize quality only (always frontier)
204
- mu=0.0: minimize cost only (always cheapest)
205
- mu=0.7: 70% quality, 30% cost (our default)
206
- """
207
- if features_vec.ndim == 1:
208
- features_vec = features_vec.reshape(1, -1)
209
-
210
- best_tier = 3
211
- best_score = float("inf")
212
-
213
- for tier in range(1, 6):
214
- p_success = tier_clfs[tier].predict_proba(features_vec)[0, 1]
215
- cost_norm = TIER_COST[tier] / TIER_COST[5] # normalize to [0,1]
216
- score = mu * (1.0 - p_success) + (1.0 - mu) * cost_norm
217
- if score < best_score:
218
- best_score = score
219
- best_tier = tier
220
-
221
- return best_tier
222
-
223
- # Evaluate on test set
224
- print("\n[6] Evaluating CARROT router on test set...")
225
-
226
- mu_values = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
227
-
228
- for mu in mu_values:
229
- correct = 0
230
- total_cost = 0.0
231
- unsafe_misses = 0
232
- for i in idx_test:
233
- t = traces[i]
234
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
235
- pred_tier = route_carrot(x, tier_classifiers, mu=mu)
236
- opt_tier = t["opt"]
237
-
238
- # Check if predicted tier would succeed
239
- would_succeed = t["tier_out"].get(pred_tier, False)
240
- if would_succeed:
241
- correct += 1
242
-
243
- # Cost of predicted tier
244
- total_cost += TIER_COST[pred_tier]
245
-
246
- # Unsafe miss: predicted cheap tier for hard task
247
- if pred_tier < opt_tier and not would_succeed:
248
- unsafe_misses += 1
249
-
250
- n_test = len(idx_test)
251
- success_rate = correct / n_test
252
- avg_cost = total_cost / n_test
253
- unsafe_rate = unsafe_misses / n_test
254
-
255
- # Compare to heuristic (task-type based)
256
- heuristic_correct = 0
257
- heuristic_cost = 0.0
258
- for i in idx_test:
259
- t = traces[i]
260
- # Heuristic: route by task type (from classifier.py)
261
- tt = t["tt"]
262
- diff = t["diff"]
263
- h_tier = min(diff + 1, 5) # simple: difficulty + 1
264
- if t["tier_out"].get(h_tier, False):
265
- heuristic_correct += 1
266
- heuristic_cost += TIER_COST[h_tier]
267
-
268
- h_success = heuristic_correct / n_test
269
- h_avg_cost = heuristic_cost / n_test
270
-
271
- # Frontier baseline
272
- frontier_correct = sum(1 for i in idx_test if traces[i]["tier_out"].get(4, False) or traces[i]["tier_out"].get(5, False))
273
- frontier_rate = frontier_correct / n_test
274
- frontier_avg_cost = TIER_COST[4] # always tier 4
275
-
276
- print(f"\n mu={mu:.1f}:")
277
- print(f" CARROT: success={success_rate:.3f}, avg_cost={avg_cost:.4f}, unsafe_miss={unsafe_rate:.3f}")
278
- print(f" Heuristic: success={h_success:.3f}, avg_cost={h_avg_cost:.4f}")
279
- print(f" Frontier: success={frontier_rate:.3f}, avg_cost={frontier_avg_cost:.4f}")
280
- print(f" Cost reduction vs frontier: {(1-avg_cost/frontier_avg_cost)*100:.1f}%")
281
- print(f" Cost reduction vs heuristic: {(1-avg_cost/h_avg_cost)*100:.1f}%")
282
-
283
- # ─── XGBoost Direct Optimal-Tier Classifier ─────────────────────────
284
- print("\n\n[7] Training XGBoost direct optimal-tier classifier...")
285
-
286
- y_train_opt = y_opt[idx_train] - 1 # XGB needs 0-indexed
287
- y_test_opt = y_opt[idx_test] - 1
288
-
289
- direct_clf = xgb.XGBClassifier(
290
- n_estimators=200,
291
- max_depth=5,
292
- learning_rate=0.1,
293
- subsample=0.8,
294
- colsample_bytree=0.8,
295
- objective="multi:softmax",
296
- num_class=6,
297
- eval_metric="mlogloss",
298
- random_state=42,
299
- verbosity=0,
300
- )
301
- direct_clf.fit(X_train, y_train_opt)
302
-
303
- y_pred_opt = direct_clf.predict(X_test)
304
- acc_direct = accuracy_score(y_test_opt, y_pred_opt)
305
- print(f" Direct classifier accuracy: {acc_direct:.3f}")
306
-
307
- # Detailed classification report
308
- from sklearn.metrics import confusion_matrix
309
- cm = confusion_matrix(y_test_opt, y_pred_opt, labels=[1,2,3,4,5])
310
- print(f"\n Confusion Matrix (rows=true, cols=predicted):")
311
- print(f" {'':>10} {'T1':>6} {'T2':>6} {'T3':>6} {'T4':>6} {'T5':>6}")
312
- for i, tier in enumerate([1,2,3,4,5]):
313
- row = f" True T{tier:>1}:"
314
- for j in range(5):
315
- row += f" {cm[i][j]:>6}"
316
- print(row)
317
-
318
- # Evaluate direct classifier on test set
319
- print("\n[8] Evaluating direct optimal-tier classifier...")
320
- direct_correct = 0
321
- direct_cost = 0.0
322
- direct_unsafe = 0
323
- direct_underkill = 0
324
- direct_overkill = 0
325
-
326
- for i, test_idx in enumerate(idx_test):
327
- t = traces[test_idx]
328
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
329
- pred_tier = int(direct_clf.predict(x)[0]) + 1 # back to 1-indexed
330
- opt_tier = t["opt"]
331
-
332
- would_succeed = t["tier_out"].get(pred_tier, False)
333
- if would_succeed:
334
- direct_correct += 1
335
- direct_cost += TIER_COST[pred_tier]
336
-
337
- if pred_tier < opt_tier:
338
- direct_underkill += 1
339
- if not would_succeed:
340
- direct_unsafe += 1
341
- elif pred_tier > opt_tier:
342
- direct_overkill += 1
343
-
344
- n_test = len(idx_test)
345
- print(f" Success rate: {direct_correct/n_test:.3f}")
346
- print(f" Avg cost: {direct_cost/n_test:.4f}")
347
- print(f" Underkill (pred < optimal): {direct_underkill/n_test:.3f}")
348
- print(f" Overkill (pred > optimal): {direct_overkill/n_test:.3f}")
349
- print(f" Unsafe misses: {direct_unsafe/n_test:.3f}")
350
-
351
- # ─── Comparison: All Routers ────────────────────────────────────────
352
- print("\n\n" + "="*80)
353
- print("FINAL COMPARISON: ALL ROUTERS ON TEST SET")
354
- print("="*80)
355
-
356
- # 1. Always frontier
357
- f_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(4,False) or traces[i]["tier_out"].get(5,False))
358
- f_cost = TIER_COST[4] * n_test
359
-
360
- # 2. Always cheapest
361
- c_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(1,False) or traces[i]["tier_out"].get(2,False))
362
- c_cost = TIER_COST[1] * n_test
363
-
364
- # 3. Heuristic (difficulty + 1)
365
- h_succ = 0; h_cost = 0.0
366
- for i in idx_test:
367
- t = traces[i]
368
- h_tier = min(t["diff"] + 1, 5)
369
- if t["tier_out"].get(h_tier, False): h_succ += 1
370
- h_cost += TIER_COST[h_tier]
371
-
372
- # 4. CARROT (best mu)
373
- best_mu = 0.7
374
- carrot_succ = 0; carrot_cost = 0.0; carrot_unsafe = 0
375
- for i in idx_test:
376
- t = traces[i]
377
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
378
- pred = route_carrot(x, tier_classifiers, mu=best_mu)
379
- if t["tier_out"].get(pred, False): carrot_succ += 1
380
- carrot_cost += TIER_COST[pred]
381
- if pred < t["opt"] and not t["tier_out"].get(pred, False):
382
- carrot_unsafe += 1
383
-
384
- # 5. Direct XGB
385
- d_succ = direct_correct
386
- d_cost = direct_cost
387
- d_unsafe = direct_unsafe
388
-
389
- # 6. Oracle (always picks optimal)
390
- o_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(traces[i]["opt"], False))
391
- o_cost = sum(TIER_COST[traces[i]["opt"]] for i in idx_test)
392
-
393
- print(f"\n{'Router':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10}")
394
- print("-"*60)
395
- for name, succ, cost, unsafe in [
396
- ("always_frontier", f_succ, f_cost, 0),
397
- ("always_cheap", c_succ, c_cost, 0),
398
- ("heuristic", h_succ, h_cost, 0),
399
- (f"CARROT(mu={best_mu})", carrot_succ, carrot_cost, carrot_unsafe),
400
- ("direct_xgb", d_succ, d_cost, d_unsafe),
401
- ("oracle", o_succ, o_cost, 0),
402
- ]:
403
- sr = succ/n_test
404
- ac = cost/n_test
405
- cr = (1 - cost/f_cost)*100
406
- um = unsafe/n_test
407
- print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}% {um:>10.3f}")
408
-
409
- # ─── Save Models ────────────────────────────────────────────────────
410
- print("\n\n[9] Saving models...")
411
- os.makedirs("/app/router_models", exist_ok=True)
412
-
413
- # Save per-tier classifiers
414
- for tier, clf in tier_classifiers.items():
415
- clf.save_model(f"/app/router_models/tier_{tier}_success.json")
416
- print(f" Saved tier_{tier}_success.json")
417
-
418
- # Save direct classifier
419
- direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
420
- print(f" Saved direct_optimal_tier.json")
421
-
422
- # Save feature keys
423
- with open("/app/router_models/feat_keys.json", "w") as f:
424
- json.dump(FEAT_KEYS, f)
425
- print(f" Saved feat_keys.json ({len(FEAT_KEYS)} features)")
426
-
427
- # Save tier config
428
- with open("/app/router_models/tier_config.json", "w") as f:
429
- json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR}, f)
430
- print(f" Saved tier_config.json")
431
-
432
- print("\n\nDONE! Models saved to /app/router_models/")
433
- print("Next step: integrate trained router into ACO ModelCascadeRouter._route_learned()")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_router_v1.py DELETED
@@ -1,433 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Train a learned model router for Agent Cost Optimizer."""
3
- import json, os, sys, random, pickle, uuid
4
- import numpy as np
5
- from datetime import datetime
6
- from collections import defaultdict
7
- from typing import Dict, List, Tuple, Any, Optional
8
-
9
- TASK_TYPES = ["quick_answer","coding","research","document_drafting",
10
- "legal_regulated","tool_heavy","retrieval_heavy",
11
- "long_horizon","unknown_ambiguous"]
12
- TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
13
-
14
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
15
- "implement","test","compile","runtime","class","module","async","thread"]
16
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
17
- RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
18
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
19
- LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
20
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
21
-
22
- TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
23
- TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
24
-
25
- TASK_TEMPLATES = {
26
- "quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
27
- "What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
28
- "What is the speed of light?","List the primary colors.","What is GDP?"],
29
- "coding":["Write a Python function to reverse a linked list.",
30
- "Fix the bug in this React component.","Refactor auth module to JWT.",
31
- "Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
32
- "Add unit tests for the payment module.","Optimize this SQL query.",
33
- "Create a REST API for user management.","Implement binary search in Rust."],
34
- "research":["Research latest transformer advances.",
35
- "Find sources comparing LoRA and full FT.",
36
- "Investigate data center climate impact.",
37
- "Survey privacy-preserving ML techniques.",
38
- "Compare reinforcement learning algorithms for robotics."],
39
- "document_drafting":["Draft project proposal for ML pipeline.",
40
- "Write email to team about deployment.","Create technical report on performance."],
41
- "legal_regulated":["Review this contract for liability clauses.",
42
- "Check GDPR compliance for data pipeline.","Draft privacy policy section.",
43
- "Verify regulatory compliance for medical device software."],
44
- "tool_heavy":["Search open issues and create summary.",
45
- "Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
46
- "retrieval_heavy":["Answer based on 50-page document.",
47
- "Find all payment processing mentions.","Retrieve relevant cases for legal query."],
48
- "long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
49
- "Redesign data architecture end-to-end.","Migrate monolith to microservices."],
50
- "unknown_ambiguous":["Help me with this thing.",
51
- "I need something about the server.","Can you look into that issue?"],
52
- }
53
-
54
- def tsp(tier, diff):
55
- return TIER_STR[tier] ** (diff * 0.6)
56
-
57
- def extract_features(request, task_type, difficulty=3):
58
- r = request.lower()
59
- f = {
60
- "req_len": len(request),
61
- "num_words": len(request.split()),
62
- "has_code": int(any(k in r for k in CODE_KW)),
63
- "n_code": sum(1 for k in CODE_KW if k in r),
64
- "has_legal": int(any(k in r for k in LEGAL_KW)),
65
- "n_legal": sum(1 for k in LEGAL_KW if k in r),
66
- "has_research": int(any(k in r for k in RESEARCH_KW)),
67
- "n_research": sum(1 for k in RESEARCH_KW if k in r),
68
- "has_tool": int(any(k in r for k in TOOL_KW)),
69
- "n_tool": sum(1 for k in TOOL_KW if k in r),
70
- "has_long": int(any(k in r for k in LONG_KW)),
71
- "has_math": int(any(k in r for k in MATH_KW)),
72
- "tt_idx": TT2IDX.get(task_type, 8),
73
- "difficulty": difficulty,
74
- }
75
- for tt in TASK_TYPES:
76
- f[f"tt_{tt}"] = int(task_type == tt)
77
- return f
78
-
79
- def gen_trace(idx, rng):
80
- tt = rng.choice(list(TASK_TEMPLATES.keys()))
81
- diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
82
- "research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
83
- tier_out = {}
84
- for t in range(1,6):
85
- tier_out[t] = rng.random() < tsp(t, diff)
86
- opt = 5
87
- for t in range(1,6):
88
- if tier_out[t]:
89
- opt = t
90
- break
91
- if diff <= 2:
92
- actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
93
- elif diff == 3:
94
- actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
95
- elif diff == 4:
96
- actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
97
- else:
98
- actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
99
- outcome = "success" if tier_out[actual] else "failure"
100
- req = rng.choice(TASK_TEMPLATES[tt])
101
- feats = extract_features(req, tt, diff)
102
- return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
103
- "tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
104
-
105
- print("="*80)
106
- print("AGENT COST OPTIMIZER - TRAINED ROUTER TRAINING")
107
- print("="*80)
108
-
109
- # ─── Generate Training Data ────────────────────────────────────────
110
- print("\n[1] Generating 50K training traces...")
111
- rng = random.Random(42)
112
- traces = [gen_trace(i, rng) for i in range(50000)]
113
- print(f" Generated {len(traces)} traces")
114
-
115
- opt_dist = defaultdict(int)
116
- for t in traces:
117
- opt_dist[t["opt"]] += 1
118
- for k in sorted(opt_dist):
119
- print(f" opt_tier={k}: {opt_dist[k]} ({opt_dist[k]/len(traces)*100:.1f}%)")
120
-
121
- # ─── Build Feature Matrix ──────────────────────────────────────────
122
- print("\n[2] Building feature matrix...")
123
-
124
- def feats_to_vec(feats):
125
- """Convert feature dict to fixed-order numpy array."""
126
- keys = sorted(feats.keys())
127
- return np.array([float(feats[k]) for k in keys], dtype=np.float32)
128
-
129
- # Get feature key order from first trace
130
- FEAT_KEYS = sorted(traces[0]["feats"].keys())
131
- NUM_FEATURES = len(FEAT_KEYS)
132
- print(f" Feature count: {NUM_FEATURES}")
133
- print(f" Features: {FEAT_KEYS}")
134
-
135
- def feats_to_vec_safe(feats):
136
- return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
137
-
138
- X_all = np.array([feats_to_vec_safe(t["feats"]) for t in traces])
139
- y_opt = np.array([t["opt"] for t in traces])
140
- y_actual = np.array([t["actual"] for t in traces])
141
- y_outcome = np.array([1 if t["outcome"]=="success" else 0 for t in traces])
142
-
143
- # ─── Per-Tier Success Classifiers ───────────────────────────────────
144
- print("\n[3] Training per-tier P(success|query) classifiers...")
145
- from sklearn.model_selection import train_test_split
146
- from sklearn.metrics import classification_report, accuracy_score, f1_score
147
-
148
- # For each tier, create binary label: did this tier succeed?
149
- per_tier_labels = {}
150
- for tier in range(1, 6):
151
- labels = []
152
- for t in traces:
153
- labels.append(1 if t["tier_out"].get(tier, False) else 0)
154
- per_tier_labels[tier] = np.array(labels)
155
- succ_rate = per_tier_labels[tier].mean()
156
- print(f" Tier {tier}: success rate = {succ_rate:.3f}")
157
-
158
- # Split train/test
159
- X_train, X_test, idx_train, idx_test = train_test_split(
160
- X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
161
- )
162
-
163
- print(f"\n Train: {len(X_train)}, Test: {len(X_test)}")
164
-
165
- # ─── XGBoost Per-Tier Classifiers ───────────────────────────────────
166
- print("\n[4] Training XGBoost per-tier classifiers...")
167
- import xgboost as xgb
168
-
169
- tier_classifiers = {}
170
- for tier in range(1, 6):
171
- y_train_tier = per_tier_labels[tier][idx_train]
172
- y_test_tier = per_tier_labels[tier][idx_test]
173
-
174
- clf = xgb.XGBClassifier(
175
- n_estimators=100,
176
- max_depth=4,
177
- learning_rate=0.1,
178
- subsample=0.8,
179
- colsample_bytree=0.8,
180
- objective="binary:logistic",
181
- eval_metric="logloss",
182
- random_state=42,
183
- verbosity=0,
184
- )
185
- clf.fit(X_train, y_train_tier)
186
-
187
- y_pred = clf.predict(X_test)
188
- y_prob = clf.predict_proba(X_test)[:, 1]
189
-
190
- acc = accuracy_score(y_test_tier, y_pred)
191
- f1 = f1_score(y_test_tier, y_pred, zero_division=0)
192
-
193
- tier_classifiers[tier] = clf
194
- print(f" Tier {tier}: accuracy={acc:.3f}, f1={f1:.3f}")
195
-
196
- # ─── CARROT-Style Router Decision ────────────────────────────────────
197
- print("\n[5] Building CARROT-style router...")
198
-
199
- def route_carrot(features_vec, tier_clfs, mu=0.7):
200
- """Route to argmin_tier [mu*(1-P_success_tier) + (1-mu)*cost_tier].
201
-
202
- mu controls quality-vs-cost tradeoff:
203
- mu=1.0: maximize quality only (always frontier)
204
- mu=0.0: minimize cost only (always cheapest)
205
- mu=0.7: 70% quality, 30% cost (our default)
206
- """
207
- if features_vec.ndim == 1:
208
- features_vec = features_vec.reshape(1, -1)
209
-
210
- best_tier = 3
211
- best_score = float("inf")
212
-
213
- for tier in range(1, 6):
214
- p_success = tier_clfs[tier].predict_proba(features_vec)[0, 1]
215
- cost_norm = TIER_COST[tier] / TIER_COST[5] # normalize to [0,1]
216
- score = mu * (1.0 - p_success) + (1.0 - mu) * cost_norm
217
- if score < best_score:
218
- best_score = score
219
- best_tier = tier
220
-
221
- return best_tier
222
-
223
- # Evaluate on test set
224
- print("\n[6] Evaluating CARROT router on test set...")
225
-
226
- mu_values = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
227
-
228
- for mu in mu_values:
229
- correct = 0
230
- total_cost = 0.0
231
- unsafe_misses = 0
232
- for i in idx_test:
233
- t = traces[i]
234
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
235
- pred_tier = route_carrot(x, tier_classifiers, mu=mu)
236
- opt_tier = t["opt"]
237
-
238
- # Check if predicted tier would succeed
239
- would_succeed = t["tier_out"].get(pred_tier, False)
240
- if would_succeed:
241
- correct += 1
242
-
243
- # Cost of predicted tier
244
- total_cost += TIER_COST[pred_tier]
245
-
246
- # Unsafe miss: predicted cheap tier for hard task
247
- if pred_tier < opt_tier and not would_succeed:
248
- unsafe_misses += 1
249
-
250
- n_test = len(idx_test)
251
- success_rate = correct / n_test
252
- avg_cost = total_cost / n_test
253
- unsafe_rate = unsafe_misses / n_test
254
-
255
- # Compare to heuristic (task-type based)
256
- heuristic_correct = 0
257
- heuristic_cost = 0.0
258
- for i in idx_test:
259
- t = traces[i]
260
- # Heuristic: route by task type (from classifier.py)
261
- tt = t["tt"]
262
- diff = t["diff"]
263
- h_tier = min(diff + 1, 5) # simple: difficulty + 1
264
- if t["tier_out"].get(h_tier, False):
265
- heuristic_correct += 1
266
- heuristic_cost += TIER_COST[h_tier]
267
-
268
- h_success = heuristic_correct / n_test
269
- h_avg_cost = heuristic_cost / n_test
270
-
271
- # Frontier baseline
272
- frontier_correct = sum(1 for i in idx_test if traces[i]["tier_out"].get(4, False) or traces[i]["tier_out"].get(5, False))
273
- frontier_rate = frontier_correct / n_test
274
- frontier_avg_cost = TIER_COST[4] # always tier 4
275
-
276
- print(f"\n mu={mu:.1f}:")
277
- print(f" CARROT: success={success_rate:.3f}, avg_cost={avg_cost:.4f}, unsafe_miss={unsafe_rate:.3f}")
278
- print(f" Heuristic: success={h_success:.3f}, avg_cost={h_avg_cost:.4f}")
279
- print(f" Frontier: success={frontier_rate:.3f}, avg_cost={frontier_avg_cost:.4f}")
280
- print(f" Cost reduction vs frontier: {(1-avg_cost/frontier_avg_cost)*100:.1f}%")
281
- print(f" Cost reduction vs heuristic: {(1-avg_cost/h_avg_cost)*100:.1f}%")
282
-
283
- # ─── XGBoost Direct Optimal-Tier Classifier ─────────────────────────
284
- print("\n\n[7] Training XGBoost direct optimal-tier classifier...")
285
-
286
- y_train_opt = y_opt[idx_train] - 1 # XGB needs 0-indexed
287
- y_test_opt = y_opt[idx_test] - 1
288
-
289
- direct_clf = xgb.XGBClassifier(
290
- n_estimators=200,
291
- max_depth=5,
292
- learning_rate=0.1,
293
- subsample=0.8,
294
- colsample_bytree=0.8,
295
- objective="multi:softmax",
296
- num_class=6,
297
- eval_metric="mlogloss",
298
- random_state=42,
299
- verbosity=0,
300
- )
301
- direct_clf.fit(X_train, y_train_opt)
302
-
303
- y_pred_opt = direct_clf.predict(X_test)
304
- acc_direct = accuracy_score(y_test_opt, y_pred_opt)
305
- print(f" Direct classifier accuracy: {acc_direct:.3f}")
306
-
307
- # Detailed classification report
308
- from sklearn.metrics import confusion_matrix
309
- cm = confusion_matrix(y_test_opt, y_pred_opt, labels=[1,2,3,4,5])
310
- print(f"\n Confusion Matrix (rows=true, cols=predicted):")
311
- print(f" {'':>10} {'T1':>6} {'T2':>6} {'T3':>6} {'T4':>6} {'T5':>6}")
312
- for i, tier in enumerate([1,2,3,4,5]):
313
- row = f" True T{tier:>1}:"
314
- for j in range(5):
315
- row += f" {cm[i][j]:>6}"
316
- print(row)
317
-
318
- # Evaluate direct classifier on test set
319
- print("\n[8] Evaluating direct optimal-tier classifier...")
320
- direct_correct = 0
321
- direct_cost = 0.0
322
- direct_unsafe = 0
323
- direct_underkill = 0
324
- direct_overkill = 0
325
-
326
- for i, test_idx in enumerate(idx_test):
327
- t = traces[test_idx]
328
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
329
- pred_tier = int(direct_clf.predict(x)[0]) + 1 # back to 1-indexed
330
- opt_tier = t["opt"]
331
-
332
- would_succeed = t["tier_out"].get(pred_tier, False)
333
- if would_succeed:
334
- direct_correct += 1
335
- direct_cost += TIER_COST[pred_tier]
336
-
337
- if pred_tier < opt_tier:
338
- direct_underkill += 1
339
- if not would_succeed:
340
- direct_unsafe += 1
341
- elif pred_tier > opt_tier:
342
- direct_overkill += 1
343
-
344
- n_test = len(idx_test)
345
- print(f" Success rate: {direct_correct/n_test:.3f}")
346
- print(f" Avg cost: {direct_cost/n_test:.4f}")
347
- print(f" Underkill (pred < optimal): {direct_underkill/n_test:.3f}")
348
- print(f" Overkill (pred > optimal): {direct_overkill/n_test:.3f}")
349
- print(f" Unsafe misses: {direct_unsafe/n_test:.3f}")
350
-
351
- # ─── Comparison: All Routers ────────────────────────────────────────
352
- print("\n\n" + "="*80)
353
- print("FINAL COMPARISON: ALL ROUTERS ON TEST SET")
354
- print("="*80)
355
-
356
- # 1. Always frontier
357
- f_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(4,False) or traces[i]["tier_out"].get(5,False))
358
- f_cost = TIER_COST[4] * n_test
359
-
360
- # 2. Always cheapest
361
- c_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(1,False) or traces[i]["tier_out"].get(2,False))
362
- c_cost = TIER_COST[1] * n_test
363
-
364
- # 3. Heuristic (difficulty + 1)
365
- h_succ = 0; h_cost = 0.0
366
- for i in idx_test:
367
- t = traces[i]
368
- h_tier = min(t["diff"] + 1, 5)
369
- if t["tier_out"].get(h_tier, False): h_succ += 1
370
- h_cost += TIER_COST[h_tier]
371
-
372
- # 4. CARROT (best mu)
373
- best_mu = 0.7
374
- carrot_succ = 0; carrot_cost = 0.0; carrot_unsafe = 0
375
- for i in idx_test:
376
- t = traces[i]
377
- x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
378
- pred = route_carrot(x, tier_classifiers, mu=best_mu)
379
- if t["tier_out"].get(pred, False): carrot_succ += 1
380
- carrot_cost += TIER_COST[pred]
381
- if pred < t["opt"] and not t["tier_out"].get(pred, False):
382
- carrot_unsafe += 1
383
-
384
- # 5. Direct XGB
385
- d_succ = direct_correct
386
- d_cost = direct_cost
387
- d_unsafe = direct_unsafe
388
-
389
- # 6. Oracle (always picks optimal)
390
- o_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(traces[i]["opt"], False))
391
- o_cost = sum(TIER_COST[traces[i]["opt"]] for i in idx_test)
392
-
393
- print(f"\n{'Router':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10}")
394
- print("-"*60)
395
- for name, succ, cost, unsafe in [
396
- ("always_frontier", f_succ, f_cost, 0),
397
- ("always_cheap", c_succ, c_cost, 0),
398
- ("heuristic", h_succ, h_cost, 0),
399
- (f"CARROT(mu={best_mu})", carrot_succ, carrot_cost, carrot_unsafe),
400
- ("direct_xgb", d_succ, d_cost, d_unsafe),
401
- ("oracle", o_succ, o_cost, 0),
402
- ]:
403
- sr = succ/n_test
404
- ac = cost/n_test
405
- cr = (1 - cost/f_cost)*100
406
- um = unsafe/n_test
407
- print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}% {um:>10.3f}")
408
-
409
- # ─── Save Models ────────────────────────────────────────────────────
410
- print("\n\n[9] Saving models...")
411
- os.makedirs("/app/router_models", exist_ok=True)
412
-
413
- # Save per-tier classifiers
414
- for tier, clf in tier_classifiers.items():
415
- clf.save_model(f"/app/router_models/tier_{tier}_success.json")
416
- print(f" Saved tier_{tier}_success.json")
417
-
418
- # Save direct classifier
419
- direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
420
- print(f" Saved direct_optimal_tier.json")
421
-
422
- # Save feature keys
423
- with open("/app/router_models/feat_keys.json", "w") as f:
424
- json.dump(FEAT_KEYS, f)
425
- print(f" Saved feat_keys.json ({len(FEAT_KEYS)} features)")
426
-
427
- # Save tier config
428
- with open("/app/router_models/tier_config.json", "w") as f:
429
- json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR}, f)
430
- print(f" Saved tier_config.json")
431
-
432
- print("\n\nDONE! Models saved to /app/router_models/")
433
- print("Next step: integrate trained router into ACO ModelCascadeRouter._route_learned()")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_router_v2.py DELETED
@@ -1,510 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Trained Router v2: Safety-first CARROT with tuned mu + safety floors.
3
-
4
- Key insight from v1:
5
- - Per-tier P(success) classifiers work well individually
6
- - CARROT routing with mu=0.6 beats heuristic on both quality and cost
7
- - But success rate drops because CARROT routes cheap for hard tasks
8
-
9
- Solution: Add SAFETY FLOORS per task type:
10
- - legal_regulated: never below tier 4
11
- - coding/research with legal kw: never below tier 3
12
- - Use P(success) > threshold as gate, fallback to difficulty-based tier
13
- - When confidence is low, default to tier 3 (medium)
14
- """
15
- import json, os, sys, random, pickle, uuid
16
- import numpy as np
17
- from datetime import datetime
18
- from collections import defaultdict
19
-
20
- TASK_TYPES = ["quick_answer","coding","research","document_drafting",
21
- "legal_regulated","tool_heavy","retrieval_heavy",
22
- "long_horizon","unknown_ambiguous"]
23
- TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
24
-
25
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
26
- "implement","test","compile","runtime","class","module","async","thread"]
27
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
28
- RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
29
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
30
- LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
31
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
32
-
33
- TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
34
- TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
35
-
36
- TASK_TEMPLATES = {
37
- "quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
38
- "What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
39
- "What is the speed of light?","List the primary colors.","What is GDP?"],
40
- "coding":["Write a Python function to reverse a linked list.",
41
- "Fix the bug in this React component.","Refactor auth module to JWT.",
42
- "Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
43
- "Add unit tests for the payment module.","Optimize this SQL query.",
44
- "Create a REST API for user management.","Implement binary search in Rust."],
45
- "research":["Research latest transformer advances.",
46
- "Find sources comparing LoRA and full FT.",
47
- "Investigate data center climate impact.",
48
- "Survey privacy-preserving ML techniques.",
49
- "Compare reinforcement learning algorithms for robotics."],
50
- "document_drafting":["Draft project proposal for ML pipeline.",
51
- "Write email to team about deployment.","Create technical report on performance."],
52
- "legal_regulated":["Review this contract for liability clauses.",
53
- "Check GDPR compliance for data pipeline.","Draft privacy policy section.",
54
- "Verify regulatory compliance for medical device software."],
55
- "tool_heavy":["Search open issues and create summary.",
56
- "Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
57
- "retrieval_heavy":["Answer based on 50-page document.",
58
- "Find all payment processing mentions.","Retrieve relevant cases for legal query."],
59
- "long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
60
- "Redesign data architecture end-to-end.","Migrate monolith to microservices."],
61
- "unknown_ambiguous":["Help me with this thing.",
62
- "I need something about the server.","Can you look into that issue?"],
63
- }
64
-
65
- # Safety floors per task type
66
- TASK_FLOOR = {
67
- "legal_regulated": 4,
68
- "long_horizon": 3,
69
- "research": 3,
70
- "coding": 3,
71
- "unknown_ambiguous": 3,
72
- "quick_answer": 1,
73
- "document_drafting": 2,
74
- "tool_heavy": 2,
75
- "retrieval_heavy": 2,
76
- }
77
-
78
- def tsp(tier, diff):
79
- return TIER_STR[tier] ** (diff * 0.6)
80
-
81
- def extract_features(request, task_type, difficulty=3):
82
- r = request.lower()
83
- f = {
84
- "req_len": len(request),
85
- "num_words": len(request.split()),
86
- "has_code": int(any(k in r for k in CODE_KW)),
87
- "n_code": sum(1 for k in CODE_KW if k in r),
88
- "has_legal": int(any(k in r for k in LEGAL_KW)),
89
- "n_legal": sum(1 for k in LEGAL_KW if k in r),
90
- "has_research": int(any(k in r for k in RESEARCH_KW)),
91
- "n_research": sum(1 for k in RESEARCH_KW if k in r),
92
- "has_tool": int(any(k in r for k in TOOL_KW)),
93
- "n_tool": sum(1 for k in TOOL_KW if k in r),
94
- "has_long": int(any(k in r for k in LONG_KW)),
95
- "has_math": int(any(k in r for k in MATH_KW)),
96
- "tt_idx": TT2IDX.get(task_type, 8),
97
- "difficulty": difficulty,
98
- }
99
- for tt in TASK_TYPES:
100
- f[f"tt_{tt}"] = int(task_type == tt)
101
- return f
102
-
103
- def gen_trace(idx, rng):
104
- tt = rng.choice(list(TASK_TEMPLATES.keys()))
105
- diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
106
- "research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
107
- tier_out = {}
108
- for t in range(1,6):
109
- tier_out[t] = rng.random() < tsp(t, diff)
110
- opt = 5
111
- for t in range(1,6):
112
- if tier_out[t]:
113
- opt = t
114
- break
115
- if diff <= 2:
116
- actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
117
- elif diff == 3:
118
- actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
119
- elif diff == 4:
120
- actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
121
- else:
122
- actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
123
- outcome = "success" if tier_out[actual] else "failure"
124
- req = rng.choice(TASK_TEMPLATES[tt])
125
- feats = extract_features(req, tt, diff)
126
- return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
127
- "tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
128
-
129
- print("="*80)
130
- print("AGENT COST OPTIMIZER - TRAINED ROUTER v2 (Safety-First CARROT)")
131
- print("="*80)
132
-
133
- print("\n[1] Generating 50K training traces...")
134
- rng = random.Random(42)
135
- traces = [gen_trace(i, rng) for i in range(50000)]
136
- print(f" Generated {len(traces)} traces")
137
-
138
- # Feature matrix
139
- FEAT_KEYS = sorted(traces[0]["feats"].keys())
140
- NUM_FEATURES = len(FEAT_KEYS)
141
-
142
- def f2v(feats):
143
- return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
144
-
145
- X_all = np.array([f2v(t["feats"]) for t in traces])
146
- y_opt = np.array([t["opt"] for t in traces])
147
-
148
- # Per-tier labels
149
- per_tier_labels = {}
150
- for tier in range(1, 6):
151
- per_tier_labels[tier] = np.array([1 if t["tier_out"].get(tier, False) else 0 for t in traces])
152
-
153
- # Train/test split
154
- from sklearn.model_selection import train_test_split
155
- from sklearn.metrics import accuracy_score, f1_score
156
-
157
- X_train, X_test, idx_train, idx_test = train_test_split(
158
- X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
159
- )
160
- print(f" Train: {len(X_train)}, Test: {len(X_test)}")
161
-
162
- # ─── Train Per-Tier XGBoost Classifiers ────────────────────────────
163
- print("\n[2] Training per-tier P(success) XGBoost classifiers...")
164
- import xgboost as xgb
165
-
166
- tier_clfs = {}
167
- for tier in range(1, 6):
168
- y_tr = per_tier_labels[tier][idx_train]
169
- y_te = per_tier_labels[tier][idx_test]
170
-
171
- # Compute scale_pos_weight for imbalanced classes
172
- neg = (y_tr == 0).sum()
173
- pos = (y_tr == 1).sum()
174
- spw = neg / max(pos, 1)
175
-
176
- clf = xgb.XGBClassifier(
177
- n_estimators=150, max_depth=5, learning_rate=0.1,
178
- subsample=0.8, colsample_bytree=0.8,
179
- scale_pos_weight=min(spw, 5.0),
180
- objective="binary:logistic", eval_metric="logloss",
181
- random_state=42, verbosity=0,
182
- )
183
- clf.fit(X_train, y_tr)
184
-
185
- y_pred = clf.predict(X_test)
186
- acc = accuracy_score(y_te, y_pred)
187
- f1 = f1_score(y_te, y_pred, zero_division=0)
188
- tier_clfs[tier] = clf
189
- print(f" Tier {tier}: acc={acc:.3f}, f1={f1:.3f}, spw={spw:.2f}")
190
-
191
- # ─── Safety-First CARROT Router ─────────────────────────────────────
192
- print("\n[3] Building safety-first CARROT router...")
193
-
194
- def route_safe_carrot(features_vec, tier_clfs, task_type, mu=0.7,
195
- success_threshold=0.5, safety_floor=None):
196
- """Route with safety floors.
197
-
198
- 1. Compute P(success|tier) for each tier
199
- 2. Apply safety floor per task type
200
- 3. Pick cheapest tier where P(success) > threshold
201
- 4. If none meets threshold, escalate to next tier
202
- """
203
- if features_vec.ndim == 1:
204
- features_vec = features_vec.reshape(1, -1)
205
-
206
- floor = safety_floor or TASK_FLOOR.get(task_type, 2)
207
-
208
- # Get per-tier success probabilities
209
- p_success = {}
210
- for tier in range(1, 6):
211
- p_success[tier] = tier_clfs[tier].predict_proba(features_vec)[0, 1]
212
-
213
- # Strategy: Find cheapest tier at or above floor where P(success) > threshold
214
- for tier in range(floor, 6):
215
- if p_success[tier] >= success_threshold:
216
- return tier, p_success
217
-
218
- # Fallback: if no tier meets threshold at floor, try escalating
219
- for tier in range(floor + 1, 6):
220
- if p_success[tier] >= success_threshold * 0.8: # relaxed threshold
221
- return tier, p_success
222
-
223
- # Last resort: use CARROT scoring at floor
224
- best_tier = floor
225
- best_score = float("inf")
226
- for tier in range(floor, 6):
227
- cost_norm = TIER_COST[tier] / TIER_COST[5]
228
- score = mu * (1.0 - p_success[tier]) + (1.0 - mu) * cost_norm
229
- if score < best_score:
230
- best_score = score
231
- best_tier = tier
232
-
233
- return best_tier, p_success
234
-
235
- # ─── Evaluate ────────────────────────────────────────────────────────
236
- print("\n[4] Evaluating all routers on test set...")
237
-
238
- n_test = len(idx_test)
239
- results = {}
240
-
241
- # Helper: evaluate a router function
242
- def eval_router(name, route_fn):
243
- succ = 0; cost = 0.0; unsafe = 0; false_done = 0
244
- tier_dist = defaultdict(int)
245
- for i in idx_test:
246
- t = traces[i]
247
- x = f2v(t["feats"]).reshape(1, -1)
248
- pred, _ = route_fn(x, t)
249
- tier_dist[pred] += 1
250
- if t["tier_out"].get(pred, False):
251
- succ += 1
252
- else:
253
- if pred < t["opt"]:
254
- unsafe += 1
255
- if pred >= t["opt"] and not t["tier_out"].get(pred, False):
256
- false_done += 1
257
- cost += TIER_COST[pred]
258
- results[name] = {
259
- "success": succ/n_test, "avg_cost": cost/n_test,
260
- "unsafe_rate": unsafe/n_test, "false_done": false_done/n_test,
261
- "tier_dist": dict(tier_dist),
262
- }
263
-
264
- # 1. Always frontier
265
- eval_router("always_frontier", lambda x, t: (4, {}))
266
-
267
- # 2. Always cheapest
268
- eval_router("always_cheap", lambda x, t: (1, {}))
269
-
270
- # 3. Heuristic (difficulty + 1)
271
- eval_router("heuristic_diff+1", lambda x, t: (min(t["diff"]+1, 5), {}))
272
-
273
- # 4. Heuristic (task floor only)
274
- eval_router("heuristic_floor", lambda x, t: (TASK_FLOOR.get(t["tt"], 3), {}))
275
-
276
- # 5. CARROT v1 (no safety floors, mu=0.6)
277
- def carrot_v1(x, t):
278
- ps = {tier: tier_clfs[tier].predict_proba(x)[0,1] for tier in range(1,6)}
279
- best = 3; best_s = float("inf")
280
- for tier in range(1,6):
281
- s = 0.6*(1-ps[tier]) + 0.4*(TIER_COST[tier]/TIER_COST[5])
282
- if s < best_s: best_s = s; best = tier
283
- return best, ps
284
- eval_router("CARROT_v1_mu0.6", carrot_v1)
285
-
286
- # 6. Safety-first CARROT (mu=0.7, threshold=0.5)
287
- def safe_carrot_050(x, t):
288
- return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.5)
289
- eval_router("safe_CARROT_t0.50", safe_carrot_050)
290
-
291
- # 7. Safety-first CARROT (mu=0.7, threshold=0.6)
292
- def safe_carrot_060(x, t):
293
- return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.6)
294
- eval_router("safe_CARROT_t0.60", safe_carrot_060)
295
-
296
- # 8. Safety-first CARROT (mu=0.7, threshold=0.65)
297
- def safe_carrot_065(x, t):
298
- return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.65)
299
- eval_router("safe_CARROT_t0.65", safe_carrot_065)
300
-
301
- # 9. Oracle
302
- eval_router("oracle", lambda x, t: (t["opt"], {}))
303
-
304
- # Print comparison
305
- print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
306
- print("-"*75)
307
- frontier_cost = results["always_frontier"]["avg_cost"]
308
- for name, r in sorted(results.items(), key=lambda x: -x[1]["success"]):
309
- cr = (1 - r["avg_cost"]/frontier_cost)*100
310
- print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
311
-
312
- # ─── Train Improved Direct Classifier ───────────────────────────────
313
- print("\n\n[5] Training improved direct classifier (0-indexed)...")
314
-
315
- y_train_direct = y_opt[idx_train] - 1
316
- y_test_direct = y_opt[idx_test] - 1
317
-
318
- # Use sample weights: penalize underprediction more
319
- from sklearn.utils.class_weight import compute_sample_weight
320
-
321
- # Custom weight: underkill is 3x worse than overkill
322
- sample_weights = []
323
- for i in idx_train:
324
- t = traces[i]
325
- opt = t["opt"]
326
- # Weight by inverse frequency + safety penalty
327
- sample_weights.append(1.0)
328
- sample_weights = np.array(sample_weights)
329
-
330
- direct_clf = xgb.XGBClassifier(
331
- n_estimators=300, max_depth=6, learning_rate=0.05,
332
- subsample=0.8, colsample_bytree=0.8,
333
- objective="multi:softmax", num_class=5,
334
- eval_metric="mlogloss", random_state=42, verbosity=0,
335
- )
336
- direct_clf.fit(X_train, y_train_direct, sample_weight=sample_weights)
337
-
338
- y_pred_direct = direct_clf.predict(X_test) + 1 # back to 1-indexed
339
- acc = accuracy_score(y_opt[idx_test], y_pred_direct)
340
- print(f" Direct classifier accuracy: {acc:.3f}")
341
-
342
- # Evaluate direct classifier with safety floors
343
- def direct_safe(x, t):
344
- pred = int(direct_clf.predict(x)[0]) + 1
345
- floor = TASK_FLOOR.get(t["tt"], 2)
346
- return max(pred, floor), {}
347
-
348
- eval_router("direct_safe_xgb", direct_safe)
349
-
350
- # ─── Feature Importance ─────────────────────────────────────────────
351
- print("\n\n[6] Feature importance (from direct classifier)...")
352
- imp = direct_clf.feature_importances_
353
- for feat, score in sorted(zip(FEAT_KEYS, imp), key=lambda x: -x[1])[:10]:
354
- print(f" {feat:<25}: {score:.4f}")
355
-
356
- # ─── Save Models ────────────────────────────────────────────────────
357
- print("\n\n[7] Saving models...")
358
- os.makedirs("/app/router_models", exist_ok=True)
359
- for tier, clf in tier_clfs.items():
360
- clf.save_model(f"/app/router_models/tier_{tier}_success.json")
361
- direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
362
- with open("/app/router_models/feat_keys.json", "w") as f:
363
- json.dump(FEAT_KEYS, f)
364
- with open("/app/router_models/tier_config.json", "w") as f:
365
- json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR, "task_floor": TASK_FLOOR}, f, indent=2)
366
-
367
- # Final print
368
- print(f"\n\n{'='*80}")
369
- print("FINAL COMPARISON (ALL ROUTERS)")
370
- print(f"{'='*80}")
371
- print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
372
- print("-"*75)
373
- frontier_cost = results["always_frontier"]["avg_cost"]
374
- for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
375
- cr = (1 - r["avg_cost"]/frontier_cost)*100
376
- print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
377
-
378
- print(f"\n\nDONE! Models saved to /app/router_models/")
379
-
380
- # ─── RouteLLM-Style Binary Router ────────────────────────────────────
381
- print("\n\n[8] Training RouteLLM-style binary classifiers...")
382
- print(" (For each tier pair, train: should we route to cheaper or more expensive tier?)")
383
-
384
- # For each tier boundary, train a binary classifier
385
- # tier_boundary[t] = P(should use tier >= t | query)
386
- # Route to the first tier where the boundary classifier says "yes, this is enough"
387
-
388
- boundary_clfs = {}
389
- for boundary in range(2, 6):
390
- # Label: 1 if optimal_tier < boundary (cheaper tier is sufficient)
391
- # 0 if optimal_tier >= boundary (need this tier or higher)
392
- y_boundary = np.array([1 if traces[i]["opt"] < boundary else 0 for i in range(len(traces))])
393
-
394
- y_tr = y_boundary[idx_train]
395
- y_te = y_boundary[idx_test]
396
-
397
- neg = (y_tr == 0).sum()
398
- pos = (y_tr == 1).sum()
399
- spw = neg / max(pos, 1)
400
-
401
- clf = xgb.XGBClassifier(
402
- n_estimators=150, max_depth=5, learning_rate=0.1,
403
- subsample=0.8, colsample_bytree=0.8,
404
- scale_pos_weight=min(spw, 3.0),
405
- objective="binary:logistic", eval_metric="logloss",
406
- random_state=42, verbosity=0,
407
- )
408
- clf.fit(X_train, y_tr)
409
-
410
- y_pred = clf.predict(X_test)
411
- acc = accuracy_score(y_te, y_pred)
412
- f1 = f1_score(y_te, y_pred, zero_division=0)
413
-
414
- boundary_clfs[boundary] = clf
415
- rate = (y_tr == 0).mean() # fraction that needs this tier
416
- print(f" Boundary {boundary}: acc={acc:.3f}, f1={f1:.3f}, needs_tier={rate:.3f}")
417
-
418
- def route_cascade_binary(x, t):
419
- """RouteLLM-style cascade: check each boundary, route to first that passes."""
420
- if x.ndim == 1:
421
- x = x.reshape(1, -1)
422
- floor = TASK_FLOOR.get(t["tt"], 2)
423
-
424
- # Start at floor, check if we need higher
425
- current_tier = floor
426
-
427
- for boundary in range(floor + 1, 6):
428
- # boundary_clfs[boundary] predicts P(optimal < boundary)
429
- # If P(optimal < boundary) > threshold, we can stay below boundary
430
- # i.e., if P(need tier >= boundary) > threshold, escalate
431
- p_need_higher = boundary_clfs[boundary].predict_proba(x)[0, 0] # P(optimal >= boundary)
432
- if p_need_higher > 0.4: # confidence threshold
433
- current_tier = boundary
434
- else:
435
- break
436
-
437
- return current_tier, {}
438
-
439
- eval_router("cascade_binary_t0.4", route_cascade_binary)
440
-
441
- def route_cascade_binary_t050(x, t):
442
- if x.ndim == 1: x = x.reshape(1, -1)
443
- floor = TASK_FLOOR.get(t["tt"], 2)
444
- current_tier = floor
445
- for boundary in range(floor + 1, 6):
446
- p_need = boundary_clfs[boundary].predict_proba(x)[0, 0]
447
- if p_need > 0.5:
448
- current_tier = boundary
449
- else:
450
- break
451
- return current_tier, {}
452
-
453
- eval_router("cascade_binary_t0.5", route_cascade_binary_t050)
454
-
455
- def route_cascade_binary_t030(x, t):
456
- if x.ndim == 1: x = x.reshape(1, -1)
457
- floor = TASK_FLOOR.get(t["tt"], 2)
458
- current_tier = floor
459
- for boundary in range(floor + 1, 6):
460
- p_need = boundary_clfs[boundary].predict_proba(x)[0, 0]
461
- if p_need > 0.3:
462
- current_tier = boundary
463
- else:
464
- break
465
- return current_tier, {}
466
-
467
- eval_router("cascade_binary_t0.3", route_cascade_binary_t030)
468
-
469
- # Save boundary classifiers
470
- for boundary, clf in boundary_clfs.items():
471
- clf.save_model(f"/app/router_models/boundary_{boundary}.json")
472
- print(f" Saved boundary_{boundary}.json")
473
-
474
- # ─── Final Final Comparison ───────────────────────────────────────────
475
- print(f"\n\n{'='*80}")
476
- print("FINAL COMPARISON v2 (WITH BINARY CASCADE ROUTER)")
477
- print(f"{'='*80}")
478
- print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
479
- print("-"*75)
480
- frontier_cost = results["always_frontier"]["avg_cost"]
481
- for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
482
- cr = (1 - r["avg_cost"]/frontier_cost)*100
483
- # Only show key results
484
- if name in ("oracle","always_frontier","heuristic_diff+1","safe_CARROT_t0.60",
485
- "cascade_binary_t0.4","cascade_binary_t0.5","cascade_binary_t0.3",
486
- "always_cheap"):
487
- print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
488
-
489
- # Find best Pareto
490
- print("\n\nPARETO FRONTIER:")
491
- pareto = []
492
- for name, r in results.items():
493
- if name in ("always_cheap",):
494
- continue # skip dominated
495
- dominated = False
496
- for name2, r2 in results.items():
497
- if name == name2: continue
498
- if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
499
- if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
500
- dominated = True; break
501
- if not dominated:
502
- pareto.append((name, r))
503
- cr = (1 - r["avg_cost"]/frontier_cost)*100
504
- print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}%")
505
-
506
- # Save all results
507
- with open("/app/router_models/eval_results.json", "w") as f:
508
- json.dump(results, f, indent=2, default=str)
509
- print(f"\n Saved eval_results.json")
510
- print(f"\nDONE!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_router_v3.py DELETED
@@ -1,262 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Trained Router v3: Combined best approach.
3
- Uses per-tier P(success) classifiers + safety floors +
4
- cost-aware routing with ASYMMETRIC penalties (underkill penalized 5x harder than overkill).
5
- """
6
- import json, os, sys, random, pickle, uuid
7
- import numpy as np
8
- from datetime import datetime
9
- from collections import defaultdict
10
- from typing import Dict, List, Tuple, Any, Optional
11
-
12
- TASK_TYPES = ["quick_answer","coding","research","document_drafting",
13
- "legal_regulated","tool_heavy","retrieval_heavy",
14
- "long_horizon","unknown_ambiguous"]
15
- TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
16
-
17
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
18
- "implement","test","compile","runtime","class","module","async","thread"]
19
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
20
- RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
21
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
22
- LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
23
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
24
-
25
- TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
26
- TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
27
- TASK_FLOOR = {"legal_regulated":4,"long_horizon":3,"research":3,"coding":3,
28
- "unknown_ambiguous":3,"quick_answer":1,"document_drafting":2,
29
- "tool_heavy":2,"retrieval_heavy":2}
30
-
31
- TASK_TEMPLATES = {
32
- "quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
33
- "What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
34
- "What is the speed of light?","List the primary colors.","What is GDP?"],
35
- "coding":["Write a Python function to reverse a linked list.",
36
- "Fix the bug in this React component.","Refactor auth module to JWT.",
37
- "Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
38
- "Add unit tests for the payment module.","Optimize this SQL query.",
39
- "Create a REST API for user management.","Implement binary search in Rust."],
40
- "research":["Research latest transformer advances.",
41
- "Find sources comparing LoRA and full FT.",
42
- "Investigate data center climate impact.",
43
- "Survey privacy-preserving ML techniques.",
44
- "Compare reinforcement learning algorithms for robotics."],
45
- "document_drafting":["Draft project proposal for ML pipeline.",
46
- "Write email to team about deployment.","Create technical report on performance."],
47
- "legal_regulated":["Review this contract for liability clauses.",
48
- "Check GDPR compliance for data pipeline.","Draft privacy policy section.",
49
- "Verify regulatory compliance for medical device software."],
50
- "tool_heavy":["Search open issues and create summary.",
51
- "Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
52
- "retrieval_heavy":["Answer based on 50-page document.",
53
- "Find all payment processing mentions.","Retrieve relevant cases for legal query."],
54
- "long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
55
- "Redesign data architecture end-to-end.","Migrate monolith to microservices."],
56
- "unknown_ambiguous":["Help me with this thing.",
57
- "I need something about the server.","Can you look into that issue?"],
58
- }
59
-
60
- def tsp(tier, diff):
61
- return TIER_STR[tier] ** (diff * 0.6)
62
-
63
- def extract_features(request, task_type, difficulty=3):
64
- r = request.lower()
65
- f = {
66
- "req_len": len(request), "num_words": len(request.split()),
67
- "has_code": int(any(k in r for k in CODE_KW)),
68
- "n_code": sum(1 for k in CODE_KW if k in r),
69
- "has_legal": int(any(k in r for k in LEGAL_KW)),
70
- "n_legal": sum(1 for k in LEGAL_KW if k in r),
71
- "has_research": int(any(k in r for k in RESEARCH_KW)),
72
- "n_research": sum(1 for k in RESEARCH_KW if k in r),
73
- "has_tool": int(any(k in r for k in TOOL_KW)),
74
- "n_tool": sum(1 for k in TOOL_KW if k in r),
75
- "has_long": int(any(k in r for k in LONG_KW)),
76
- "has_math": int(any(k in r for k in MATH_KW)),
77
- "tt_idx": TT2IDX.get(task_type, 8), "difficulty": difficulty,
78
- }
79
- for tt in TASK_TYPES:
80
- f[f"tt_{tt}"] = int(task_type == tt)
81
- return f
82
-
83
- def gen_trace(idx, rng):
84
- tt = rng.choice(list(TASK_TEMPLATES.keys()))
85
- diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
86
- "research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
87
- tier_out = {}
88
- for t in range(1,6):
89
- tier_out[t] = rng.random() < tsp(t, diff)
90
- opt = 5
91
- for t in range(1,6):
92
- if tier_out[t]: opt = t; break
93
- if diff <= 2: actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
94
- elif diff == 3: actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
95
- elif diff == 4: actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
96
- else: actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
97
- outcome = "success" if tier_out[actual] else "failure"
98
- req = rng.choice(TASK_TEMPLATES[tt])
99
- feats = extract_features(req, tt, diff)
100
- return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
101
- "tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
102
-
103
- print("="*80)
104
- print("AGENT COST OPTIMIZER - TRAINED ROUTER v3")
105
- print("Asymmetric cost scoring: underkill 5x penalty")
106
- print("="*80)
107
-
108
- # ─── Generate ────────────────────────────────────────────────────────
109
- print("\n[1] Generating 50K training traces...")
110
- rng = random.Random(42)
111
- traces = [gen_trace(i, rng) for i in range(50000)]
112
- FEAT_KEYS = sorted(traces[0]["feats"].keys())
113
- def f2v(feats):
114
- return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
115
-
116
- X_all = np.array([f2v(t["feats"]) for t in traces])
117
- y_opt = np.array([t["opt"] for t in traces])
118
-
119
- per_tier_labels = {}
120
- for tier in range(1,6):
121
- per_tier_labels[tier] = np.array([1 if t["tier_out"].get(tier,False) else 0 for t in traces])
122
-
123
- from sklearn.model_selection import train_test_split
124
- from sklearn.metrics import accuracy_score, f1_score
125
- import xgboost as xgb
126
-
127
- X_train, X_test, idx_train, idx_test = train_test_split(X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt)
128
- print(f" Train: {len(X_train)}, Test: {len(X_test)}")
129
-
130
- # ─── Train Per-Tier Classifiers ─────────────────────────────────────
131
- print("\n[2] Training per-tier P(success) classifiers...")
132
- tier_clfs = {}
133
- for tier in range(1,6):
134
- y_tr = per_tier_labels[tier][idx_train]
135
- neg = (y_tr==0).sum(); pos = (y_tr==1).sum()
136
- spw = neg / max(pos,1)
137
- clf = xgb.XGBClassifier(n_estimators=150, max_depth=5, learning_rate=0.1,
138
- subsample=0.8, colsample_bytree=0.8, scale_pos_weight=min(spw,5.0),
139
- objective="binary:logistic", eval_metric="logloss", random_state=42, verbosity=0)
140
- clf.fit(X_train, y_tr)
141
- y_pred = clf.predict(X_test)
142
- acc = accuracy_score(per_tier_labels[tier][idx_test], y_pred)
143
- f1 = f1_score(per_tier_labels[tier][idx_test], y_pred, zero_division=0)
144
- tier_clfs[tier] = clf
145
- print(f" Tier {tier}: acc={acc:.3f}, f1={f1:.3f}")
146
-
147
- # ─── Asymmetric Cost Router ──────────────────────────────────────────
148
- print("\n[3] Building asymmetric cost router...")
149
-
150
- def route_asymmetric(x, task_type, tier_clfs, underkill_penalty=5.0, overkill_penalty=1.0):
151
- """Score each tier with asymmetric penalties.
152
-
153
- score(tier) = P(failure@tier) * underkill_penalty * cost_of_failure
154
- + cost(tier) * overkill_penalty
155
-
156
- Underkill (routing too low) is penalized 5x more than overkill.
157
- """
158
- if x.ndim == 1:
159
- x = x.reshape(1, -1)
160
- floor = TASK_FLOOR.get(task_type, 2)
161
-
162
- best_tier = floor
163
- best_score = float("inf")
164
-
165
- for tier in range(floor, 6):
166
- p_fail = 1.0 - tier_clfs[tier].predict_proba(x)[0, 1]
167
- cost_norm = TIER_COST[tier] / TIER_COST[5] # [0.03, 1.0]
168
-
169
- # Expected cost of failure (cheap model on hard task)
170
- failure_cost = p_fail * underkill_penalty
171
-
172
- # Cost of using this tier (overkill penalty)
173
- tier_cost = cost_norm * overkill_penalty
174
-
175
- score = failure_cost + tier_cost
176
-
177
- if score < best_score:
178
- best_score = score
179
- best_tier = tier
180
-
181
- return best_tier
182
-
183
- # ─── Evaluate ─────────────────────────────────────────────────────────
184
- print("\n[4] Evaluating all routers...")
185
- n_test = len(idx_test)
186
- results = {}
187
-
188
- def eval_router(name, route_fn):
189
- succ = 0; cost = 0.0; unsafe = 0; fd = 0
190
- td = defaultdict(int)
191
- for i in idx_test:
192
- t = traces[i]
193
- x = f2v(t["feats"]).reshape(1,-1)
194
- pred = route_fn(x, t)
195
- td[pred] += 1
196
- if t["tier_out"].get(pred, False): succ += 1
197
- elif pred < t["opt"]: unsafe += 1
198
- elif pred >= t["opt"]: fd += 1
199
- cost += TIER_COST[pred]
200
- results[name] = {"success":succ/n_test, "avg_cost":cost/n_test,
201
- "unsafe_rate":unsafe/n_test, "false_done":fd/n_test,
202
- "tier_dist":dict(td)}
203
-
204
- eval_router("always_frontier", lambda x,t: 4)
205
- eval_router("always_cheap", lambda x,t: 1)
206
- eval_router("heuristic_diff+1", lambda x,t: min(t["diff"]+1,5))
207
- eval_router("heuristic_floor", lambda x,t: TASK_FLOOR.get(t["tt"],2))
208
-
209
- for ukp in [3.0, 5.0, 8.0, 10.0, 15.0]:
210
- eval_router(f"asymmetric_uk{ukp:.0f}", lambda x,t,uk=ukp: route_asymmetric(x, t["tt"], tier_clfs, underkill_penalty=uk))
211
-
212
- eval_router("oracle", lambda x,t: t["opt"])
213
-
214
- # Print comparison
215
- print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
216
- print("-"*75)
217
- fc = results["always_frontier"]["avg_cost"]
218
- for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
219
- cr = (1 - r["avg_cost"]/fc)*100
220
- print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
221
-
222
- # Pareto frontier
223
- print("\nPARETO FRONTIER:")
224
- pareto = []
225
- for name, r in results.items():
226
- if name == "always_cheap": continue
227
- dominated = False
228
- for name2, r2 in results.items():
229
- if name == name2: continue
230
- if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
231
- if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
232
- dominated = True; break
233
- if not dominated:
234
- pareto.append((name, r))
235
- cr = (1 - r["avg_cost"]/fc)*100
236
- print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}% unsafe={r['unsafe_rate']:.3f}")
237
-
238
- # ─── Save Best Model ─────────────────────────────────────────────────
239
- print("\n[5] Saving models...")
240
- os.makedirs("/app/router_models", exist_ok=True)
241
- for tier, clf in tier_clfs.items():
242
- clf.save_model(f"/app/router_models/tier_{tier}_success.json")
243
- with open("/app/router_models/feat_keys.json","w") as f:
244
- json.dump(FEAT_KEYS, f)
245
- with open("/app/router_models/tier_config.json","w") as f:
246
- json.dump({"tier_cost":TIER_COST,"tier_str":TIER_STR,"task_floor":TASK_FLOOR}, f, indent=2)
247
- with open("/app/router_models/eval_results_v3.json","w") as f:
248
- json.dump(results, f, indent=2, default=str)
249
- print(f" Saved all models to /app/router_models/")
250
-
251
- # Find best config
252
- best_name = ""
253
- best_score = -float("inf")
254
- for name, r in results.items():
255
- if name in ("oracle","always_cheap"): continue
256
- # Composite: success*20 - cost*50 - unsafe*100
257
- score = r["success"]*20 - r["avg_cost"]*50 - r["unsafe_rate"]*100
258
- if score > best_score:
259
- best_score = score
260
- best_name = name
261
- print(f"\n BEST CONFIG: {best_name} (composite score: {best_score:.2f})")
262
- print(f"\nDONE!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training/train_router_v4_production.py DELETED
@@ -1,241 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Production Trained Router v4: Difficulty-first + ML refinement.
3
-
4
- Architecture:
5
- 1. Predict difficulty (task_type -> difficulty mapping + ML difficulty classifier)
6
- 2. Convert difficulty to base_tier (difficulty + 1, capped at 5)
7
- 3. Apply safety floor per task_type
8
- 4. Use per-tier P(success) classifiers to CONFIRM or ESCALATE
9
- 5. If P(success@base_tier) < 0.65, escalate to next tier
10
-
11
- This combines the best of both worlds:
12
- - Difficulty mapping (heuristic) is reliable and interpretable
13
- - ML classifiers add a safety net: they catch cases where difficulty is underestimated
14
- - Safety floors prevent dangerous under-routing on legal/critical tasks
15
- """
16
- import json, os, sys, random, uuid
17
- import numpy as np
18
- from datetime import datetime
19
- from collections import defaultdict
20
- from typing import Dict, List, Optional, Any
21
-
22
- import xgboost as xgb
23
-
24
- # ─── Load Models ──────────────────────────────────────────────────────
25
- MODEL_DIR = "/app/router_models"
26
- feat_keys = json.load(open(f"{MODEL_DIR}/feat_keys.json"))
27
- tier_config = json.load(open(f"{MODEL_DIR}/tier_config.json"))
28
- TIER_COST = {int(k):v for k,v in tier_config["tier_cost"].items()}
29
- TIER_STR = {int(k):v for k,v in tier_config["tier_str"].items()}
30
- TASK_FLOOR = tier_config["task_floor"]
31
-
32
- tier_clfs = {}
33
- for tier in range(1, 6):
34
- clf = xgb.XGBClassifier()
35
- clf.load_model(f"{MODEL_DIR}/tier_{tier}_success.json")
36
- tier_clfs[tier] = clf
37
-
38
- # ─── Feature Extraction ────────────────────────────────────────────────
39
- TASK_TYPES = ["quick_answer","coding","research","document_drafting",
40
- "legal_regulated","tool_heavy","retrieval_heavy",
41
- "long_horizon","unknown_ambiguous"]
42
- TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
43
-
44
- CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
45
- "implement","test","compile","runtime","class","module","async","thread"]
46
- LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
47
- RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
48
- TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
49
- LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
50
- MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
51
-
52
- def extract_features(request, task_type, difficulty=3):
53
- r = request.lower()
54
- f = {"req_len":len(request),"num_words":len(request.split()),
55
- "has_code":int(any(k in r for k in CODE_KW)),
56
- "n_code":sum(1 for k in CODE_KW if k in r),
57
- "has_legal":int(any(k in r for k in LEGAL_KW)),
58
- "n_legal":sum(1 for k in LEGAL_KW if k in r),
59
- "has_research":int(any(k in r for k in RESEARCH_KW)),
60
- "n_research":sum(1 for k in RESEARCH_KW if k in r),
61
- "has_tool":int(any(k in r for k in TOOL_KW)),
62
- "n_tool":sum(1 for k in TOOL_KW if k in r),
63
- "has_long":int(any(k in r for k in LONG_KW)),
64
- "has_math":int(any(k in r for k in MATH_KW)),
65
- "tt_idx":TT2IDX.get(task_type,8),"difficulty":difficulty}
66
- for tt in TASK_TYPES:
67
- f[f"tt_{tt}"] = int(task_type == tt)
68
- return f
69
-
70
- def f2v(feats):
71
- return np.array([float(feats.get(k,0.0)) for k in feat_keys], dtype=np.float32)
72
-
73
- # ─── Production Router ─────────────────────────────────────────────────
74
- def route_production(request, task_type, difficulty, escalation_threshold=0.65):
75
- """Production router: difficulty-first + ML confirmation.
76
-
77
- Step 1: difficulty -> base_tier (difficulty + 1, capped at 5)
78
- Step 2: Apply safety floor (task_type -> minimum tier)
79
- Step 3: base_tier = max(base_tier, safety_floor)
80
- Step 4: If P(success@base_tier) < escalation_threshold, escalate one tier
81
- Step 5: Never exceed tier 5
82
- """
83
- base_tier = min(difficulty + 1, 5)
84
- floor = TASK_FLOOR.get(task_type, 2)
85
- base_tier = max(base_tier, floor)
86
-
87
- # ML confirmation: check if base_tier is likely to succeed
88
- feats = extract_features(request, task_type, difficulty)
89
- x = f2v(feats).reshape(1, -1)
90
-
91
- p_success = tier_clfs[base_tier].predict_proba(x)[0, 1]
92
-
93
- # If P(success) at base_tier is too low, escalate
94
- while p_success < escalation_threshold and base_tier < 5:
95
- base_tier += 1
96
- p_success = tier_clfs[base_tier].predict_proba(x)[0, 1]
97
-
98
- return base_tier
99
-
100
- # ─── Generate Evaluation ────────────────────────────────────────────────
101
- TASK_TEMPLATES_EVAL = {
102
- "quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
103
- "What is 237*452?","Define photosynthesis.","Who wrote Hamlet?"],
104
- "coding":["Write a Python function to reverse a linked list.",
105
- "Fix the bug in this React component.","Refactor auth module to JWT.",
106
- "Implement LRU cache in Go.","Debug segfault in C++ thread pool."],
107
- "research":["Research latest transformer advances.",
108
- "Find sources comparing LoRA and full FT.",
109
- "Investigate data center climate impact."],
110
- "document_drafting":["Draft project proposal for ML pipeline.",
111
- "Write email to team about deployment.","Create technical report on performance."],
112
- "legal_regulated":["Review this contract for liability clauses.",
113
- "Check GDPR compliance for data pipeline.","Draft privacy policy section."],
114
- "tool_heavy":["Search open issues and create summary.",
115
- "Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
116
- "retrieval_heavy":["Answer based on 50-page document.",
117
- "Find all payment processing mentions.","Retrieve relevant cases for legal query."],
118
- "long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
119
- "Redesign data architecture end-to-end."],
120
- "unknown_ambiguous":["Help me with this thing.",
121
- "I need something about the server.","Can you look into that issue?"],
122
- }
123
-
124
- def tsp(tier, diff):
125
- s = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}[tier]
126
- return s ** (diff * 0.6)
127
-
128
- print("="*80)
129
- print("PRODUCTION TRAINED ROUTER v4 BENCHMARK")
130
- print("="*80)
131
- print("\nArchitecture: difficulty-first + ML confirmation + safety floors")
132
-
133
- print("\n[1] Generating 2K eval traces (seed=999)...")
134
- eval_rng = random.Random(999)
135
- eval_traces = []
136
- for i in range(2000):
137
- tt = eval_rng.choice(list(TASK_TEMPLATES_EVAL.keys()))
138
- diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
139
- "research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
140
- tier_out = {t: eval_rng.random() < tsp(t, diff) for t in range(1,6)}
141
- opt = 5
142
- for t in range(1,6):
143
- if tier_out[t]: opt = t; break
144
- req = eval_rng.choice(TASK_TEMPLATES_EVAL[tt])
145
- eval_traces.append({"tt":tt,"diff":diff,"opt":opt,"tier_out":tier_out,"req":req})
146
- print(f" Generated {len(eval_traces)} traces")
147
-
148
- # ─── Evaluate ──────────────────────────────────────────────────────────
149
- print("\n[2] Evaluating all routers...")
150
-
151
- def eval_router(name, route_fn):
152
- succ=0; cost=0.0; unsafe=0; fd=0; td=defaultdict(int)
153
- for t in eval_traces:
154
- pred = route_fn(t)
155
- td[pred] += 1
156
- if t["tier_out"].get(pred, False): succ += 1
157
- elif pred < t["opt"]: unsafe += 1
158
- else: fd += 1
159
- cost += TIER_COST[pred]
160
- n = len(eval_traces)
161
- return {"success":succ/n, "avg_cost":cost/n, "unsafe_rate":unsafe/n,
162
- "false_done":fd/n, "tier_dist":dict(td)}
163
-
164
- # Baselines
165
- results = {}
166
- results["always_frontier"] = eval_router("always_frontier", lambda t: 4)
167
- results["always_cheap"] = eval_router("always_cheap", lambda t: 1)
168
- results["heuristic_diff+1"] = eval_router("heuristic_diff+1", lambda t: min(t["diff"]+1, 5))
169
- results["heuristic_floor"] = eval_router("heuristic_floor", lambda t: TASK_FLOOR.get(t["tt"], 2))
170
- results["oracle"] = eval_router("oracle", lambda t: t["opt"])
171
-
172
- # Production router at different escalation thresholds
173
- for threshold in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75]:
174
- name = f"prod_t{threshold:.2f}"
175
- results[name] = eval_router(name,
176
- lambda t, th=threshold: route_production(t["req"], t["tt"], t["diff"], escalation_threshold=th))
177
-
178
- # Print comparison
179
- print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
180
- print("-"*75)
181
- fc = results["always_frontier"]["avg_cost"]
182
- for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
183
- cr = (1 - r["avg_cost"]/fc)*100
184
- print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
185
-
186
- # Find best production config
187
- print("\n\n[3] Finding best production config...")
188
- best_name = ""
189
- best_score = -float("inf")
190
- for name, r in results.items():
191
- if name in ("oracle","always_cheap"): continue
192
- # Composite: maximize success, minimize cost and unsafe
193
- score = r["success"]*20 - r["avg_cost"]*30 - r["unsafe_rate"]*100
194
- if score > best_score:
195
- best_score = score
196
- best_name = name
197
-
198
- print(f" Best: {best_name} (composite: {best_score:.2f})")
199
- print(f" Success: {results[best_name]['success']:.3f}")
200
- print(f" Avg cost: {results[best_name]['avg_cost']:.4f}")
201
- fc_val = results["always_frontier"]["avg_cost"]
202
- cr = (1 - results[best_name]["avg_cost"]/fc_val)*100
203
- print(f" Cost reduction vs frontier: {cr:.1f}%")
204
- print(f" Unsafe rate: {results[best_name]['unsafe_rate']:.3f}")
205
- print(f" False-DONE rate: {results[best_name]['false_done']:.3f}")
206
-
207
- # Per-task breakdown for best
208
- print(f"\n\n[4] Per-task breakdown for {best_name}...")
209
- for tt in sorted(set(t["tt"] for t in eval_traces)):
210
- tt_traces = [t for t in eval_traces if t["tt"] == tt]
211
- n_tt = len(tt_traces)
212
- if n_tt == 0: continue
213
-
214
- for rname, rfn in [("frontier", lambda t:4),
215
- ("heuristic", lambda t:min(t["diff"]+1,5)),
216
- ("trained", lambda t:route_production(t["req"],t["tt"],t["diff"],
217
- escalation_threshold=float(best_name.split("t")[1]))),
218
- ("oracle", lambda t:t["opt"])]:
219
- succ = sum(1 for t in tt_traces if t["tier_out"].get(rfn(t), False))
220
- cost = sum(TIER_COST[rfn(t)] for t in tt_traces)
221
- sr = succ/n_tt; ac = cost/n_tt
222
- if rname == "frontier":
223
- print(f"\n {tt} (n={n_tt}):")
224
- cr = (1 - ac/fc_val)*100
225
- print(f" {rname:<12} success={sr:.3f} cost={ac:.4f} costRed={cr:.1f}%")
226
-
227
- # Pareto
228
- print("\n\nPARETO FRONTIER:")
229
- for name, r in results.items():
230
- if name == "always_cheap": continue
231
- dominated = False
232
- for name2, r2 in results.items():
233
- if name == name2: continue
234
- if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
235
- if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
236
- dominated = True; break
237
- if not dominated:
238
- cr = (1 - r["avg_cost"]/fc_val)*100
239
- print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}% unsafe={r['unsafe_rate']:.3f}")
240
-
241
- print(f"\nDONE!")