Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
31d90a0
feat(router): add escalation_router profile with judge-latched strong…
linj-glitch Jul 11, 2026
411620e
feat(router): ESC8 escalation-judge campaign through ESC7 + prompt ge…
linj-glitch Jul 13, 2026
ee550bd
feat(router): ESC9 judge prompt — impossibility-holds gate, setup-gri…
linj-glitch Jul 14, 2026
5a07418
revert(router): restore ESC7 judge prompt as benchmark line of record
linj-glitch Jul 14, 2026
e85396e
refactor(router): load judge prompt from package data; add judge.prom…
linj-glitch Jul 15, 2026
7495a39
refactor(router): reuse session-affinity key resolution for judge str…
linj-glitch Jul 15, 2026
04b0f6e
docs(cli): tighten the escalation route-bundle docstring, point at th…
linj-glitch Jul 15, 2026
92072b5
fix(router): wire judge_timeout_s through the escalation profile buil…
linj-glitch Jul 15, 2026
0e3bc56
docs(router): clarify the judge starts at a configurable minimum turn
linj-glitch Jul 15, 2026
5ece2a5
docs(router): fix repeated-trial benchmarking anchor (MD051)
linj-glitch Jul 15, 2026
3caf3fe
docs(router): align escalation routing guide
linj-glitch Jul 13, 2026
f6824de
refactor(session): fold the judge's deep benchmark key into session_k…
linj-glitch Jul 15, 2026
ef6f47a
refactor(processors): share message-condensing helpers across classif…
linj-glitch Jul 15, 2026
6e588a6
fix(router): key escalation tiers by the configured target ids so ove…
linj-glitch Jul 15, 2026
084edbd
fix(router): only send the vLLM thinking-off hint to judge models tha…
linj-glitch Jul 15, 2026
ff71fba
fix(processors): render Anthropic tool_use blocks in condensed transc…
linj-glitch Jul 15, 2026
11ac385
feat(router): add judge completion-budget and opt-in verdict-dump kno…
linj-glitch Jul 15, 2026
d556ffd
refactor(cli): forward escalation route options verbatim so the confi…
linj-glitch Jul 15, 2026
7cf9452
refactor(profiles): share one two-tier target and backend builder bet…
linj-glitch Jul 15, 2026
6e3f5f0
fix(router): key deterministic tiers by the configured target ids so …
linj-glitch Jul 15, 2026
34d5ba9
docs(router): correct fallback-id and verdict-dump wording; cover the…
linj-glitch Jul 15, 2026
48af4ba
refactor(cli): surface escalation config errors as route-bundle diagn…
linj-glitch Jul 15, 2026
f3f58aa
chore(benchmark): opt escalation profiles into judge verdict dumps
linj-glitch Jul 16, 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
9 changes: 5 additions & 4 deletions .agents/skills/switchyard-lib-core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ right validation set. If the change is driven by a launcher need, also read
| An OpenAI-compatible provider target such as NVIDIA Inference Hub or OpenRouter | Use the existing OpenAI-compatible backend/profile with `base_url`, `api_key`, and model id wiring. Add a new backend only when the provider has a real wire-format, auth, retry, or health contract that cannot fit that path. |
| Direct Rust component bindings | Add concrete PyO3 classes under `crates/switchyard-py/src/component_bindings/`, keep config bindings near the component binding that consumes them, and expose them lazily from `switchyard_rust/components.py`. Do not keep growing `core_bindings.rs` or `switchyard_rust/core.py` with concrete component classes. |
| Route YAML / model dispatch | Use `switchyard/cli/route_bundle.py` and `switchyard/lib/route_table_builders.py`. They build `RouteTable` entries from profile-backed runtimes and keep launchers plus `switchyard serve --routing-profiles` on one path. |
| Shared/persistent session-affinity pins across workers or pod churn | Configure the latency route with `session_affinity: true` + `affinity_store: redis` + `affinity_store_url` (optional `affinity_store_ttl_seconds`, `affinity_key_prefix`). `SessionAffinity` keeps the Rust `SessionCache` as L1 and reads/writes through the `AffinityPinStore` L2 (`switchyard/lib/redis_pin_store.py`), fail-open behind a 0.1s socket timeout and a 3-failure/10s-cooldown circuit breaker (`switchyard_affinity_l2_breaker_open` gauge). Requires the `switchyard[affinity-redis]` extra. |
| Shared/persistent session-affinity pins across workers or pod churn | Configure the latency route with `session_affinity: true` + `affinity_store: redis` + `affinity_store_url` (optional `affinity_store_ttl_seconds`, `affinity_key_prefix`); the escalation_router route takes the same `affinity_store*` keys (no `session_affinity` flag — its latch is always on; default prefix `swyd:esc:`). `SessionAffinity` keeps the Rust `SessionCache` as L1 and reads/writes through the `AffinityPinStore` L2 (`switchyard/lib/redis_pin_store.py`), fail-open behind a 0.1s socket timeout and a 3-failure/10s-cooldown circuit breaker (`switchyard_affinity_l2_breaker_open` gauge). Requires the `switchyard[affinity-redis]` extra. |
| Stats / telemetry | Reuse `StatsRequestProcessor`, `StatsResponseProcessor`, `StatsLlmBackend`, and `StatsAccumulator`. A profile config should thread one accumulator through all three when stats are enabled. Do not write a parallel collector. |
| A fixed-path endpoint contributed by per-route components | Set `Endpoint.register_once = True`; `build_switchyard_app(...)` mounts the first instance while still running every component's lifecycle. Leave the default `False` for configurable endpoint classes that may mount distinct instances. |
| Per-endpoint attribution on `/metrics` for a Python backend that can't be wrapped by `StatsLlmBackend` | Set `ctx.selected_model = endpoint_id` before returning the response. Also set `ctx.backend_call_latency_ms = upstream_call_ms` so the response processor can compute routing overhead. `LatencyServiceLLMBackend.call` is the reference. |
Expand All @@ -62,9 +62,9 @@ Profile-owned construction keeps the full behavior in one reviewable module:
5. Use `ProfileSwitchyard(config.build())` only when serving through the
existing Python endpoint contract.

Random routing, deterministic routing, plan-execute, passthrough, no-op,
stage-router, latency-service, RouteLLM, and OSS-router profiles are the reference
set under `switchyard/lib/profiles/`.
Random routing, deterministic routing, escalation-router, plan-execute,
passthrough, no-op, stage-router, latency-service, RouteLLM, and OSS-router
profiles are the reference set under `switchyard/lib/profiles/`.

## Anti-Patterns

Expand All @@ -76,6 +76,7 @@ set under `switchyard/lib/profiles/`.
| A new role-shaped abstraction invented outside the backend role. | Keep pre-call logic in request-side profile code or plain request components, call logic in `LLMBackend`, post-call logic in response-side profile code or plain response components, and final wire conversion in `TranslationEngine`. |
| Adding an OpenRouter-specific backend or translator just to point at `https://openrouter.ai/api/v1`. | Route it through the OpenAI-compatible backend/profile and provider configuration. Keep provider-specific code for actual protocol differences. |
| Making route YAML declare arbitrary Python processors. | Route YAML is deployment config for supported profile route types. Runtime-only hooks such as intake are injected by the caller through the route-table builder kwargs. |
| Hand-billing token costs from raw stats fields — charging `prompt_tokens` at the fresh-input rate and adding `cached_tokens` on top. | `prompt_tokens` in stats snapshots is **total** input: `uncached + cached_tokens + cache_creation_tokens` (Anthropic's three sibling usage counters are normalized into it). Fresh input = `prompt − cached − creation`. Use the built-in estimators — the snapshot's own `cost_estimate`, `estimate_model_cost()` in `cost_estimator.py`, or Rust `stats/cost.rs` — instead of ad-hoc math; ignoring the subtraction overstates cost ~5× on cache-heavy agentic traffic. |

## When Something Genuinely Doesn't Fit

Expand Down
126 changes: 126 additions & 0 deletions benchmark/compare_runs.py
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())
Comment on lines +25 to +28

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

