Skip to content

Add SkippyBench external eval pack - #865

Merged
i386 merged 8 commits into
mainfrom
codex/skippy-bench-external-evals
Jul 14, 2026
Merged

Add SkippyBench external eval pack#865
i386 merged 8 commits into
mainfrom
codex/skippy-bench-external-evals

Conversation

@i386

@i386 i386 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

What changed

This PR makes SkippyBench the entry point for the core external agent/coding eval pack while keeping the native upstream harnesses and datasets authoritative. SkippyBench wraps, installs, launches, and collects reports; it does not rebuild miniature benchmark datasets or substitute Skippy-owned task filters.

The branch has been updated with current origin/main as of 2026-07-02.

SkippyBench eval commands

Adds the skippy-bench eval command family:

  • eval list
  • eval info
  • eval sync
  • eval install
  • eval doctor
  • eval run

External harness download/install remains explicit through eval sync / eval install; normal builds do not clone benchmark repos, install Python tools, or pull Docker images.

Core benchmark pack

Eval Upstream repo Use when Native run shape
speed-bench ggml-org/llama.cpp SPEED-Bench OpenAI-compatible serving latency, tok/s, and SPEED-Bench traffic Runs upstream speed_bench.py with qualitative/all-category traffic against the configured OpenAI endpoint.
terminal-bench harbor-framework/terminal-bench Terminal agent behavior, shell/task execution, Docker sandbox readiness Installs/runs the Terminal-Bench CLI with the Terminus/OpenAI path and no Skippy-owned task filter.
swe-bench-pro scaleapi/SWE-bench_Pro-os Coding-agent patch generation and issue-resolution prompts Runs upstream instance generation, SWE-agent patch generation, patch gathering, and SWE-Bench Pro evaluation flow.
mcp-atlas scaleapi/mcp-atlas MCP tool-use benchmark flow Starts the native MCP services and runs the upstream completion/scoring scripts with the full Hugging Face dataset path.

Optional future packs remain documented but are not wired in this PR: NL2RepoBench and Toolathlon / Tool-Decathlon.

Metrics and reports

  • Makes metrics-server mandatory for endpoint-driving reportable benchmarks.
  • eval run creates/finalizes a metrics-server run and stores the raw metrics report beside native harness artifacts.
  • chat-corpus now creates/finalizes a metrics-server run, writes telemetry into its JSON output, and fails if metrics export is unavailable.
  • Adds normalized harness metrics where available: request/failure counts, latency, token counts, tok/s, pass rate, draft acceptance, and raw artifact paths.
  • Adds telemetry-derived request latency, generation latency, TTFT/FTTT, request count, and span count from metrics-server report.json.
  • Adds --endpoint-concurrency and validates harness-level request concurrency against the serving endpoint concurrency so benchmark workers do not overload a smaller Skippy lane count.

Skippy/OpenAI compatibility

  • Requires benchmark-managed Skippy server runs to use release target/release/skippy-server instead of debug builds.
  • Configures standalone skippy-server serve-openai with metrics-only guardrails by default so weak local model output is recorded as model behavior instead of becoming transport-level HTTP 400 failure.
  • Allows structured-output response_format: { type: "json_schema" } through the Skippy runtime feature guard so native eval clients can send their usual requests.
  • Adds local JSON Schema $defs / $ref support in structured-output validation.
  • Keeps the main branch decode-batcher changes while wrapping standalone Skippy OpenAI serving with the new guardrail/context-limit layer.

Skill and docs

  • Updates crates/skippy-bench/README.md with the eval command flow, metrics-server requirements, external benchmark table, use-case guidance, and optional later packs.
  • Updates .agents/skills/skippy-bench/SKILL.md so Codex knows:
    • which external eval to use for which use case
    • external evals must use native upstream harnesses/datasets
    • metrics-server correlation is required for TTFT/FTTT and other endpoint telemetry
    • endpoint/harness concurrency must match
    • benchmark-managed Skippy server runs must use release skippy-server

Validation

Ran on the updated-with-main branch:

  • cargo fmt --all -- --check
  • just with-lld cargo check -p skippy-bench -p skippy-server -p openai-frontend
  • just with-lld cargo clippy -p skippy-bench -p skippy-server -p openai-frontend --all-targets -- -D warnings
  • just with-lld cargo test -p skippy-bench --bins
  • just with-lld cargo test -p openai-frontend guardrails
  • just with-lld cargo test -p skippy-server --lib frontend

Notes:

  • lab-experiments/ benchmark artifacts remain untracked and are not part of this PR.
  • The obsolete standalone llama patch 0078-Contain-chat-grammar-sampler-exceptions.patch was dropped during the main merge because current main has the chat-grammar containment behavior in the squashed patch queue.

Summary by CodeRabbit

  • New Features

    • Added a new eval workflow for running, syncing, inspecting, and validating external benchmark packs.
    • Improved structured-output handling, including direct JSON responses and local schema references.
    • Added a new OpenAI guardrails mode with disabled, metrics, and enforce options.
  • Bug Fixes

    • Structured outputs are now accepted in more chat paths.
    • Benchmark runs now use release builds and clearer metrics reporting, reducing false failures.
  • Documentation

    • Expanded benchmark and eval usage guidance, including metrics-server requirements and prerequisites.

