Skip to content

Commit 7e98f41

Browse files
yl231Louie Luclaude
authored
Fix Grok 4.1 Fast → Grok 4.3 mispricing on the leaderboard (#172)
xAI retired `grok-4-1-fast-reasoning` on 2026-05-15 and silently redirects it to grok-4.3 (low reasoning), which is stronger and ~5-6x pricier. Submissions that selected the retired slug via xAI/OpenRouter were served grok-4.3 but billed at the retired slug's $0.20/$0.50 price, inflating their cost-aware Arena scores. Reported and audited by @loswald (thanks!). Fix (prices the model that actually answered, not the requested alias): - model_cost.json: add `grok-4.3` and `x-ai/grok-4.3` at $1.25/$2.50. - evaluate_models.py: add `has_price()`; factor the cost lookup into `_lookup_cost_info()`. - run.py: bill by `generated_result.model_used` when it is present and priced, else the router's selected model. Redirected slugs are now billed correctly. - check_config_prediction_files.py: preflight warning for retired/redirected slugs (guardrail #1 from the issue). Precision (only provably-redirected rows change; Azure-hosted Grok 4.1 is left alone): - cross-router 217 rows (openrouter, model_used=x-ai/grok-4.3) -> repriced - vllm-sr 257 rows (openrouter, model_used=x-ai/grok-4.3) -> repriced - nadir-tumbler 3571 rows (xAI, model_used absent): backfilled model_used=x-ai/grok-4.3 (retired slug + 2026-07-04 generation date) so the same mechanism reprices it. Team to be notified. - azure-model-router (4803) and nadir-cascade-v2 (3101): provider=azure, NOT subject to the xAI redirect -> unchanged. Leaderboard (accuracy held fixed, affected calls billed at grok-4.3): - Cross-Router 76.12 -> 75.75 ($0.30 -> $0.40/1K) stays #1 - vLLM-SR 75.30 -> 74.86 ($0.30 -> $0.42/1K) #2 -> #3 - Nadir-Tumbler 75.17 -> 73.44 ($0.08 -> $0.37/1K) #4 -> #5 Co-authored-by: Louie Lu <yl231@datalab2.cs.rice.edu> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2f37e6f commit 7e98f41

6 files changed

Lines changed: 119 additions & 21 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ For more details, please see our [website](https://routeworks.github.io/leaderbo
3737

3838
| Rank | Router | Affiliation | Acc-Cost Arena | Accuracy | Cost/1K Queries | Optimal Selection | Optimal Cost | Optimal Accuracy | Latency | Robustness |
3939
|------|--------------------|-----------------------------|--------|----------|---------|-----------------|--------------|----------------|---------|------------|
40-
| 🥇 | [Cross-Router]() | 👤&nbsp;[@JiaHg](https://github.com/JiaHg) | 76.12 | 78.14 | $0.30 | 17.66 | 45.49 | 90.31 || 67.14 |
41-
| 🥈 | [vLLM‑SR](https://vllm-semantic-router.com/)&nbsp;[[Code]](https://github.com/vllm-project/semantic-router)&nbsp;[[HF]](https://huggingface.co/llm-semantic-router) | 🎓&nbsp;vLLM SR Team | 75.30 | 77.18 | $0.30 | 16.81 | 25.10 | 89.37 || 67.62 |
42-
| 🥉 | [Sqwish Router](https://www.sqwish.ai/) | 👤&nbsp;[@namitha-sqwish](https://github.com/namitha-sqwish) | 75.27 | 76.40 | $0.18 | 7.41 | 25.10 | 90.47 || 100.00 |
43-
| 4 | [Nadir-Tumbler]() | 👤&nbsp;[@doramirdor](https://github.com/doramirdor) | 75.17 | 75.34 | $0.08 | | | || 66.43 |
44-
| 5 | [AgentForge Router]() | 👤&nbsp;[@YangY-Z](https://github.com/YangY-Z) | 74.13 | 74.72 | $0.13 | 17.84 | 52.47 | 98.68 || 40.48 |
40+
| 🥇 | [Cross-Router]() | 👤&nbsp;[@JiaHg](https://github.com/JiaHg) | 75.75 | 78.14 | $0.40 | 17.66 | 45.49 | 90.31 || 67.14 |
41+
| 🥈 | [Sqwish Router](https://www.sqwish.ai/) | 👤&nbsp;[@namitha-sqwish](https://github.com/namitha-sqwish) | 75.27 | 76.40 | $0.18 | 7.41 | 25.10 | 90.47 || 100.00 |
42+
| 🥉 | [vLLM‑SR](https://vllm-semantic-router.com/)&nbsp;[[Code]](https://github.com/vllm-project/semantic-router)&nbsp;[[HF]](https://huggingface.co/llm-semantic-router) | 🎓&nbsp;vLLM SR Team | 74.86 | 77.18 | $0.42 | 16.81 | 25.10 | 89.37 || 67.62 |
43+
| 4 | [AgentForge Router]() | 👤&nbsp;[@YangY-Z](https://github.com/YangY-Z) | 74.13 | 74.72 | $0.13 | 17.84 | 52.47 | 98.68 || 40.48 |
44+
| 5 | [Nadir-Tumbler]() | 👤&nbsp;[@doramirdor](https://github.com/doramirdor) | 73.44 | 75.34 | $0.37 | | | || 66.43 |
4545
| 6 | [Weave Router](https://workweave.ai) | 🎓&nbsp;Weave | 72.82 | 76.32 | $0.94 ||||| 100.00 |
4646
| 7 | [Nadir Router](https://github.com/NadirRouter/NadirClaw) | 🎓&nbsp;NadirRouter | 72.29 | 75.01 | $0.68 ||||| 25.48 |
4747
| 8 | [OrcaRouter‑Adaptive](https://www.orcarouter.ai/)&nbsp;[[Code]](https://github.com/Continuum-AI-Corp/OrcaRouter-Lite)&nbsp;[[Paper]](https://arxiv.org/abs/2605.30736)&nbsp;[[X]](https://x.com/orcarouter) | 🎓&nbsp;[Continuum&nbsp;AI](https://www.continuum01.ai/) | 72.08 | 75.54 | $1.00 ||||| 22.62 |

llm_evaluation/evaluate_models.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,29 @@ def load_cost_config(self):
206206
print(f"Warning: Could not load cost configuration from {cost_file}: {e}")
207207
self.cost_config = {}
208208

209+
def _lookup_cost_info(self, model_name: str):
210+
"""Find the pricing entry for a model name, trying an exact match first
211+
and then a substring fallback (historical behaviour). Returns the cost
212+
dict, or None if no price is known."""
213+
if not self.cost_config or not model_name:
214+
return None
215+
# Remove _batch suffix if present for cost lookup
216+
cost_lookup_name = (
217+
model_name[:-6] if model_name.endswith("_batch") else model_name
218+
)
219+
if cost_lookup_name in self.cost_config:
220+
return self.cost_config[cost_lookup_name]
221+
for config_name in self.cost_config.keys():
222+
if config_name in cost_lookup_name or cost_lookup_name in config_name:
223+
return self.cost_config[config_name]
224+
return None
225+
226+
def has_price(self, model_name: str) -> bool:
227+
"""Whether a price is known for this model name. Used to decide whether a
228+
provider-reported actual model (generated_result.model_used) can be
229+
billed directly instead of the router's selected slug. See issue #166."""
230+
return self._lookup_cost_info(model_name) is not None
231+
209232
def calculate_inference_cost(
210233
self, model_name: str, token_usage: Dict[str, int]
211234
) -> float:
@@ -222,17 +245,7 @@ def calculate_inference_cost(
222245
if model_name.endswith("_batch"):
223246
cost_lookup_name = model_name[:-6] # Remove '_batch' suffix
224247

225-
# Use model name directly - assume model_cost.json keys match model names exactly
226-
# Try to find exact match first
227-
if cost_lookup_name in self.cost_config:
228-
cost_info = self.cost_config[cost_lookup_name]
229-
else:
230-
# Try to find partial matches as fallback
231-
cost_info = None
232-
for config_name in self.cost_config.keys():
233-
if config_name in cost_lookup_name or cost_lookup_name in config_name:
234-
cost_info = self.cost_config[config_name]
235-
break
248+
cost_info = self._lookup_cost_info(model_name)
236249

237250
if not cost_info:
238251
print(

llm_evaluation/run.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,23 @@ def evaluate_single_prediction(
425425
generated_answer, ground_truth, scorer_func, dataset_name
426426
)
427427

428-
# Calculate inference cost
429-
# Use universal model name for cost lookup to respect user-defined mappings
428+
# Calculate inference cost.
429+
# Prefer the model the provider actually served (generated_result.model_used)
430+
# when it has a known price, so retired/redirected slugs are billed at the
431+
# model that truly answered rather than the requested alias. For example the
432+
# retired `grok-4-1-fast-reasoning` slug is redirected by xAI to grok-4.3,
433+
# which is both stronger and ~5-6x more expensive; billing it at the alias's
434+
# old price understates cost. Fall back to the router's selected model
435+
# (universal name, to respect universal_model_names.py mappings) when the
436+
# provider did not report an actual model or its price is unknown. See #166.
430437
token_usage = generated_result.get("token_usage", {})
438+
cost_model_name = universal_model_name
439+
actual_model = generated_result.get("model_used")
440+
if actual_model and evaluator.has_price(actual_model):
441+
cost_model_name = actual_model
431442
inference_cost = evaluator.calculate_inference_cost(
432-
universal_model_name,
433-
token_usage, # Use universal_model_name to respect mapping in universal_model_names.py
443+
cost_model_name,
444+
token_usage,
434445
)
435446

436447
# Update the prediction with evaluation results

model_cost/model_cost.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,5 +330,13 @@
330330
"z-ai/glm-4.7": {
331331
"input_token_price_per_million": 0.4,
332332
"output_token_price_per_million": 1.5
333+
},
334+
"grok-4.3": {
335+
"input_token_price_per_million": 1.25,
336+
"output_token_price_per_million": 2.5
337+
},
338+
"x-ai/grok-4.3": {
339+
"input_token_price_per_million": 1.25,
340+
"output_token_price_per_million": 2.5
333341
}
334342
}

router_inference/check_config_prediction_files.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,57 @@ def check_model_costs(
221221
return len(missing_costs) == 0, errors
222222

223223

224+
# Model slugs that upstream providers have retired and silently redirect to a
225+
# different (often stronger and pricier) model. A submission may still
226+
# legitimately select these via a provider that keeps hosting the original model
227+
# (e.g. Azure), so this is a warning rather than a hard failure: it reminds
228+
# submitters to record the actually-served model (generated_result.model_used)
229+
# so the evaluator prices the model that truly answered. See issue #166.
230+
RETIRED_SLUGS = {
231+
"grok-4-1-fast-reasoning": "x-ai/grok-4.3 (xAI redirect after 2026-05-15)",
232+
"grok-4-1-fast-non-reasoning": "x-ai/grok-4.3 (xAI redirect after 2026-05-15)",
233+
}
234+
235+
236+
def check_retired_slugs(predictions: List[Dict[str, Any]]) -> List[str]:
237+
"""
238+
Warn when predictions select a provider-retired slug, especially when the
239+
recorded actual model (generated_result.model_used) differs from the selected
240+
slug. Returns a list of warning strings; it never fails the run. See #166.
241+
"""
242+
warnings: List[str] = []
243+
counts: Dict[str, int] = {}
244+
redirected: Dict[str, int] = {}
245+
246+
for prediction in predictions:
247+
slug = prediction.get("prediction")
248+
if slug in RETIRED_SLUGS:
249+
counts[slug] = counts.get(slug, 0) + 1
250+
generated = prediction.get("generated_result") or {}
251+
model_used = generated.get("model_used")
252+
if model_used and model_used.split("/")[-1].lower() not in slug.lower():
253+
redirected[slug] = redirected.get(slug, 0) + 1
254+
255+
for slug, count in counts.items():
256+
msg = (
257+
f"'{slug}' is a retired slug that providers redirect to "
258+
f"{RETIRED_SLUGS[slug]}; {count} prediction(s) select it."
259+
)
260+
if redirected.get(slug):
261+
msg += (
262+
f" {redirected[slug]} row(s) recorded a different model_used, "
263+
"confirming the redirect — these are priced at the actually-served model."
264+
)
265+
else:
266+
msg += (
267+
" Record generated_result.model_used with the actually-served model so "
268+
"it is priced correctly, or select the resolved model explicitly."
269+
)
270+
warnings.append(msg)
271+
272+
return warnings
273+
274+
224275
def check_config_models(config: Dict[str, Any]) -> Tuple[bool, List[str]]:
225276
"""
226277
Check that all model names in config can be found in ModelNameManager.
@@ -627,6 +678,21 @@ def main():
627678
all_valid = False
628679
errors_summary.append(f"Cost check error: {str(e)}")
629680

681+
# Check 5: Warn about retired/redirected model slugs (informational only)
682+
print("\n[5] Checking for retired/redirected model slugs...")
683+
try:
684+
if predictions is not None:
685+
slug_warnings = check_retired_slugs(predictions)
686+
if slug_warnings:
687+
for warning in slug_warnings:
688+
print(f" ⚠ {warning}")
689+
else:
690+
print("✓ No retired/redirected model slugs detected")
691+
else:
692+
print("⚠ Skipping retired-slug check (predictions not loaded)")
693+
except Exception as e:
694+
print(f"⚠ Error checking retired slugs: {e}")
695+
630696
# Final summary
631697
print("\n" + "=" * 80)
632698
if all_valid:

router_inference/predictions/nadir-tumbler.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)