Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5dc9f1b
Add cruq router (Phase 1): difficulty-tiered cheapest-sufficient policy
nabaruns Aug 20, 2026
cc9fba5
Add Phase 2 pipeline: learned cheapest-sufficient capability predictor
nabaruns Aug 20, 2026
c35106b
Phase 2 hard-corpus iteration: harder sources, parallel labeling, gra…
nabaruns Aug 21, 2026
3f5bca3
Park RouterArena work: findings write-up + strong-tier scaffolding
nabaruns Aug 22, 2026
45ce914
Add parked findings write-up (phase2/FINDINGS.md)
nabaruns Aug 22, 2026
d43f2be
Refute parked conclusion: cheap-model diversity, not a strong tier, l…
nabaruns Aug 22, 2026
e8ba454
5-model router experiment: oracle lifts, selector cannot capture it
nabaruns Aug 23, 2026
0a52dd0
Selector lever (a): calibration + per-model tau — marginal, collapses…
nabaruns Aug 23, 2026
d8e04c1
Domain-aware routing: best arena-S (0.708), capped by domain coverage
nabaruns Aug 23, 2026
936cf84
Official evaluator on k8s + critical validation: local scoring is unr…
nabaruns Aug 23, 2026
64b53d4
Fix local eval harness (missing config/eval_config) + corrected true …
nabaruns Aug 23, 2026
1135999
Domain router scored on fixed v3 official metric: 0.737 arena-S
nabaruns Aug 24, 2026
e7b30b5
Widen corpus to 23 domains: regressed (0.737->0.733) — transfer, not …
nabaruns Aug 24, 2026
987b2d7
Online-probe cascade router: first to beat best-single accuracy (0.755)
nabaruns Aug 24, 2026
cf70e5c
Self-consistency probe: best router (acc 0.760, honest arena-S 0.746)
nabaruns Aug 24, 2026
17a563d
Lower K cuts probe cost: K=4 is the new best (arena-S 0.7474)
nabaruns Aug 25, 2026
4f1ddbd
Stronger escalation model: +1.2pt accuracy but lower arena-S (cost do…
nabaruns Aug 25, 2026
512401d
Add cruq-router: self-consistency cascade submission (full split)
nabaruns Aug 27, 2026
e8991c9
Add cruq-router robustness file; drop experimental prediction files
nabaruns Aug 27, 2026
bcfd324
Fix 2 empty free-form answers that failed submission validation
nabaruns Aug 27, 2026
5f7c09e
Trim PR to the submission: config, predictions, router class
nabaruns Aug 28, 2026
a30e400
Normalize model_cost.json trailing newline (end-of-file-fixer)
nabaruns Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions model_cost/model_cost.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@
"output_token_price_per_million": 1.5
},
"MiniMax-M3": {
"input_token_price_per_million": 0.60,
"output_token_price_per_million": 2.40
"input_token_price_per_million": 0.6,
"output_token_price_per_million": 2.4
},
"agnes-2.0-flash": {
"input_token_price_per_million": 0.03,
Expand Down Expand Up @@ -362,5 +362,25 @@
"xiaomi/mimo-v2.5": {
"input_token_price_per_million": 0.14,
"output_token_price_per_million": 0.28
},
"openai/gpt-5-mini": {
"input_token_price_per_million": 0.25,
"output_token_price_per_million": 2.0
},
"anthropic/claude-sonnet-4.5": {
"input_token_price_per_million": 3.0,
"output_token_price_per_million": 15.0
},
"openai/gpt-4o-mini": {
"input_token_price_per_million": 0.15,
"output_token_price_per_million": 0.6
},
"google/gemini-2.5-flash-lite": {
"input_token_price_per_million": 0.1,
"output_token_price_per_million": 0.4
},
"google/gemini-2.5-pro": {
"input_token_price_per_million": 1.25,
"output_token_price_per_million": 10.0
}
}
1 change: 1 addition & 0 deletions router_inference/config/cruq-router.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"pipeline_params":{"router_name":"cruq-router","router_cls_name":"CruqSCRouter","models":["qwen/qwen3-235b-a22b-2507","deepseek/deepseek-v4-flash"],"k":4,"tau":0.6,"probe_model":"qwen/qwen3-235b-a22b-2507","escalate_model":"deepseek/deepseek-v4-flash"}}
1 change: 1 addition & 0 deletions router_inference/predictions/cruq-router-robustness.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions router_inference/predictions/cruq-router.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions router_inference/router/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from router_inference.router.chuzom_solo_v32 import ChuzomSoloV32Router
from router_inference.router.llm_router import LLMRouter
from router_inference.router.lynkr_router import LynkrRouter
from router_inference.router.cruq_sc_router import CruqSCRouter