@i386 i386 changed the title [codex] Add SkippyBench external eval pack Add SkippyBench external eval pack Jun 17, 2026
@i386
i386 force-pushed the codex/skippy-bench-external-evals branch from dcecac4 to f7d7ece Compare June 17, 2026 06:30
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds local $ref/$defs resolution to guardrails JSON Schema validation, introduces direct structured-output payload classification in the OpenAI frontend, adds a CLI-configurable guardrails mode to skippy-server, and adds a new skippy-bench eval subsystem with metrics telemetry reporting, release-binary enforcement, and documentation.

Changes

Structured Output Support

Layer / File(s) Summary
JSON Schema $ref/$defs resolution
crates/mesh-llm-guardrails/src/structured.rs
Schema and payload validation thread definitions/depth through recursion, resolve #/$defs/<name> refs with depth limits, and add a supporting unit test.
Direct structured payload classification
crates/openai-frontend/src/guardrails/rescue.rs, .../mod.rs, crates/openai-frontend/src/lib.rs, .../tests/response_validation.rs
Adds classify_direct_structured_payload short-circuit for tool-less structured JSON responses, plus re-exports and new tests.
Skippy-server guardrails mode wiring
crates/skippy-server/src/cli.rs, .../frontend.rs, .../frontend/request.rs, .../frontend/tests.rs, scripts/skippy-ci-smoke.sh
Adds --openai-guardrails CLI flag, wraps standalone backend with a compatibility/enforce policy, removes structured-output runtime rejection, and updates smoke test expectations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Skippy-Bench External Evals and Telemetry

Layer / File(s) Summary
Release-binary enforcement
crates/skippy-bench/src/support.rs, .../distributed.rs, .../local_single.rs, .../local_split.rs, .../cli.rs
Adds ensure_release_skippy_server_bin checks and switches default binary paths from target/debug to target/release.
Telemetry-report module
crates/skippy-bench/src/telemetry_report.rs
New module creates/finalizes metrics-server runs and derives TTFT/latency aggregates from spans.
Chat-corpus metrics integration
crates/skippy-bench/src/chat_corpus.rs, .../cli.rs, crates/skippy-bench/README.md
Wires chat-corpus to create/finalize a metrics-server run, tags requests via x-request-id, and embeds telemetry in the report.
External Agent Evals command
crates/skippy-bench/src/evals.rs, .../cli.rs, .../main.rs, Cargo.toml, corpora/bench_corpus_sources.json, README.md, .agents/skills/skippy-bench/SKILL.md
Adds skippy-bench eval command supporting speed-bench/terminal-bench/swe-bench-pro/mcp-atlas packs with sync/doctor/run flows, timeouts, metrics extraction, and documentation.

Estimated code review effort: 5 (Critical) | ~110 minutes

Suggested reviewers: michaelneale

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding SkippyBench external eval pack support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/skippy-bench-external-evals

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@i386
i386 force-pushed the codex/skippy-bench-external-evals branch 4 times, most recently from 779f392 to a787530 Compare June 17, 2026 07:45
@i386
i386 force-pushed the codex/skippy-bench-external-evals branch from a787530 to beba5a7 Compare June 17, 2026 07:49
i386 added 2 commits July 2, 2026 07:15
…ternal-evals

# Conflicts:
#	crates/skippy-server/src/frontend.rs
#	third_party/llama.cpp/patches/0078-Contain-chat-grammar-sampler-exceptions.patch
@i386
i386 marked this pull request as ready for review July 3, 2026 22:22
@github-actions
github-actions Bot requested a review from ndizazzo July 3, 2026 22:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
crates/skippy-server/src/frontend.rs (1)

272-311: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated GuardrailPolicy construction across compatibility_for_skippy/for_standalone_mode's enforce arm.

disabled_for_skippy, compatibility_for_skippy, and the Enforce arm of for_standalone_mode each build a near-identical Self { target: OpenAiGuardrailsTarget::Skippy, policy: GuardrailPolicy { ... }.into(), compaction: None } shape. Could be consolidated into a small private helper (e.g., fn skippy_config(policy: GuardrailPolicy) -> Self) to reduce duplication as more modes are added.

🤖 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 `@crates/skippy-server/src/frontend.rs` around lines 272 - 311, There is
repeated OpenAiGuardrailsConfig construction in disabled_for_skippy,
compatibility_for_skippy, and the Enforce arm of for_standalone_mode; extract
the shared Skippy Self shape into a small private helper such as a skippy_config
constructor that takes a GuardrailPolicy and returns the configured
OpenAiGuardrailsConfig, then have each mode-specific method build only its
policy and delegate to that helper.
crates/openai-frontend/src/lib.rs (1)

35-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New crate-root re-exports added.

