-
Notifications
You must be signed in to change notification settings - Fork 26
Linj router exp #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Linj router exp #61
Changes from all commits
31d90a0
411620e
ee550bd
5a07418
e85396e
7495a39
04b0f6e
92072b5
0e3bc56
5ece2a5
3caf3fe
f6824de
ef6f47a
6e588a6
084edbd
ff71fba
11ac385
d556ffd
7cf9452
6e3f5f0
34d5ba9
48af4ba
f3f58aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Compare accuracy and token cost across run-baseline.sh run directories. | ||
|
|
||
| Usage:: | ||
|
|
||
| uv run --no-sync python benchmark/compare_runs.py \ | ||
| label1=benchmark/tb_runs/<run-dir> label2=benchmark/tb_runs/<run-dir> ... | ||
|
|
||
| Reads each run's Harbor ``result.json`` (accuracy) and Switchyard | ||
| ``routing_stats_final.json`` (per-tier tokens, judge/classifier overhead, | ||
| cost estimate) and prints a side-by-side table plus per-task outcomes. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
|
|
||
| def _load_run(run_dir: Path) -> dict[str, Any]: | ||
| results = sorted(run_dir.glob("jobs/*/result.json")) | ||
| if not results: | ||
| raise FileNotFoundError(f"no jobs/*/result.json under {run_dir}") | ||
| harbor = json.loads(results[0].read_text()) | ||
| stats_path = run_dir / "routing_stats_final.json" | ||
| routing = json.loads(stats_path.read_text()) if stats_path.exists() else {} | ||
|
|
||
| ev = next(iter(harbor["stats"]["evals"].values())) | ||
| rewards: dict[str, float] = {} | ||
| for value, trials in (ev.get("reward_stats", {}).get("reward", {}) or {}).items(): | ||
| for trial in trials: | ||
| # trial names look like <task>-<suffix>; strip the trial suffix. | ||
| rewards[trial.rsplit("-", 1)[0]] = float(value) | ||
|
|
||
| return { | ||
| "harbor": harbor["stats"], | ||
| "eval": ev, | ||
| "rewards": rewards, | ||
| "routing": routing, | ||
| } | ||
|
|
||
|
|
||
| def _fmt_cost(routing: dict[str, Any]) -> tuple[float, float, float]: | ||
| ce = routing.get("cost_estimate", {}) or {} | ||
| return ( | ||
| ce.get("total_cost", 0.0), | ||
| ce.get("backend_cost", 0.0), | ||
| ce.get("classifier_cost", 0.0), | ||
| ) | ||
|
|
||
|
|
||
| def main(argv: list[str]) -> int: | ||
| runs: dict[str, dict[str, Any]] = {} | ||
| for arg in argv: | ||
| label, _, path = arg.partition("=") | ||
| if not path: | ||
| print(f"ERROR: expected label=path, got {arg!r}") | ||
| return 2 | ||
| runs[label] = _load_run(Path(path)) | ||
|
|
||
| header = f"{'metric':<28}" + "".join(f"{label:>18}" for label in runs) | ||
| print(header) | ||
| print("-" * len(header)) | ||
|
|
||
| def row(name: str, values: list[str]) -> None: | ||
| print(f"{name:<28}" + "".join(f"{v:>18}" for v in values)) | ||
|
|
||
| row("completed trials", [str(r["harbor"]["n_completed_trials"]) for r in runs.values()]) | ||
| row("errored trials", [str(r["harbor"]["n_errored_trials"]) for r in runs.values()]) | ||
|
Comment on lines
+72
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle Harbor results without run-level trial counters. The baseline fixture in 🤖 Prompt for AI Agents |
||
| row("mean reward", [f"{r['eval']['metrics'][0].get('mean', 0.0):.3f}" for r in runs.values()]) | ||
| row( | ||
| "solved / total", | ||
| [ | ||
| f"{sum(1 for v in r['rewards'].values() if v >= 1.0)}/{len(r['rewards'])}" | ||
| for r in runs.values() | ||
| ], | ||
| ) | ||
| row( | ||
| "input tokens", | ||
| [f"{r['harbor'].get('n_input_tokens', 0):,}" for r in runs.values()], | ||
| ) | ||
| row( | ||
| "output tokens", | ||
| [f"{r['harbor'].get('n_output_tokens', 0):,}" for r in runs.values()], | ||
| ) | ||
| for label_idx, name in ((0, "total cost $"), (1, "backend cost $"), (2, "judge/clf cost $")): | ||
| row(name, [f"{_fmt_cost(r['routing'])[label_idx]:.3f}" for r in runs.values()]) | ||
|
|
||
| # Tier split (escalation / routed runs only). | ||
| row( | ||
| "tier split (calls)", | ||
| [ | ||
| " ".join( | ||
| f"{v.get('tier') or m.rsplit('/', 1)[-1]}:{v.get('calls', 0)}" | ||
| for m, v in (r["routing"].get("models", {}) or {}).items() | ||
| ) | ||
| or "-" | ||
| for r in runs.values() | ||
| ], | ||
| ) | ||
| clf_rows = [] | ||
| for r in runs.values(): | ||
| clf = r["routing"].get("classifier", {}) or {} | ||
| n, e = clf.get("total_requests", 0), clf.get("total_errors", 0) | ||
| clf_rows.append(f"{n} ({e} err)" if n or e else "-") | ||
| row("judge calls", clf_rows) | ||
|
|
||
| # Per-task outcome grid. | ||
| tasks = sorted({t for r in runs.values() for t in r["rewards"]}) | ||
| print() | ||
| print(f"{'task':<44}" + "".join(f"{label:>14}" for label in runs)) | ||
| for task in tasks: | ||
| marks = [] | ||
| for r in runs.values(): | ||
| v = r["rewards"].get(task) | ||
| marks.append("-" if v is None else ("PASS" if v >= 1.0 else "fail")) | ||
| print(f"{task:<44}" + "".join(f"{m:>14}" for m in marks)) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main(sys.argv[1:])) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| routes: | ||
| switchyard: | ||
| type: escalation_router | ||
| enable_stats: true | ||
| fallback_target_on_evict: strong | ||
| # Distinguish k>1 repeated-task trials against one shared server: the | ||
| # session key includes the first 2 post-task messages, so trials diverge | ||
| # via early model responses instead of sharing an escalation latch. | ||
| # Requires nonzero sampling temperature; keep 0 outside benchmarks. | ||
| session_key_depth: 2 | ||
| strong: | ||
| model: anthropic/claude-opus-4.7 | ||
| api_key: ${OPENROUTER_API_KEY} | ||
| base_url: https://openrouter.ai/api/v1 | ||
| format: openai | ||
| timeout_secs: 600 | ||
| extra_body: | ||
| reasoning: | ||
| effort: high | ||
| weak: | ||
| model: deepseek/deepseek-v4-pro | ||
| api_key: ${OPENROUTER_API_KEY} | ||
| base_url: https://openrouter.ai/api/v1 | ||
| format: openai | ||
| timeout_secs: 600 | ||
| judge: | ||
| model: google/gemini-3.5-flash | ||
| api_key: ${OPENROUTER_API_KEY} | ||
| base_url: https://openrouter.ai/api/v1 | ||
|
Comment on lines
+12
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== profile file ==\n'
git ls-files 'benchmark/routing-profiles/tb-lite-escalation-opus-deepseek-gemini.yaml' && \
sed -n '1,120p' benchmark/routing-profiles/tb-lite-escalation-opus-deepseek-gemini.yaml
printf '\n== search pricing/lookup references ==\n'
rg -n "claude-opus-4\.7|gemini-3\.5-flash|enable_stats|pricing|alias|unknown IDs|zero" -S .
printf '\n== locate likely pricing files ==\n'
git ls-files | rg 'pricing|cost|model.*price|models?.*yaml|models?.*json|bench'Repository: NVIDIA-NeMo/Switchyard Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== cost table around gemini/opus mappings ==\n'
sed -n '1,340p' crates/switchyard-components/src/stats/cost.rs
printf '\n== tests around unknown/default behavior ==\n'
sed -n '1,120p' tests/test_cost_estimator_gemini.py 2>/dev/null || true
sed -n '1,360p' crates/switchyard-components/src/stats/cost.rs | sed -n '240,320p'Repository: NVIDIA-NeMo/Switchyard Length of output: 12882 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== cost table around gemini/opus mappings ==\n'
sed -n '1,340p' crates/switchyard-components/src/stats/cost.rs
printf '\n== tests around unknown/default behavior ==\n'
sed -n '1,120p' tests/test_cost_estimator_gemini.py 2>/dev/null || trueRepository: NVIDIA-NeMo/Switchyard Length of output: 11870 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== opus/gemini pricing entries ==\n'
rg -n -A4 -B4 'claude-opus-4\.7|gemini-3\.5-flash|unknown_model_defaults_to_zero|ModelPrice' crates/switchyard-components/src/stats/cost.rs
printf '\n== any other pricing tables or normalization helpers ==\n'
rg -n 'claude-opus-4\.7|gemini-3\.5-flash|normalize.*model|pricing.*alias|model.*alias' crates switchyard tests -SRepository: NVIDIA-NeMo/Switchyard Length of output: 12339 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== python cost estimator pricing table ==\n'
sed -n '1,260p' switchyard/lib/cost_estimator.py
printf '\n== live stats model normalization ==\n'
sed -n '1,140p' switchyard/lib/live_stats_collector.py
printf '\n== exact references around claude/gemini pricing in python ==\n'
rg -n -A3 -B3 'claude-opus-4\.7|gemini-3\.5-flash|MODEL_PRICING|_PRICE_PER_1M|normalize_model_name' switchyard/lib switchyard/tests tests -SRepository: NVIDIA-NeMo/Switchyard Length of output: 35836 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -A6 -B6 'MODEL_PRICING|claude-opus-4\.7|gemini-3\.5-flash|_normalize_model_name|_PRICE_PER_1M' switchyard/lib/cost_estimator.py switchyard/lib/live_stats_collector.pyRepository: NVIDIA-NeMo/Switchyard Length of output: 11983 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,240p' switchyard/lib/cost_estimator.pyRepository: NVIDIA-NeMo/Switchyard Length of output: 11369 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== cost estimator call sites ==\n'
rg -n -A4 -B4 'estimate_cost\(|MODEL_PRICING|estimated_cost_usd|to_dict\(\)|routing/stats' switchyard crates tests -S
printf '\n== model name normalization in stats accumulation ==\n'
rg -n -A4 -B4 '_normalize_model_name|estimated_cost_usd|record\(|snapshot\(|to_dict\(' switchyard/lib/live_stats_collector.py switchyard/lib/stats* crates/switchyard-components/src/stats -S
printf '\n== route/profile model names used for stats ==\n'
rg -n -A3 -B3 'model: anthropic/claude-opus-4\.7|model: google/gemini-3\.5-flash|anthropic/claude-opus-4\.7|google/gemini-3\.5-flash' benchmark switchyard crates tests -SRepository: NVIDIA-NeMo/Switchyard Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== search for model-id rewrite/normalization rules ==\n'
rg -n -A4 -B4 'claude-opus-4\.7|claude-opus-4-7|google/gemini-3\.5-flash|gcp/google/gemini-3\.5-flash|replace\(|normalize.*model|alias.*model' switchyard crates tests docs -S
printf '\n== route/stats serialization path ==\n'
rg -n -A4 -B4 'routing/stats|estimated_cost_usd|estimated_cost|ModelStats|model_id|model_name' crates/switchyard-components/src switchyard/lib switchyard/server -SRepository: NVIDIA-NeMo/Switchyard Length of output: 50379 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '140,320p' switchyard/lib/live_stats_collector.pyRepository: NVIDIA-NeMo/Switchyard Length of output: 5875 Add pricing aliases for the exact model IDs used here. 🤖 Prompt for AI Agents |
||
| timeout_secs: 5 | ||
| min_turn: 3 | ||
| recent_turn_window: 14 | ||
| # Verdict dumps became opt-in upstream; the bench harness reads | ||
| # escalation_verdict= lines from the captured server log. | ||
| dump_verdicts: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Escalation-router Opus/DeepSeek profile for Harbor Terminal-Bench Lite on | ||
| # the NVIDIA Inference Hub. Every conversation starts on DeepSeek V4 Pro; a | ||
| # DeepSeek V4 Flash judge watches the trajectory and latches the session to | ||
| # Claude Opus 4.8 on a clear pattern of trouble. | ||
| # | ||
| # All three tiers use https://inference-api.nvidia.com/v1, which requires a | ||
| # LiteLLM virtual key (sk-...), not an nvapi- key. Export the same key for | ||
| # all three vars (they are the ones run-baseline.sh forwards into the | ||
| # Switchyard container): | ||
| # export SWITCHYARD_STRONG_API_KEY=sk-... | ||
| # export SWITCHYARD_WEAK_API_KEY=sk-... | ||
| # export SWITCHYARD_CLASSIFIER_API_KEY=sk-... | ||
| # | ||
| # Run with: | ||
| # benchmark/run-baseline.sh \ | ||
| # --harbor-path benchmark/datasets/openthoughts-tblite-closed-book \ | ||
| # --routing-profiles benchmark/routing-profiles/tb-lite-escalation-opus-deepseek-nvidia.yaml \ | ||
| # --model switchyard | ||
|
|
||
| routes: | ||
| switchyard: | ||
| type: escalation_router | ||
| enable_stats: true | ||
| fallback_target_on_evict: strong | ||
| # Distinguish k>1 repeated-task trials against one shared server: the | ||
| # session key includes the first 2 post-task messages, so trials diverge | ||
| # via early model responses instead of sharing an escalation latch. | ||
| # Requires nonzero sampling temperature; keep 0 outside benchmarks. | ||
| session_key_depth: 2 | ||
| strong: | ||
| model: aws/anthropic/bedrock-claude-opus-4-8 | ||
| api_key: ${SWITCHYARD_STRONG_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| format: openai | ||
| timeout_secs: 600 | ||
| weak: | ||
| # The evals- batch-gateway variant (nvidia/deepseek-ai/evals-deepseek-v4-pro) | ||
| # avoids the regular gateway's 6-min timeout under high concurrency, but | ||
| # its upstream host is currently failing TLS verification; switch back | ||
| # once it probes clean. | ||
| model: nvidia/deepseek-ai/deepseek-v4-pro | ||
| api_key: ${SWITCHYARD_WEAK_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| format: openai | ||
| timeout_secs: 600 | ||
| # Explicit extra_body overrides the DeepSeek default of | ||
| # enable_thinking: false; the X-Inference-Priority: batch header is | ||
| # still injected (headers merge independently of the body). | ||
| extra_body: | ||
| chat_template_kwargs: | ||
| enable_thinking: true | ||
| judge: | ||
| model: nvidia/deepseek-ai/deepseek-v4-flash | ||
| api_key: ${SWITCHYARD_CLASSIFIER_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| # Hub Flash latency is ~4-8s on small judge calls (and the profile | ||
| # build pins DeepSeek judges to the X-Inference-Priority: batch | ||
| # gateway); the 5s default used with Gemini Flash would fail-open | ||
| # (stay weak) on most turns here. | ||
| timeout_secs: 30 | ||
| min_turn: 3 | ||
| # Require a second consecutive escalate verdict before latching: | ||
| # round-2 traces showed one-shot eager verdicts (single failed | ||
| # command at turn 4-9) causing regressions on tasks the weak tier | ||
| # solves; real loops persist across turns and still latch. | ||
| confirmations: 2 | ||
| recent_turn_window: 14 | ||
| # Verdict dumps became opt-in upstream; the bench harness reads | ||
| # escalation_verdict= lines from the captured server log. | ||
| dump_verdicts: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Serve-backed single-model DeepSeek V4 Pro profile on the NVIDIA Inference | ||
| # Hub — weak-only baseline for the escalation-router comparison. | ||
| # Requires a LiteLLM virtual key (sk-...): | ||
| # export SWITCHYARD_WEAK_API_KEY=sk-... | ||
| # Run with: | ||
| # benchmark/run-baseline.sh \ | ||
| # --harbor-path benchmark/datasets/openthoughts-tblite-closed-book \ | ||
| # --routing-profiles benchmark/routing-profiles/tb-lite-single-deepseek-v4-pro-nvidia.yaml \ | ||
| # --model tb-lite-single-deepseek-v4-pro-nvidia | ||
|
|
||
| defaults: | ||
| api_key: ${SWITCHYARD_WEAK_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| timeout_secs: 600.0 | ||
|
|
||
| routes: | ||
| tb-lite-single-deepseek-v4-pro-nvidia: | ||
| type: model | ||
| target: | ||
| # The evals- batch-gateway variant avoids the regular gateway's 6-min | ||
| # timeout under high concurrency but is currently failing TLS | ||
| # verification upstream; switch back once it probes clean. | ||
| model: nvidia/deepseek-ai/deepseek-v4-pro | ||
| format: openai | ||
| extra_headers: | ||
| X-Inference-Priority: batch | ||
| # Thinking on, matching the escalation profile's weak tier so the | ||
| # weak-only baseline stays apples-to-apples. | ||
| extra_body: | ||
| chat_template_kwargs: | ||
| enable_thinking: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Serve-backed single-model Opus 4.7 profile on the NVIDIA Inference Hub — | ||
| # strong-only baseline for the escalation-router comparison. | ||
| # Requires a LiteLLM virtual key (sk-...): | ||
| # export SWITCHYARD_STRONG_API_KEY=sk-... | ||
| # Run with: | ||
| # benchmark/run-baseline.sh \ | ||
| # --harbor-path benchmark/datasets/openthoughts-tblite-closed-book \ | ||
| # --routing-profiles benchmark/routing-profiles/tb-lite-single-opus-4-7-nvidia.yaml \ | ||
| # --model tb-lite-single-opus-4-7-nvidia | ||
|
|
||
| defaults: | ||
| api_key: ${SWITCHYARD_STRONG_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| timeout_secs: 600.0 | ||
|
|
||
| routes: | ||
| tb-lite-single-opus-4-7-nvidia: | ||
| type: model | ||
| target: | ||
| model: aws/anthropic/bedrock-claude-opus-4-7 | ||
| format: openai |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Serve-backed single-model Opus 4.8 profile on the NVIDIA Inference Hub — | ||
| # strong-only baseline for the escalation-router comparison. | ||
| # Requires a LiteLLM virtual key (sk-...): | ||
| # export SWITCHYARD_STRONG_API_KEY=sk-... | ||
| # Run with: | ||
| # benchmark/run-baseline.sh \ | ||
| # --harbor-path benchmark/datasets/openthoughts-tblite-closed-book \ | ||
| # --routing-profiles benchmark/routing-profiles/tb-lite-single-opus-4-8-nvidia.yaml \ | ||
| # --model tb-lite-single-opus-4-8-nvidia | ||
|
|
||
| defaults: | ||
| api_key: ${SWITCHYARD_STRONG_API_KEY} | ||
| base_url: https://inference-api.nvidia.com/v1 | ||
| timeout_secs: 600.0 | ||
|
|
||
| routes: | ||
| tb-lite-single-opus-4-8-nvidia: | ||
| type: model | ||
| target: | ||
| model: aws/anthropic/bedrock-claude-opus-4-8 | ||
| format: openai |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA-NeMo/Switchyard
Length of output: 1966
Do not ignore additional job results in
benchmark/compare_runs.py:25-28. The glob can match multiplejobs/*/result.jsonfiles, but onlyresults[0]is read, so multi-job runs will report a single lexicographically first job instead of the run as a whole. Aggregate the results or fail fast when more than one file is present.🤖 Prompt for AI Agents