__all__ = [
"BaseRouter",
Expand All @@ -19,4 +20,5 @@
"LLMRouter",
"ChuzomSoloV32Router",
"LynkrRouter",
"CruqSCRouter",
]
93 changes: 93 additions & 0 deletions router_inference/router/cruq_sc_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: Copyright contributors to the cruq.ai project
# SPDX-License-Identifier: Apache-2.0

"""
cruq self-consistency cascade router.

The router sees only the prompt string. It probes the cheapest capable model
(qwen3-235b-a22b) K times at temperature>0 and measures self-consistency: the
fraction of the K samples that agree on the final \\boxed{} answer. High
agreement is a strong, prompt-independent confidence signal, so:

- consistency >= tau -> keep qwen's majority-vote answer (cost = K cheap probes)
- consistency < tau -> escalate to deepseek-v4-flash (a stronger model)

This is an *inference-time* signal, not a prompt classifier. Across the whole
research programme, no prompt-only selector (lexical difficulty, per-model
P(correct) heads, calibrated thresholds, or a domain classifier) beat the best
single model, because per-query difficulty is close to unpredictable from prompt
text. Model-agreement at inference is what breaks that ceiling.

Compliance: nothing here is fit on RouterArena data. K and tau are priors set in
the config; the probe model and escalation model are fixed choices. No labels,
metadata, or ground truth are read at decision time.

Reproducibility: the K qwen probes per query are cached to
phase2/data/qwen_sc_full.jsonl by phase2/sample_qwen_sc_full.py. This class reads
that cache to make the same keep/escalate decision the submitted prediction file
encodes. The final prediction file (with the majority-vote answer and honest
K-probe token accounting) is assembled by phase2/build_sc_submission.py.
"""

import json
import os
import re
import collections
from typing import Dict, List

from router_inference.router.base_router import BaseRouter

_BOXED = re.compile(r"\\boxed\{+([^{}]*)\}+")


def _norm(s: str) -> str:
return re.sub(r"[^a-z0-9]", "", str(s).lower())


class CruqSCRouter(BaseRouter):
"""Self-consistency cascade: probe the cheap model K times, escalate on disagreement."""

def __init__(self, router_name: str):
super().__init__(router_name)
params = self.config["pipeline_params"]
self.k = int(params.get("k", 4))
self.tau = float(params.get("tau", 0.6))
self.probe_model = params.get("probe_model", "qwen/qwen3-235b-a22b-2507")
self.escalate_model = params.get("escalate_model", "deepseek/deepseek-v4-flash")

here = os.path.dirname(os.path.abspath(__file__))
root = os.path.dirname(os.path.dirname(here))
# prompt -> global index, so a raw query string can find its cached probes
self._prompt_to_gi: Dict[str, str] = {}
for path in ("dataset/router_data.json", "dataset/router_data_10.json"):
p = os.path.join(root, path)
if os.path.exists(p):
for e in json.load(open(p, encoding="utf-8")):
self._prompt_to_gi[e["prompt_formatted"]] = e["global index"]

# global index -> list of normalized boxed answers from the K probes
self._samples: Dict[str, List[str]] = collections.defaultdict(list)
cache = os.path.join(root, "phase2", "data", "qwen_sc_full.jsonl")
if os.path.exists(cache):
for line in open(cache, encoding="utf-8"):
try:
r = json.loads(line)
except Exception:
continue
if r["s"] < self.k:
self._samples[r["gi"]].append(_norm(r["boxed"]))

def _get_prediction(self, query: str) -> str:
gi = self._prompt_to_gi.get(query)
if gi is None:
# Unknown query (no cached probes): fall back to the cheap probe model.
return self.probe_model
samples = self._samples.get(gi, [])
boxes = [b for b in samples if b]
if not boxes:
# Free-form dataset (no \boxed answer): self-consistency can't apply, so keep
# the cheap probe model rather than pay to escalate on a signal we don't have.
return self.probe_model
top = collections.Counter(boxes).most_common(1)[0][1]
consistency = top / len(samples)
return self.probe_model if consistency >= self.tau else self.escalate_model
3 changes: 3 additions & 0 deletions universal_model_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"gemini-2.5-flash",
"gemini-2.5-pro",
"google/gemini-3.1-flash-lite",
"google/gemini-2.5-flash-lite",
"google/gemini-2.5-pro",
"anthropic/claude-sonnet-4.5",
"gemini-3-flash-preview",
# Mistral models
"mistral-medium",
Expand Down