fd -a '^result\.json$' benchmark -x sh -c '
  case "$1" in
    */jobs/*/result.json) printf "%s\n" "$1" ;;
  esac
' sh {} | head -100

rg -n -C3 'jobs/\*/result\.json|n_completed_trials|reward_stats' benchmark tests

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 multiple jobs/*/result.json files, but only results[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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/compare_runs.py` around lines 25 - 28, Update the result-loading
logic around results in the benchmark comparison flow so it does not silently
use only results[0] when multiple job result files exist. Either aggregate all
matched result.json files into the comparison input or explicitly raise an error
when more than one file is present, while preserving the existing
missing-results failure behavior.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 tests/test_run_baseline_script.py contains stats.evals.*.n_trials but no stats.n_completed_trials or stats.n_errored_trials; these direct subscripts therefore crash the utility. Use safe reads and derive a sensible completed count when the aggregate fields are absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/compare_runs.py` around lines 72 - 73, Update the trial-count
extraction in the run comparison rows to safely handle missing harbor-level
n_completed_trials and n_errored_trials fields. Reuse the available
stats.evals.*.n_trials data to derive a sensible completed-trials count when the
aggregate completed field is absent, while preserving existing values when
present and avoiding direct-subscript KeyErrors.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 || true

Repository: 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 -S

Repository: 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 -S

Repository: 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.py

Repository: NVIDIA-NeMo/Switchyard

Length of output: 11983


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' switchyard/lib/cost_estimator.py

Repository: 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 -S

Repository: 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 -S

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '140,320p' switchyard/lib/live_stats_collector.py

Repository: NVIDIA-NeMo/Switchyard

Length of output: 5875


Add pricing aliases for the exact model IDs used here. anthropic/claude-opus-4.7 and google/gemini-3.5-flash don’t match the current cost-table keys, so this profile’s strong/judge spend will show up as unknown and underreported in routing stats.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/routing-profiles/tb-lite-escalation-opus-deepseek-gemini.yaml`
around lines 12 - 29, Add pricing-table aliases for the exact model IDs
anthropic/claude-opus-4.7 and google/gemini-3.5-flash, mapping each to its
corresponding existing cost entry so strong and judge usage is reported instead
of unknown. Leave the profile configuration unchanged.

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
24 changes: 24 additions & 0 deletions benchmark/routing-profiles/tb-lite-single-opus-4-7-nvidia.yaml
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
24 changes: 24 additions & 0 deletions benchmark/routing-profiles/tb-lite-single-opus-4-8-nvidia.yaml
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
20 changes: 19 additions & 1 deletion crates/switchyard-components/src/stats/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,28 @@ fn raw_model_price(model: &str) -> Option<ModelPrice> {
cached: 0.15,
cache_write: 1.50,
},
"aws/anthropic/bedrock-claude-opus-4-7"
// Nemotron 3 Ultra (550B/55B MoE) — OpenRouter reference list
// price, June 2026. evals-N ids are parallel benchmarking-gateway
// deployments of the same model.
"nvidia/nvidia/nemotron-3-ultra"
| "openai/nvidia/nvidia/nemotron-3-ultra"
| "nvidia/nvidia/evals-nemotron-ultra"
| "nvidia/nvidia/evals-nemotron-ultra-2"
| "nvidia/nvidia/evals-nemotron-ultra-3"
| "nvidia/nvidia/evals-nemotron-ultra-4" => ModelPrice {
input: 0.50,
output: 2.20,
cached: 0.05,
cache_write: 0.50,
},
"aws/anthropic/bedrock-claude-opus-4-8"
| "aws/anthropic/bedrock-claude-opus-4-7"
| "aws/anthropic/bedrock-claude-opus-4-6"
| "aws/anthropic/bedrock-claude-opus-4-5"
| "azure/anthropic/claude-opus-4-8"
| "azure/anthropic/claude-opus-4-7"
| "azure/anthropic/claude-opus-4-6"
| "claude-opus-4-8"
| "claude-opus-4-7"
| "claude-opus-4-6"
| "claude-opus-4-5" => ModelPrice {
Expand All @@ -204,6 +221,7 @@ fn raw_model_price(model: &str) -> Option<ModelPrice> {
},
"aws/anthropic/bedrock-claude-sonnet-4-6"
| "aws/anthropic/bedrock-claude-sonnet-4-5"
| "azure/anthropic/claude-sonnet-4-5"
| "claude-sonnet-4-6"
| "claude-sonnet-4-5" => ModelPrice {
input: 3.00,
Expand Down
Loading
Loading