RetryExhaustionMode and StreamingGuardrailMode are added to the crate-root pub use guardrails::{...} list. As per coding guidelines, crates/*/src/lib.rs: "Minimize crate-root re-exports. Root re-exports are acceptable as temporary compatibility shims during refactors, but new code should prefer importing from the owning module directly."

Since consumers (e.g., skippy-server) only need these two types, consider importing them from openai_frontend::guardrails directly rather than growing the root re-export surface.

🤖 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 `@crates/openai-frontend/src/lib.rs` around lines 35 - 36, The crate root
re-export list in openai_frontend has been expanded with RetryExhaustionMode and
StreamingGuardrailMode, which should be avoided for new code. Update the
consuming code to import these types directly from openai_frontend::guardrails
instead of relying on the crate-root pub use in lib.rs, and keep the root
re-export surface minimal unless it is needed as a temporary compatibility shim.

Source: Coding guidelines

crates/skippy-bench/src/support.rs (1)

52-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Debug-binary detection is a fragile substring heuristic.

ensure_release_skippy_server_bin only rejects paths containing the literal substring target/debug/skippy-server (or the Windows variant). This can both:

  • False-negative: miss a debug binary launched via a non-canonical relative path (e.g., cwd already inside target/, so the path is just debug/skippy-server), silently letting a debug build corrupt benchmark numbers.
  • False-positive: reject a legitimate release binary that happens to live under a directory whose name coincidentally matches that substring.

Since this guard exists specifically to protect benchmark result integrity (debug builds run much slower / with different codegen), consider checking path components rather than a raw substring match, e.g. via Path::components() looking for adjacent target/debug segments plus a skippy-server file stem, ideally after canonicalizing the path.

♻️ Proposed refactor using path components instead of substring matching
 pub fn ensure_release_skippy_server_bin(path: &Path) -> Result<()> {
-    let path = path.to_string_lossy();
-    let debug_path = path.contains("target/debug/skippy-server")
-        || path.contains("target\\debug\\skippy-server");
+    let is_skippy_server = path
+        .file_stem()
+        .and_then(|stem| stem.to_str())
+        .is_some_and(|stem| stem == "skippy-server");
+    let components: Vec<_> = path.components().collect();
+    let debug_path = is_skippy_server
+        && components.windows(2).any(|window| {
+            matches!(window[0].as_os_str().to_str(), Some("target"))
+                && matches!(window[1].as_os_str().to_str(), Some("debug"))
+        });
     if debug_path {
         bail!(
             "SkippyBench benchmark-managed skippy-server runs require a release binary; run `just release-build` and use --stage-server-bin target/release/skippy-server"
         );
     }
     Ok(())
 }
🤖 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 `@crates/skippy-bench/src/support.rs` around lines 52 - 63, The debug-binary
check in ensure_release_skippy_server_bin is using a fragile substring match on
the input Path. Replace the raw path.contains logic with a path-component based
check (optionally after canonicalizing) so it only rejects actual
target/debug/skippy-server binaries and does not miss relative debug paths or
falsely reject unrelated release binaries. Use the
ensure_release_skippy_server_bin function and its debug_path detection block as
the location to update.
crates/skippy-bench/README.md (1)

208-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doc doesn't mention that a telemetry fetch failure fails the whole eval run even when the harness itself succeeded.

This section explains what the telemetry block records, but doesn't call out that eval run currently marks report.success = false (and the command exits non-zero) whenever the metrics-server report can't be fetched at finalize time — independent of whether the native harness itself passed. Given how expensive some of these harnesses are (SWE-Bench Pro, MCP-Atlas can run for hours), documenting this failure mode explicitly would help operators avoid discarding a valid harness result due to a collector-side blip. See related comment on evals.rs::run_eval.

🤖 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 `@crates/skippy-bench/README.md` around lines 208 - 220, Update the eval run
telemetry docs in README to explicitly state that a metrics-server fetch failure
during finalize causes the overall eval to fail, even if the native harness
succeeded. Mention that when SkippyBench cannot fetch
`/v1/runs/<run-id>/report.json` in the finalize path, `report.success` is set to
false and `eval run` exits non-zero. Keep the note near the existing telemetry
block description and reference the `eval run`, `metrics-server`, and `run_eval`
behavior so readers can find the failure mode quickly.
.agents/skills/skippy-bench/SKILL.md (1)

150-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same telemetry/success-coupling caveat applies here as in evals.rs/chat_corpus.rs.

This section documents that eval run/chat-corpus "fail if the metrics report cannot be exported," which matches current behavior — but doesn't clarify that this failure applies even when the underlying benchmark/harness itself completed successfully (see the related comment on evals.rs::run_eval). Worth a follow-up doc note once/if that behavior is revisited.

🤖 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 @.agents/skills/skippy-bench/SKILL.md around lines 150 - 156, Clarify the
success/coupling behavior in the SKILL.md telemetry section: the `skippy-bench
eval run` and `skippy-bench chat-corpus` paths in this document should state
that metrics export failure causes the command to fail even if the benchmark
itself completed successfully. Update the wording near the TTFT/FTTT guidance
and the `metrics-server` run/export note to make this explicit, and keep the
reference consistent with the behavior documented in `evals.rs` and
`chat_corpus.rs`.
crates/skippy-bench/src/cli.rs (1)

77-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

EvalListArgs lacks a --pack filter, unlike EvalSyncArgs/EvalDoctorArgs.

list_evals always calls selected_evals(&[], EvalPack::Core) regardless of any pack selection, since EvalListArgs has no pack field. This is harmless today (only one pack, core, exists), but will silently ignore pack scoping once a second pack is added, unlike sync/doctor which both expose --pack.

🤖 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 `@crates/skippy-bench/src/cli.rs` around lines 77 - 84, EvalListArgs is missing
a pack selector, so list_evals cannot honor pack scoping like EvalSyncArgs and
EvalDoctorArgs do. Add a pack field to EvalListArgs with the same CLI behavior
as the other arg structs, then update list_evals to pass the chosen pack into
selected_evals instead of always using EvalPack::Core; use the existing EvalPack
handling in EvalSyncArgs and EvalDoctorArgs as the reference.
crates/skippy-bench/src/evals.rs (1)

356-395: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Failures before/around the harness invocation skip writing run.json, losing debug context.

If create_metrics_run (line 357) or the initial spawn() inside run_command_with_timeout (propagated via ? at line 361-367) fails, run_eval returns early without ever reaching the fs::write(&report_path, ...) call at line 384-386. The already-computed command (full shell invocation), run_dir, and pending-telemetry state are lost, making post-mortem debugging of failed benchmark launches harder — especially since these are often run unattended/in CI.

Consider writing a partial run.json (with success: false and whatever context is available) before propagating these early errors.

🤖 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 `@crates/skippy-bench/src/evals.rs` around lines 356 - 395, Failures in
run_eval before or during the harness launch currently return early and skip
writing run.json, so partial context is lost. Update run_eval to persist a
partial report to report_path before propagating errors from create_metrics_run
or run_command_with_timeout, marking report.success as false and including
whatever command/run_dir/telemetry context is available. Keep the existing final
write path intact, but ensure these early failure paths also serialize the
report so debugging information is always preserved.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/mesh-llm-guardrails/src/structured.rs`:
- Around line 55-56: Reject malformed $defs values instead of treating them as
absent. In structured.rs, the validation entry point that calls
validate_supported_schema_inner and the recursive $defs handling inside
validate_supported_schema_inner should first detect whether "$defs" is present;
if it is present and not an object, return a validation error rather than
passing None. Keep the existing object path for valid $defs and preserve the
current $ref validation flow, but ensure a non-object $defs never silently
passes.

In `@crates/openai-frontend/src/guardrails/rescue.rs`:
- Around line 455-459: Direct structured classification in `rescue.rs` is taking
priority over tool-call-shaped JSON, which makes the synthetic structured
tool-call path unreachable. Update the flow in
`classify_direct_structured_payload` and the `InvalidStructuredPayload` handling
so JSON objects shaped like `{"tool_calls":[...]}` are detected and routed
through `raw_tool_calls_from_value` before returning a structured-payload
failure. Preserve the existing direct structured path for true schema-matching
responses, but add an early tool-call-shape check in the rescue/classification
logic to avoid stealing these payloads.

In `@crates/skippy-bench/src/chat_corpus.rs`:
- Around line 113-135: The `run_config` built in `chat_corpus.rs` is using a
`"run_id"` key that gets overwritten by `telemetry_report::create_run_body` when
`metrics_run_id` is injected, so the chat-corpus run id is lost whenever the two
ids differ. Update the `run_config` construction in this call site to use a
distinct field name like `chat_corpus_run_id`, matching the approach used in
`evals.rs::create_metrics_run`, so both ids are preserved in the posted metrics
config.

In `@crates/skippy-bench/src/evals.rs`:
- Around line 1047-1054: The `mcp_atlas_metrics` function is silently
undercounting `request_count` by filtering out CSV parse errors in
`reader.records()`. Update this logic to detect and surface malformed rows
instead of dropping them quietly: either propagate the first parse error or
count/report failed records alongside the valid count, and make sure the final
`EvalMetrics` computation reflects any parse failures rather than only
successful rows.
- Around line 356-382: The telemetry finalization block in evals::run currently
flips report.success to false when collect_telemetry fails, which incorrectly
marks an otherwise successful harness run as failed. Update the error handling
around collect_telemetry so telemetry_unavailable is recorded in
report.telemetry without changing the harness outcome stored in report.success,
or make this failure-to-fail behavior conditional on an explicit flag. Keep the
logic localized to the collect_telemetry/report.telemetry handling in
evals::run.

In `@crates/skippy-bench/src/telemetry_report.rs`:
- Around line 18-19: The telemetry report currently exposes fttt_ms as a
separate metric, but TelemetryReportBuilder is populating it from the same
ttft_values as ttft_ms, so the two fields are always identical. In
telemetry_report.rs, update the TelemetryReport/TelemetryReportBuilder flow so
fttt_ms is only set from a genuinely distinct signal if one exists, otherwise
remove the redundant field and its aggregation from the builder and any report
serialization/consumers.

---

Nitpick comments:
In @.agents/skills/skippy-bench/SKILL.md:
- Around line 150-156: Clarify the success/coupling behavior in the SKILL.md
telemetry section: the `skippy-bench eval run` and `skippy-bench chat-corpus`
paths in this document should state that metrics export failure causes the
command to fail even if the benchmark itself completed successfully. Update the
wording near the TTFT/FTTT guidance and the `metrics-server` run/export note to
make this explicit, and keep the reference consistent with the behavior
documented in `evals.rs` and `chat_corpus.rs`.

In `@crates/openai-frontend/src/lib.rs`:
- Around line 35-36: The crate root re-export list in openai_frontend has been
expanded with RetryExhaustionMode and StreamingGuardrailMode, which should be
avoided for new code. Update the consuming code to import these types directly
from openai_frontend::guardrails instead of relying on the crate-root pub use in
lib.rs, and keep the root re-export surface minimal unless it is needed as a
temporary compatibility shim.

In `@crates/skippy-bench/README.md`:
- Around line 208-220: Update the eval run telemetry docs in README to
explicitly state that a metrics-server fetch failure during finalize causes the
overall eval to fail, even if the native harness succeeded. Mention that when
SkippyBench cannot fetch `/v1/runs/<run-id>/report.json` in the finalize path,
`report.success` is set to false and `eval run` exits non-zero. Keep the note
near the existing telemetry block description and reference the `eval run`,
`metrics-server`, and `run_eval` behavior so readers can find the failure mode
quickly.

In `@crates/skippy-bench/src/cli.rs`:
- Around line 77-84: EvalListArgs is missing a pack selector, so list_evals
cannot honor pack scoping like EvalSyncArgs and EvalDoctorArgs do. Add a pack
field to EvalListArgs with the same CLI behavior as the other arg structs, then
update list_evals to pass the chosen pack into selected_evals instead of always
using EvalPack::Core; use the existing EvalPack handling in EvalSyncArgs and
EvalDoctorArgs as the reference.

In `@crates/skippy-bench/src/evals.rs`:
- Around line 356-395: Failures in run_eval before or during the harness launch
currently return early and skip writing run.json, so partial context is lost.
Update run_eval to persist a partial report to report_path before propagating
errors from create_metrics_run or run_command_with_timeout, marking
report.success as false and including whatever command/run_dir/telemetry context
is available. Keep the existing final write path intact, but ensure these early
failure paths also serialize the report so debugging information is always
preserved.

In `@crates/skippy-bench/src/support.rs`:
- Around line 52-63: The debug-binary check in ensure_release_skippy_server_bin
is using a fragile substring match on the input Path. Replace the raw
path.contains logic with a path-component based check (optionally after
canonicalizing) so it only rejects actual target/debug/skippy-server binaries
and does not miss relative debug paths or falsely reject unrelated release
binaries. Use the ensure_release_skippy_server_bin function and its debug_path
detection block as the location to update.

In `@crates/skippy-server/src/frontend.rs`:
- Around line 272-311: There is repeated OpenAiGuardrailsConfig construction in
disabled_for_skippy, compatibility_for_skippy, and the Enforce arm of
for_standalone_mode; extract the shared Skippy Self shape into a small private
helper such as a skippy_config constructor that takes a GuardrailPolicy and
returns the configured OpenAiGuardrailsConfig, then have each mode-specific
method build only its policy and delegate to that helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d04082-f520-4c52-925a-7bb1b70b09e1

📥 Commits

Reviewing files that changed from the base of the PR and between b4b33ef and 5894bb6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • .agents/skills/skippy-bench/SKILL.md
  • crates/mesh-llm-guardrails/src/structured.rs
  • crates/openai-frontend/src/guardrails/mod.rs
  • crates/openai-frontend/src/guardrails/rescue.rs
  • crates/openai-frontend/src/guardrails/tests/response_validation.rs
  • crates/openai-frontend/src/lib.rs
  • crates/skippy-bench/Cargo.toml
  • crates/skippy-bench/README.md
  • crates/skippy-bench/corpora/bench_corpus_sources.json
  • crates/skippy-bench/src/chat_corpus.rs
  • crates/skippy-bench/src/cli.rs
  • crates/skippy-bench/src/distributed.rs
  • crates/skippy-bench/src/evals.rs
  • crates/skippy-bench/src/local_single.rs
  • crates/skippy-bench/src/local_split.rs
  • crates/skippy-bench/src/main.rs
  • crates/skippy-bench/src/support.rs
  • crates/skippy-bench/src/telemetry_report.rs
  • crates/skippy-server/src/cli.rs
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/request.rs
  • crates/skippy-server/src/frontend/tests.rs
  • scripts/skippy-ci-smoke.sh
💤 Files with no reviewable changes (2)
  • crates/skippy-server/src/frontend/request.rs
  • crates/skippy-bench/corpora/bench_corpus_sources.json

Comment on lines +55 to +56
let definitions = schema.get("$defs").and_then(Value::as_object);
validate_supported_schema_inner(schema, definitions, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-object $defs instead of ignoring it.

Line 55 and Line 275 currently treat a present-but-malformed $defs value as absent. That lets schemas like "$defs": [] pass validation when no $ref forces lookup, even though $defs must be an object for this supported-schema subset.

Proposed fix
 fn validate_supported_schema(schema: &Value) -> Result<(), UnsupportedStructuredSchema> {
-    let definitions = schema.get("$defs").and_then(Value::as_object);
+    let definitions = match schema.get("$defs") {
+        Some(Value::Object(definitions)) => Some(definitions),
+        Some(_) => return Err(UnsupportedStructuredSchema),
+        None => None,
+    };
     validate_supported_schema_inner(schema, definitions, 0)
 }
@@
 ) -> Result<(), UnsupportedStructuredSchema> {
-    let Some(Value::Object(local_definitions)) = object.get("$defs") else {
+    let Some(definitions_value) = object.get("$defs") else {
         return Ok(());
     };
+    let Value::Object(local_definitions) = definitions_value else {
+        return Err(UnsupportedStructuredSchema);
+    };
     for definition in local_definitions.values() {
         validate_supported_schema_inner(definition, definitions, depth + 1)?;
     }

Also applies to: 270-276

🤖 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 `@crates/mesh-llm-guardrails/src/structured.rs` around lines 55 - 56, Reject
malformed $defs values instead of treating them as absent. In structured.rs, the
validation entry point that calls validate_supported_schema_inner and the
recursive $defs handling inside validate_supported_schema_inner should first
detect whether "$defs" is present; if it is present and not an object, return a
validation error rather than passing None. Keep the existing object path for
valid $defs and preserve the current $ref validation flow, but ensure a
non-object $defs never silently passes.

Comment on lines +455 to +459
if let Some(classified) =
classify_direct_structured_payload(prepared, value, parser_stage, finish_reason)
{
return classified;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t let direct structured classification steal tool-call-shaped payloads.

Line 455 runs before raw_tool_calls_from_value, and Line 692 returns InvalidStructuredPayload for any object that fails the response schema. That makes the existing synthetic structured tool-call branch unreachable for JSON content shaped like {"tool_calls":[...]} on structured-output requests without real tools.

Proposed fix
 fn classify_direct_structured_payload(
     prepared: &PreparedGuardrailRequest,
     value: &Value,
     parser_stage: GuardrailParserStage,
     finish_reason: Option<FinishReason>,
 ) -> Option<ClassifiedGuardrailResponse> {
     if prepared.state.request_contract.has_real_tools() {
         return None;
     }
     let spec = prepared.state.request_contract.structured_output_spec()?;
     value.as_object()?;
+    if raw_tool_calls_from_value(value).is_some_and(|tool_calls| !tool_calls.is_empty()) {
+        return None;
+    }
     let valid_payload = spec.validate_payload(value).is_ok();

Also applies to: 681-692

🤖 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 `@crates/openai-frontend/src/guardrails/rescue.rs` around lines 455 - 459,
Direct structured classification in `rescue.rs` is taking priority over
tool-call-shaped JSON, which makes the synthetic structured tool-call path
unreachable. Update the flow in `classify_direct_structured_payload` and the
`InvalidStructuredPayload` handling so JSON objects shaped like
`{"tool_calls":[...]}` are detected and routed through
`raw_tool_calls_from_value` before returning a structured-payload failure.
Preserve the existing direct structured path for true schema-matching responses,
but add an early tool-call-shape check in the rescue/classification logic to
avoid stealing these payloads.

Comment on lines +113 to +135
let run_id = args
.run_id
.clone()
.unwrap_or_else(generate_chat_corpus_run_id);
let metrics_run_id = args
.metrics_run_id
.clone()
.unwrap_or_else(|| run_id.clone());
let metrics_http = args.metrics_http.trim_end_matches('/').to_string();
let metrics_report_output = metrics_report_output_path(&args, &metrics_run_id);
let run_config = json!({
"mode": "skippy-bench-chat-corpus",
"run_id": &run_id,
"model": &args.model,
"base_url": &args.base_url,
"stream": args.stream,
"request_count": prompts.len(),
"prompt_corpus": args.prompt_corpus.as_ref().map(|path| path.display().to_string()),
"prompt_limit": args.prompt_limit,
"max_tokens": args.max_tokens,
"concurrency_depth": args.concurrency_depth,
});
telemetry_report::create_run(&metrics_http, &metrics_run_id, &run_config)?;

Copy link
Copy Markdown
Contributor

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

run_config's "run_id" field is silently overwritten by metrics_run_id.

telemetry_report::create_run_body (telemetry_report.rs) always force-inserts "run_id": metrics_run_id into the posted config object, overwriting any existing "run_id" key. Here, run_config is built with "run_id": &run_id (the chat-corpus benchmark's own run id) — when --run-id and --metrics-run-id differ, the value actually stored on the metrics-server for run_id inside the config silently becomes metrics_run_id, and the original chat-corpus run id is lost from that record. This makes it harder to correlate/debug runs where the two ids diverge.

The sibling call site in evals.rs::create_metrics_run avoids this exact collision by naming its own field "eval_run_id" instead of "run_id". Consider the same fix here (e.g., rename this field to "chat_corpus_run_id").

🐛 Proposed fix — avoid the field name collision
     let run_config = json!({
         "mode": "skippy-bench-chat-corpus",
-        "run_id": &run_id,
+        "chat_corpus_run_id": &run_id,
         "model": &args.model,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let run_id = args
.run_id
.clone()
.unwrap_or_else(generate_chat_corpus_run_id);
let metrics_run_id = args
.metrics_run_id
.clone()
.unwrap_or_else(|| run_id.clone());
let metrics_http = args.metrics_http.trim_end_matches('/').to_string();
let metrics_report_output = metrics_report_output_path(&args, &metrics_run_id);
let run_config = json!({
"mode": "skippy-bench-chat-corpus",
"run_id": &run_id,
"model": &args.model,
"base_url": &args.base_url,
"stream": args.stream,
"request_count": prompts.len(),
"prompt_corpus": args.prompt_corpus.as_ref().map(|path| path.display().to_string()),
"prompt_limit": args.prompt_limit,
"max_tokens": args.max_tokens,
"concurrency_depth": args.concurrency_depth,
});
telemetry_report::create_run(&metrics_http, &metrics_run_id, &run_config)?;
let run_config = json!({
"mode": "skippy-bench-chat-corpus",
"chat_corpus_run_id": &run_id,
"model": &args.model,
"base_url": &args.base_url,
"stream": args.stream,
"request_count": prompts.len(),
"prompt_corpus": args.prompt_corpus.as_ref().map(|path| path.display().to_string()),
"prompt_limit": args.prompt_limit,
"max_tokens": args.max_tokens,
"concurrency_depth": args.concurrency_depth,
});
telemetry_report::create_run(&metrics_http, &metrics_run_id, &run_config)?;
🤖 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 `@crates/skippy-bench/src/chat_corpus.rs` around lines 113 - 135, The
`run_config` built in `chat_corpus.rs` is using a `"run_id"` key that gets
overwritten by `telemetry_report::create_run_body` when `metrics_run_id` is
injected, so the chat-corpus run id is lost whenever the two ids differ. Update
the `run_config` construction in this call site to use a distinct field name
like `chat_corpus_run_id`, matching the approach used in
`evals.rs::create_metrics_run`, so both ids are preserved in the posted metrics
config.

Comment on lines +356 to +382
if !args.dry_run {
create_metrics_run(&args, &run_id, &metrics_run_id)?;
let started = Instant::now();
let stdout_path = run_dir.join("raw").join("stdout.log");
let stderr_path = run_dir.join("raw").join("stderr.log");
let outcome = run_command_with_timeout(
&command,
args.harness_timeout_secs.map(Duration::from_secs),
&stdout_path,
&stderr_path,
)
.with_context(|| format!("run {}", definition.id.as_str()))?;
let duration_ms = started.elapsed().as_secs_f64() * 1000.0;
report.exit_status = outcome.exit_status;
report.success = outcome.success;
report.timed_out = outcome.timed_out;
report.stdout_path = Some(stdout_path.display().to_string());
report.stderr_path = Some(stderr_path.display().to_string());
report.metrics = collect_metrics(definition, &run_dir, duration_ms);
if let Err(error) = collect_telemetry(&metrics_http, &metrics_run_id, &run_dir)
.map(|telemetry| report.telemetry = telemetry)
{
report.telemetry =
telemetry_report::unavailable(&metrics_http, &metrics_run_id, &error);
report.success = false;
}
}

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the surrounding region.
ast-grep outline crates/skippy-bench/src/evals.rs --view expanded >/tmp/evals_outline.txt
sed -n '320,430p' crates/skippy-bench/src/evals.rs | cat -n

# Find report.success and collect_telemetry usage in the file.
rg -n "collect_telemetry|report\.success|timed_out|run\.json|bail!" crates/skippy-bench/src/evals.rs

Repository: Mesh-LLM/mesh-llm

Length of output: 5759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the telemetry helper and report type to understand expected semantics.
rg -n "fn collect_telemetry|struct .*Report|success:" crates/skippy-bench/src -g '!target'

Repository: Mesh-LLM/mesh-llm

Length of output: 1031


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect where run.json is written and whether telemetry failures are treated specially elsewhere.
rg -n "run\.json|serde_json|write.*report|report\.telemetry|telemetry\.status|unavailable\(" crates/skippy-bench/src -g '!target'

Repository: Mesh-LLM/mesh-llm

Length of output: 5780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read a tight window around the telemetry branch and the subsequent reporting logic.
sed -n '360,410p' crates/skippy-bench/src/evals.rs | cat -n

Repository: Mesh-LLM/mesh-llm

Length of output: 2445


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect telemetry handling and compare with another caller that records telemetry.
sed -n '900,1015p' crates/skippy-bench/src/evals.rs | cat -n
sed -n '150,205p' crates/skippy-bench/src/chat_corpus.rs | cat -n
sed -n '130,190p' crates/skippy-bench/src/telemetry_report.rs | cat -n

Repository: Mesh-LLM/mesh-llm

Length of output: 10034


Don’t fail the eval when telemetry finalization fails. collect_telemetry errors currently set report.success = false, so a successful harness run is treated as failed and the process bails just because the metrics server was unavailable at the end. Keep the harness result in report.success and report telemetry failure separately, or gate this behavior behind an explicit flag.

🤖 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 `@crates/skippy-bench/src/evals.rs` around lines 356 - 382, The telemetry
finalization block in evals::run currently flips report.success to false when
collect_telemetry fails, which incorrectly marks an otherwise successful harness
run as failed. Update the error handling around collect_telemetry so
telemetry_unavailable is recorded in report.telemetry without changing the
harness outcome stored in report.success, or make this failure-to-fail behavior
conditional on an explicit flag. Keep the logic localized to the
collect_telemetry/report.telemetry handling in evals::run.

Comment on lines +1047 to +1054
fn mcp_atlas_metrics(run_dir: &Path) -> Result<EvalMetrics> {
let mut reader = csv::Reader::from_path(mcp_atlas_output_path(run_dir))?;
let data_rows = reader.records().filter(|record| record.is_ok()).count();
Ok(EvalMetrics {
request_count: Some(data_rows as u64),
..EvalMetrics::default()
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

MCP-Atlas parse failures are silently dropped from request_count.

reader.records().filter(|record| record.is_ok()).count() silently discards any row that fails to parse as valid CSV, without logging or surfacing how many rows were dropped. If the upstream completion script emits a malformed CSV (e.g., due to an embedded error message), request_count will silently undercount rather than surfacing the discrepancy.

♻️ Suggested fix — surface parse failures
 fn mcp_atlas_metrics(run_dir: &Path) -> Result<EvalMetrics> {
     let mut reader = csv::Reader::from_path(mcp_atlas_output_path(run_dir))?;
-    let data_rows = reader.records().filter(|record| record.is_ok()).count();
+    let (ok_rows, failed_rows): (Vec<_>, Vec<_>) =
+        reader.records().partition(Result::is_ok);
+    if !failed_rows.is_empty() {
+        eprintln!(
+            "warning: mcp-atlas completion CSV had {} unparsable row(s)",
+            failed_rows.len()
+        );
+    }
+    let data_rows = ok_rows.len();
     Ok(EvalMetrics {
         request_count: Some(data_rows as u64),
         ..EvalMetrics::default()
     })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn mcp_atlas_metrics(run_dir: &Path) -> Result<EvalMetrics> {
let mut reader = csv::Reader::from_path(mcp_atlas_output_path(run_dir))?;
let data_rows = reader.records().filter(|record| record.is_ok()).count();
Ok(EvalMetrics {
request_count: Some(data_rows as u64),
..EvalMetrics::default()
})
}
fn mcp_atlas_metrics(run_dir: &Path) -> Result<EvalMetrics> {
let mut reader = csv::Reader::from_path(mcp_atlas_output_path(run_dir))?;
let (ok_rows, failed_rows): (Vec<_>, Vec<_>) =
reader.records().partition(Result::is_ok);
if !failed_rows.is_empty() {
eprintln!(
"warning: mcp-atlas completion CSV had {} unparsable row(s)",
failed_rows.len()
);
}
let data_rows = ok_rows.len();
Ok(EvalMetrics {
request_count: Some(data_rows as u64),
..EvalMetrics::default()
})
}
🤖 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 `@crates/skippy-bench/src/evals.rs` around lines 1047 - 1054, The
`mcp_atlas_metrics` function is silently undercounting `request_count` by
filtering out CSV parse errors in `reader.records()`. Update this logic to
detect and surface malformed rows instead of dropping them quietly: either
propagate the first parse error or count/report failed records alongside the
valid count, and make sure the final `EvalMetrics` computation reflects any
parse failures rather than only successful rows.

Comment on lines +18 to +19
pub ttft_ms: Option<TelemetryAggregate>,
pub fttt_ms: Option<TelemetryAggregate>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

fttt_ms is a duplicate of ttft_ms, not a distinct metric.

fttt_ms is populated with the exact same ttft_values used for ttft_ms (aggregate(&ttft_values) for both, Line 238-239). Every consumer (README/SKILL.md) treats "TTFT/FTTT" as two separate correlated metrics, but as implemented they will always be numerically identical, which is misleading in reports and wastes a field. Either compute FTTT from a genuinely distinct signal (e.g., first-byte/network time vs. decode-token time) or drop the redundant field until that logic exists.

🐛 Likely oversight — same source values reused
-        ttft_ms: aggregate(&ttft_values),
-        fttt_ms: aggregate(&ttft_values),
+        ttft_ms: aggregate(&ttft_values),
+        // TODO: compute a genuinely distinct FTTT signal, or remove this field.
+        fttt_ms: aggregate(&ttft_values),

Also applies to: 230-242

🤖 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 `@crates/skippy-bench/src/telemetry_report.rs` around lines 18 - 19, The
telemetry report currently exposes fttt_ms as a separate metric, but
TelemetryReportBuilder is populating it from the same ttft_values as ttft_ms, so
the two fields are always identical. In telemetry_report.rs, update the
TelemetryReport/TelemetryReportBuilder flow so fttt_ms is only set from a
genuinely distinct signal if one exists, otherwise remove the redundant field
and its aggregation from the builder and any report serialization/consumers.

@ndizazzo ndizazzo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional agent review on top of the code analysis from coderabbit:

Minor Findings

P2 Reproducibility gap: external eval definitions use floating refs like master/main, and run.json does not record the resolved harness commit. For benchmark evidence, this means two “same” runs can use different upstream code. See evals.rs (line 507).

P2 eval sync does not update existing clones: it runs git fetch origin and git checkout , but does not fast-forward/reset the local branch. A stale cache can remain stale while reporting “sync”. See evals.rs (line 781).

P2 --api-key is ignored for speed-bench: EvalRunArgs exposes api_key, and the other adapters pass it through, but speed_bench_command does not. Auth-required endpoints would fail only for this eval. See evals.rs (line 1151).

P3 module shape violates repo policy: the new evals.rs (line 1) is 2,154 lines. It should split into registry/sync/doctor/run plus per-adapter modules before merge.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This pull request has not been updated in at least 5 days. It will be closed after 7 days of inactivity to keep the active review queue current. Please update it within 2 days if the changes are still moving forward.

@github-actions github-actions Bot added the stale label Jul 9, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Closing this pull request because it has not been updated in at least 7 days. Please reopen or create a fresh pull request when the changes are ready to continue.

@github-actions github-actions Bot closed this Jul 12, 2026
@i386 i386 reopened this Jul 12, 2026
@github-actions github-actions Bot removed the stale label Jul 13, 2026
@i386
i386 merged commit 4f25060 into main Jul 14, 2026
89 checks passed
@i386
i386 deleted the codex/skippy-bench-external-evals branch July 14, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants