Skip to content

Add Voice Arena read endpoints - #147

Merged
seribaymadina merged 4 commits into
mainfrom
arena/04-read-api
Jun 19, 2026
Merged

Add Voice Arena read endpoints#147
seribaymadina merged 4 commits into
mainfrom
arena/04-read-api

Conversation

@seribaymadina

@seribaymadina seribaymadina commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Adds the read-only API for the Voice Arena (layer 4 of the build plan).

Endpoints

  • GET /v1/arena/battle returns one battle to vote on

  • GET /v1/arena/battle/{id} returns a specific battle

  • GET /v1/arena/leaderboard?metric=&domain= returns the latest computed board

  • Battles are served blind: the response omits provider/model identities so voting stays unbiased. The A/B to model mapping stays server-side.

  • The leaderboard returns the rows sharing the most recent computed_at for the requested metric and domain, sorted by Elo. It is empty until the snapshot job (a later layer) has run.

  • Battle selection is a placeholder (uniform random). Adaptive pairing replaces it in a later layer; the endpoint contract does not change.

  • Reads the pool directly with raw SQL like the other routers, so this lands independently of the DB access layer PR.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Arena endpoints enabling blind battle voting and leaderboard queries
    • Blind battles conceal model identities to ensure unbiased voting
    • Leaderboard displays model ratings and performance statistics, filterable by metric and domain
    • Rate limiting applied (60 requests per minute per endpoint)

Greptile Summary

This PR adds three read-only Voice Arena endpoints (GET /v1/arena/battle, GET /v1/arena/battle/{id}, and GET /v1/arena/leaderboard) along with their Pydantic schemas and a thorough test suite. Battles are served blind (provider/model identities stripped), and the leaderboard returns the most recent computed snapshot for a given metric and domain.

  • Blind battle selection uses a placeholder ORDER BY random() (acknowledged in comments) with all model identity columns withheld from the response schema.
  • Leaderboard SQL uses a CTE to isolate the single most recent (computed_at, methodology_version) pair per metric/domain before joining back for the full row set, correctly preventing mixed-version boards.
  • Test coverage is comprehensive: blindness invariant, 404/422 edge cases, domain/metric scoping, staleness exclusion, and methodology-version tiebreaking are all verified with a real in-process Postgres instance.

Confidence Score: 5/5

Safe to merge — all three endpoints use parameterized queries, the blind invariant is enforced at the SQL column-selection layer, and the leaderboard CTE correctly isolates single-version boards.

The core logic is correct: battles are served blind, leaderboard filtering by metric/domain is applied both in the CTE and outer WHERE, and all DB access is parameterized. Two minor observations (lexicographic version tiebreaker and unconstrained metric string) do not affect current correctness.

runner/src/coval_bench/api/routers/arena.py — the methodology_version tiebreaker and metric param type are worth a second look before the leaderboard snapshot job ships.

Important Files Changed

Filename Overview
runner/src/coval_bench/api/routers/arena.py New router implementing three blind read endpoints; SQL and parameterization are correct, minor lexicographic tiebreaker and unconstrained metric-param concerns noted.
runner/src/coval_bench/api/schemas.py Adds BattleOut, LeaderboardEntryOut, and ArenaLeaderboardResponse schemas; intentional float wins/losses/ties for fractional scoring as clarified in prior review thread.
runner/tests/api/test_arena.py Comprehensive test coverage: blindness, 404/422 edge cases, domain/metric filtering, latest-board selection, and methodology-version tiebreaker are all exercised.
runner/src/coval_bench/api/app.py Minimal change: imports the arena router and mounts it at /v1; no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant ArenaRouter
    participant PgPool as Postgres Pool
    participant PostHog

    Client->>ArenaRouter: GET /v1/arena/battle
    ArenaRouter->>PgPool: SELECT id, prompt_text, domain, audio_a_url, audio_b_url FROM arena.battles ORDER BY random() LIMIT 1
    PgPool-->>ArenaRouter: row (or None)
    alt No rows
        ArenaRouter-->>Client: 404 no battles available
    else Row found
        ArenaRouter->>PostHog: capture arena_battle_served
        ArenaRouter-->>Client: 200 BattleOut (blind)
    end

    Client->>ArenaRouter: "GET /v1/arena/battle/{id}"
    ArenaRouter->>PgPool: "SELECT ... FROM arena.battles WHERE id = %(id)s"
    PgPool-->>ArenaRouter: row (or None)
    alt Not found
        ArenaRouter-->>Client: 404
    else Found
        ArenaRouter-->>Client: 200 BattleOut (blind)
    end

    Client->>ArenaRouter: "GET /v1/arena/leaderboard?metric=&domain="
    ArenaRouter->>PgPool: CTE picks latest (computed_at, methodology_version), JOIN snapshots WHERE metric/domain match, ORDER BY rating_elo DESC
    PgPool-->>ArenaRouter: board rows
    ArenaRouter->>PostHog: capture arena_leaderboard_queried
    ArenaRouter-->>Client: 200 ArenaLeaderboardResponse
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant ArenaRouter
    participant PgPool as Postgres Pool
    participant PostHog

    Client->>ArenaRouter: GET /v1/arena/battle
    ArenaRouter->>PgPool: SELECT id, prompt_text, domain, audio_a_url, audio_b_url FROM arena.battles ORDER BY random() LIMIT 1
    PgPool-->>ArenaRouter: row (or None)
    alt No rows
        ArenaRouter-->>Client: 404 no battles available
    else Row found
        ArenaRouter->>PostHog: capture arena_battle_served
        ArenaRouter-->>Client: 200 BattleOut (blind)
    end

    Client->>ArenaRouter: "GET /v1/arena/battle/{id}"
    ArenaRouter->>PgPool: "SELECT ... FROM arena.battles WHERE id = %(id)s"
    PgPool-->>ArenaRouter: row (or None)
    alt Not found
        ArenaRouter-->>Client: 404
    else Found
        ArenaRouter-->>Client: 200 BattleOut (blind)
    end

    Client->>ArenaRouter: "GET /v1/arena/leaderboard?metric=&domain="
    ArenaRouter->>PgPool: CTE picks latest (computed_at, methodology_version), JOIN snapshots WHERE metric/domain match, ORDER BY rating_elo DESC
    PgPool-->>ArenaRouter: board rows
    ArenaRouter->>PostHog: capture arena_leaderboard_queried
    ArenaRouter-->>Client: 200 ArenaLeaderboardResponse
Loading

Reviews (2): Last reviewed commit: "Select a single leaderboard board so met..." | Re-trigger Greptile

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
benchmarks Ready Ready Preview, Comment Jun 19, 2026 6:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds three read-only Voice Arena API endpoints (GET /arena/battle, GET /arena/battle/{id}, GET /arena/leaderboard) backed by PostgreSQL via an async connection pool. New Pydantic schemas define the response shapes. The arena router is wired into the FastAPI app under /v1. Integration tests cover battle retrieval, blindness enforcement, leaderboard snapshot selection, domain filtering, and methodology version separation.

Changes

Voice Arena Read Endpoints

Layer / File(s) Summary
Arena response schemas
runner/src/coval_bench/api/schemas.py
Adds BattleOut (blind battle with UUID and audio URLs), LeaderboardEntryOut (ELO/BT ratings, CI fields, vote/win/loss/tie counts), and ArenaLeaderboardResponse (keyed by metric/domain with optional computed_at and methodology_version).
Arena router endpoints and app wiring
runner/src/coval_bench/api/routers/arena.py, runner/src/coval_bench/api/app.py
Implements GET /arena/battle (random, blind), GET /arena/battle/{battle_id} (by UUID), and GET /arena/leaderboard (CTE-based latest snapshot query per metric/domain). Each route applies a 60/min rate limit and optional PostHog telemetry. Arena router is mounted under /v1 in create_app.
Integration tests and DB helpers
runner/tests/api/test_arena.py
Async helpers create the arena schema and seed arena.battles / arena.leaderboard_snapshots rows. Tests cover: 404 with empty DB, blind field enforcement, by-ID retrieval, 404/422 error cases, empty leaderboard state, latest-board ordering, domain filtering, per-metric scoping, and methodology version separation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • coval-ai/benchmarks#119: Creates the Alembic migration that defines the arena schema, arena.battles, and arena.leaderboard_snapshots tables that this PR's endpoints and tests read from.

Suggested reviewers

  • callumreid
  • coval-cale

Poem

🐇 Hop hop, the arena's alive,
Three endpoints now ready to thrive!
A battle served blind,
A leaderboard refined,
With CTE magic, the rankings arrive! 🏆

🚥 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 'Add Voice Arena read endpoints' accurately summarizes the main change: adding three read-only API endpoints for the Voice Arena feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 arena/04-read-api

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 and usage tips.

Comment thread runner/src/coval_bench/api/routers/arena.py
Comment thread runner/src/coval_bench/api/schemas.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
runner/tests/api/test_arena.py (1)

153-156: ⚡ Quick win

Strengthen blind-response assertions to cover B-side identity fields.

This test currently guards only provider_a/model_a. Add checks for provider_b and model_b so an accidental leak on side B is caught.

Suggested test delta
     assert "provider_a" not in data
     assert "model_a" not in data
+    assert "provider_b" not in data
+    assert "model_b" not in data
     assert data["domain"] == "support"
🤖 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 `@runner/tests/api/test_arena.py` around lines 153 - 156, The blind-response
test assertions currently only verify that provider_a and model_a fields are not
present in the response data, but they should also check for B-side identity
fields to ensure no accidental leaks on that side. Add two additional assertions
after the existing provider_a and model_a checks to verify that "provider_b" and
"model_b" are also not present in the data dictionary, using the same assertion
pattern as the existing checks.
🤖 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.

Nitpick comments:
In `@runner/tests/api/test_arena.py`:
- Around line 153-156: The blind-response test assertions currently only verify
that provider_a and model_a fields are not present in the response data, but
they should also check for B-side identity fields to ensure no accidental leaks
on that side. Add two additional assertions after the existing provider_a and
model_a checks to verify that "provider_b" and "model_b" are also not present in
the data dictionary, using the same assertion pattern as the existing checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a13a2358-907f-4b14-b3bd-1b257561b478

📥 Commits

Reviewing files that changed from the base of the PR and between 60a8f9b and 13454e3.

📒 Files selected for processing (4)
  • runner/src/coval_bench/api/app.py
  • runner/src/coval_bench/api/routers/arena.py
  • runner/src/coval_bench/api/schemas.py
  • runner/tests/api/test_arena.py

@seribaymadina
seribaymadina requested a review from coval-cale June 19, 2026 18:19

@coval-cale coval-cale 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.

YEWWW!

Comment thread runner/src/coval_bench/api/routers/arena.py
@seribaymadina
seribaymadina added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit cf2b746 Jun 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants