Skip to content

fix(router): address PR#3 round-2 review — robustness, a11y, and code style#4

Merged
sheepdestroyer merged 22 commits into
masterfrom
finalize-pr3-fixes-review
Jun 18, 2026
Merged

fix(router): address PR#3 round-2 review — robustness, a11y, and code style#4
sheepdestroyer merged 22 commits into
masterfrom
finalize-pr3-fixes-review

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Addresses all actionable code review feedback from CodeRabbit, Sourcery, and Gemini Code Assist on PR#3 (commit e37b24b219dd8d).


Changes

🛡️ Router robustness (router/main.py)

  • Lifespan try/finally: stats and timeline are now flushed on shutdown even when an exception occurs during the app lifetime (previously only ran on clean exit)
  • Background task GC prevention: fire-and-forget create_task(save_persisted_stats()) now stores its reference in a module-level _background_tasks set with a self-removing done callback, preventing premature garbage collection (Ruff RUF006)

♿ Visualizer accessibility & correctness (router/static/visualizer.html)

  • Keyboard accessibility: prompt list items now have tabindex="0", role="button", and onkeydown (Enter/Space) handlers — keyboard-only users can now navigate and select prompts
  • Stable annotation keys: annotation storage switches from fragile array-index keys to a stable djb2 prompt hash key, preventing silent mislabeling if the dataset order changes between versions. Full backward-compatible fallback to legacy index keys for existing saved annotations

🐛 Dataset pipeline script fixes

File Fix
scripts/benchmark_classifier.py Multi-schema tier lookup (tier/llm_tier/clf_tier); skip ERROR/unknown labels gracefully; safe choices[0] guard
scripts/extract_complex.py Forward iteration → selects first user message; adds [System:/[Note: pseudo-note filtering
scripts/extract_gapfill.py Same as extract_complex.py
scripts/retry_errors.py Safe status or {} fallback (AttributeError prevention); schema-aware ERROR detection (tier + clf_tier); fixed counter only incremented on genuine success
scripts/reclassify_all.py Safe choices[0] guard to prevent IndexError on empty model responses

🧹 Code style (PEP 8 / Ruff E712)

  • test_circuit_breaker.py: 14 == True/== False comparisons replaced with idiomatic assert/assert not
  • verify_breaker.py: 5 instances replaced

Skipped (with rationale)

Comment Reason
Pin Actions to commit SHAs User preference: stay up-to-date
Auth on /data mount & save_annotations Endpoint only exposed on 127.0.0.1 behind a home network. No existing routes use authentication. Adding auth would require a new mechanism across all routes — out of scope for this review fix. The validation already added (payload limits, key format, tier whitelist, note length) provides sufficient protection.
pod.yaml hardcoded path Local dev only; noted as non-blocker
pod.yaml security context / hostNetwork Pre-existing operational concern; required for pod networking; not introduced by this PR
Dead streaming code in agy_proxy.py Not dead — stream=is_stream_requested is passed from main.py line 1246; Gemini's review was a false positive

Verification

✓ ALL CIRCUIT BREAKER TESTS PASSED
✓ verify_breaker: All assertions passed
✓ test_a2_verify: Integration verified

Summary by Sourcery

Improve router robustness, dataset tooling, and visualizer UX for the LLM triage system.

New Features:

  • Expose a dataset visualizer web UI with static assets and a new /visualizer route, plus endpoints for serving and saving human annotations.
  • Add scripts for extracting prompts, gap-filling datasets, directly classifying prompts, reclassifying the full dataset, benchmarking the classifier, and retrying failed classifications.

Bug Fixes:

  • Ensure router stats and timelines are persisted safely with throttled, atomic async writes and proper background task lifecycle management.
  • Fix agy proxy routing and streaming to correctly handle Google/vendor breaker selection and support native streaming responses with telemetry.
  • Correct dataset extraction to use the first real user message while filtering out pseudo-system notes and trivial prompts, and guard against missing or malformed classifier responses.
  • Update circuit breaker tests and verification scripts to match the dual-breaker design while maintaining backward compatibility semantics.

Enhancements:

  • Refine circuit breaker implementation and docstrings, including clearer dual-breaker status reporting and tier handling.
  • Improve router dashboard with an entry point to the dataset visualizer and additional inline documentation for helpers.
  • Tighten annotations API validation with tier whitelisting, payload size limits, and note-length constraints, while persisting annotations atomically.

Build:

  • Adjust router Containerfile and pod manifests to include static/data mounts and assets, and tune Dependabot config for the Docker context.

CI:

  • Introduce a GitHub Actions workflow to run circuit breaker and integration verification tests on pushes and pull requests.

Documentation:

  • Add an internal engineering note on avoiding synchronous disk I/O in async handlers.

Tests:

  • Extend circuit breaker and integration tests to cover dual-breaker behavior, backward compatibility, and updated paths.

Summary by CodeRabbit

  • New Features

    • Added dataset visualizer with annotation dashboard for human review of classified prompts.
    • Added streaming response support with proper SSE formatting.
  • Bug Fixes

    • Fixed synchronous disk I/O blocking in request handlers.
    • Improved concurrent safety for data persistence and telemetry writes.
  • Refactoring

    • Enhanced circuit breaker logic for improved failover handling.
    • Refactored data persistence to use atomic writes and background task management.
  • Tests

    • Added automated test workflow for circuit breaker and proxy verification.
    • Updated test suite for dual-breaker architecture.

google-labs-jules Bot and others added 21 commits June 18, 2026 10:27
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com>
- Move _last_stats_save inside try block (only advance on successful write)
- Add save_persisted_stats(force=True) + timeline flush in lifespan shutdown
- Remove redundant import time inside save_persisted_stats
- Fix misleading comment about independent throttle timers
- Use time.monotonic() instead of time.time() for throttle timestamps
- Add logger.warning to bare except in timeline write
- Add logging to bare except Exception: pass in shutdown timeline flush
- Move asyncio.create_task(push_aggregate_scores()) before yield so it runs during app lifetime
- Use atomic file writes (temp file + os.replace) in save_persisted_stats
- Use atomic file writes in shutdown timeline flush and record_tool_usage timeline write
- Restructure lifespan to single yield point (remove else: yield; return pattern)
- Capture time.monotonic() once in record_tool_usage and reuse for guard + assignment
- Extract shared _atomic_write_json_sync / _atomic_write_json_async helpers
- Make save_persisted_stats async, using _atomic_write_json_async with
  run_in_executor + deep-copy to prevent concurrent modification
- Update all 4 async call sites (lifespan, classify_request x2, chat_completions)
  to await save_persisted_stats()
- In record_tool_usage (sync), fire-and-forget both stats and timeline writes
  via loop.run_in_executor with deep-copied data; fall back to sync write
  when no event loop is running (early startup)
- Replace duplicated atomic write logic in lifespan shutdown with shared helper
- router/main.py: wrap lifespan yield in try/finally so stats flush always
  runs; add _background_tasks set to prevent fire-and-forget task GC (RUF006)
- router/static/visualizer.html: add keyboard accessibility (tabindex, role,
  onkeydown) to prompt list items; switch annotation keys from array index to
  stable djb2 prompt hash with backward-compatible index fallback
- scripts/benchmark_classifier.py: safe tier field lookup (tier/llm_tier/clf_tier),
  guard per_tier keying to skip ERROR/unknown labels, safe choices[0] access
- scripts/extract_complex.py: use forward iteration for first user message,
  add system-note filtering ([System: / [Note:])
- scripts/extract_gapfill.py: same as extract_complex.py
- scripts/retry_errors.py: safe status.get() to prevent AttributeError,
  schema-aware ERROR detection (tier + clf_tier), only increment fixed on success
- scripts/reclassify_all.py: safe choices[0] fallback to prevent IndexError
- test_circuit_breaker.py: replace == True/False with idiomatic assert (14 sites)
- verify_breaker.py: replace == True/False with idiomatic assert (5 sites)
@sourcery-ai

sourcery-ai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Improves router robustness and observability, adds an accessible dataset visualizer with secure annotation persistence, refines circuit breaker behavior and tests (including dual Google/vendor breakers and streaming handling), and introduces a small suite of dataset pipeline scripts and CI/test harness updates, plus style fixes.

Sequence diagram for async stats persistence and background task handling

sequenceDiagram
    participant ChatHandler as chat_completions
    participant ToolUsage as record_tool_usage
    participant EventLoop as asyncio_event_loop
    participant StatsSaver as save_persisted_stats
    participant Disk as _atomic_write_json_sync

    ChatHandler->>StatsSaver: await save_persisted_stats(force=False)
    activate StatsSaver
    StatsSaver->>StatsSaver: check _last_stats_save and throttle
    alt [throttled]
        StatsSaver-->>ChatHandler: return
    else [not throttled]
        StatsSaver->>EventLoop: run_in_executor(_atomic_write_json_sync, stats_copy)
        EventLoop->>Disk: _atomic_write_json_sync(STATS_JSON_PATH, stats_copy)
        Disk-->>EventLoop: write complete
        EventLoop-->>StatsSaver: executor future resolved
        StatsSaver-->>ChatHandler: return
    end
    deactivate StatsSaver

    ChatHandler->>ToolUsage: record_tool_usage(...)
    activate ToolUsage
    ToolUsage->>ToolUsage: update stats and stats[timeline]
    ToolUsage->>EventLoop: create_task(save_persisted_stats())
    activate EventLoop
    EventLoop->>StatsSaver: save_persisted_stats(force=False)
    EventLoop-->>ToolUsage: task handle
    ToolUsage->>ToolUsage: _background_tasks.add(task)
    ToolUsage->>ToolUsage: task.add_done_callback(_background_tasks.discard)
    deactivate EventLoop

    rect rgb(230,230,230)
    ToolUsage->>EventLoop: run_in_executor(_atomic_write_json_sync, router_timeline)
    EventLoop->>Disk: _atomic_write_json_sync(router_timeline_path, timeline_copy)
    Disk-->>EventLoop: write complete
    end
    ToolUsage-->>ChatHandler: return
Loading

File-Level Changes

Change Details Files
Make router stats/timeline persistence non-blocking, robust, and correctly tied to app lifespan, with safe background task handling.
  • Introduced atomic JSON write helpers using temp files and os.replace, with async wrappers offloading work to a thread pool and deep-copying data to avoid concurrent mutation.
  • Reworked save_persisted_stats into an async, throttled function (2s window) with error handling and last-save tracking, and updated call sites (classify_request, record_tool_usage, chat_completions, shutdown) to await it appropriately.
  • Adjusted FastAPI lifespan to start push_aggregate_scores before serving, cancel and await it on shutdown, and always flush stats and timeline using the new async writers in a try/finally.
  • Added a module-level _background_tasks set to hold references to fire-and-forget tasks, with done callbacks to remove them, and added throttled async persistence of router_timeline.json from record_tool_usage with sync fallbacks when no event loop is running.
router/main.py
Add dataset visualizer endpoint and static files, including keyboard-accessible UI and robust annotation handling.
  • Mounted /static and /data using StaticFiles with STATIC_DIR/DATA_DIR, ensured DATA_DIR exists, and added a new /visualizer route serving static visualizer.html.
  • Implemented visualizer.html as a client-side app that loads classified_dataset.json/benchmark_results.json, surfaces LLM vs classifier tiers, supports various filters, and uses tabindex/role/button + key handlers for keyboard accessibility.
  • Switched annotation keys from numeric indices to a stable djb2 hash of the prompt (with legacy index-key fallback), keeping annotations in localStorage and syncing via POST to /dashboard/save-annotations.
  • Added a /dashboard/save-annotations POST endpoint with a Pydantic AnnotationItem model, tier whitelist, key/length validation, and async atomic write of annotations.json in DATA_DIR.
router/main.py
router/static/visualizer.html
router/Containerfile
pod.yaml
Refine agy proxy integration, streaming handling, and dual circuit breaker behavior with expanded test coverage.
  • In chat_completions, passed the incoming stream flag into try_agy_proxy, handled agy responses that expose a streaming generator or only buffered content, and unified telemetry (record_tool_usage, Langfuse spans) for both paths.
  • In agy_proxy, switched breaker selection to detect Google tiers via model_name containing 'gemini', and documented the token_generator stream semantics.
  • Simplified PerModelBreaker.record_failure to always advance the tier when not in tier 0, documented breaker methods/properties, and kept DualCircuitBreaker backward-compatible is_allowed/tier semantics.
  • Updated test_circuit_breaker, verify_breaker, and test_a2_verify to exercise the dual google/vendor breakers, add a reset helper, validate master compatibility, and avoid direct sys.path hardcoding, plus added docstrings and idiomatic asserts.
router/main.py
router/agy_proxy.py
router/circuit_breaker.py
test_circuit_breaker.py
verify_breaker.py
test_a2_verify.py
Extend dataset extraction and classification tooling, fix existing pipeline scripts, and add a benchmark workflow.
  • Reworked extract_prompts.py to use the first real user message (skipping Hermes pseudo-system notes), improved trivial-prompt detection with regex word boundaries, increased pagination depth, and wrote richer stats to raw_prompts_v2.json.
  • Added new scripts for gap-fill and complex-prompt extraction (extract_gapfill.py, extract_complex.py) that reuse first-user-message semantics, de-duplicate against existing classified_dataset.json, and bias toward longer/complex prompts.
  • Introduced classify_direct.py, reclassify_all.py, benchmark_classifier.py, and retry_errors.py to run the gemma4-26a4b-routing classifier directly or via llama-server, handle schema differences (tier/llm_tier/clf_tier), truncate prompts, retry ERROR cases safely, and write combined/benchmark JSON artifacts atomically.
  • Strengthened retry_errors.py and benchmark_classifier.py against missing choices and schema variations, and ensured classified_dataset.json is kept consistent (counts, gaps, llm vs classifier tiers).
scripts/extract_prompts.py
scripts/extract_complex.py
scripts/extract_gapfill.py
scripts/classify_direct.py
scripts/reclassify_all.py
scripts/benchmark_classifier.py
scripts/retry_errors.py
Tighten CI configuration, container image contents, pod mounts, and style cleanups.
  • Adjusted dependabot.yml to monitor Dockerfiles under /router, simplified commit message config, and removed unused ignore block.
  • Added a minimal GitHub Actions workflow to run circuit breaker and integration verification scripts on push/PR to master.
  • Updated router Containerfile to copy static assets into the image and removed copying of a dead config file that is volume-mounted at runtime.
  • Extended pod.yaml with a dataset-data hostPath volume mounted at /app/data for serving/annotating datasets, and cleaned up various tests to remove == True/False comparisons in favor of assert/assert not idioms.
  • Added a small engineering note in .jules/bolt.md documenting the save_persisted_stats async I/O issue and its resolution.
.github/dependabot.yml
.github/workflows/test.yml
router/Containerfile
pod.yaml
test_circuit_breaker.py
verify_breaker.py
.jules/bolt.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@sheepdestroyer, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 37 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5187430-a8df-41ef-90a2-5ae2bc86838c

📥 Commits

Reviewing files that changed from the base of the PR and between aa9fe10 and d20538c.

📒 Files selected for processing (8)
  • .github/workflows/test.yml
  • router/main.py
  • scripts/benchmark_classifier.py
  • scripts/extract_complex.py
  • scripts/extract_gapfill.py
  • scripts/retry_errors.py
  • test_circuit_breaker.py
  • verify_breaker.py
📝 Walkthrough

Walkthrough

The PR refactors DualCircuitBreaker tier advancement to be unconditional for non-zero tiers and updates all tests to target google/vendor sub-breakers. router/main.py replaces synchronous JSON writes with async atomic writers, converts save_persisted_stats to async, adds native OpenAI-compatible SSE streaming for AGY proxy responses, and wires in a new dataset annotation visualizer backed by a new FastAPI endpoint. Supporting dataset pipeline scripts for extraction, classification, reclassification, error retry, and benchmarking are also added.

Changes

DualCircuitBreaker Logic and Test Suite

Layer / File(s) Summary
PerModelBreaker tier advancement and DualCircuitBreaker docs
router/circuit_breaker.py, router/agy_proxy.py
Removes the probe_granted guard from record_failure so tier advancement is unconditional for non-zero tiers. Adds docstrings to DualCircuitBreaker. Fixes agy_proxy to detect Google tier by model_name containing "gemini" instead of tier index.
Dual-breaker test suite and CI workflow
test_circuit_breaker.py, verify_breaker.py, test_a2_verify.py, .github/workflows/test.yml, .github/dependabot.yml
Rewrites all breaker tests to operate on b.google/b.vendor sub-breakers, adds reset_breakers() and test_backward_compatibility(), converts sys.path to pathlib-relative, and adds the "Run Tests" GitHub Actions workflow plus dependabot updates for /router Docker and github-actions.

Router Async Persistence, SSE Streaming, and Visualizer

Layer / File(s) Summary
Atomic write infrastructure and async save_persisted_stats
router/main.py, .jules/bolt.md
Adds _atomic_write_json_sync/_atomic_write_json_async, introduces _background_tasks set, and converts save_persisted_stats to async with throttling and deep-copy for safe concurrent serialization.
Lifespan, record_tool_usage, and routing call sites
router/main.py
Updates lifespan() to manage push_aggregate_scores() lifecycle and flush on shutdown. Converts record_tool_usage timeline writes to throttled thread executor with exception callback. Awaits save_persisted_stats() at triage and routing call sites.
AGY proxy native SSE streaming generator
router/main.py
Replaces buffered AGY proxy response handling with a native async SSE generator emitting OpenAI-compatible data: chunks and [DONE], plus a simulated streaming fallback for buffered responses when stream=true.
Visualizer endpoint, AnnotationItem model, and static mounts
router/main.py
Mounts /static and /data, serves get_visualizer(), and adds save_annotations() with AnnotationItem validation (size, tier, note length) and atomic persistence to annotations.json. Updates dashboard header with /visualizer link.
visualizer.html dataset annotation UI
router/static/visualizer.html
Adds complete standalone annotation UI: loads dataset/benchmark/annotations JSON, merges server and localStorage state, supports conflict/agree/unreviewed filtering, renders per-prompt LLM vs classifier cards with tier selection, and syncs annotations to /dashboard/save-annotations.
Container build and pod data volume wiring
router/Containerfile, pod.yaml
Adds static/ copy to /app/static/ in the Containerfile and removes best_free_model.json. Adds dataset-data hostPath volume and /app/data mount to pod.yaml.

Dataset Pipeline Scripts

Layer / File(s) Summary
Prompt extraction scripts
scripts/extract_prompts.py, scripts/extract_complex.py, scripts/extract_gapfill.py
Updates extract_prompts.py to use first-user-message extraction, regex word-boundary trivial filtering, 100-page cap, and raw_prompts_v2.json output. Adds extract_complex.py for complexity-filtered extraction and extract_gapfill.py for gap-fill targeted extraction, both deduplicating against classified_dataset.json.
Classification and reclassification scripts
scripts/classify_direct.py, scripts/reclassify_all.py, scripts/retry_errors.py
Adds classify_direct.py to classify Hermes prompts and produce classified_dataset.json. Adds reclassify_all.py for grammar-constrained clf_tier generation with agreement reporting. Adds retry_errors.py to reclassify ERROR entries via a port-discovered model endpoint.
Classifier benchmark script
scripts/benchmark_classifier.py
Adds benchmarking script that runs the local model against classified_dataset.json, computes per-tier accuracy and a confusion matrix, and writes benchmark_results.json consumed by the visualizer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hops through the code with atomic care,
No blocking writes left in the async air!
Sub-breakers now each guard their own lane,
SSE chunks stream forth like a hop in the rain.
Annotations saved, the visualizer gleams —
A rabbit's tidy PR, or so it seems! 🌟

🚥 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 changes: addressing PR#3 review feedback across router robustness, accessibility, and code style improvements, which aligns with the substantial changes across main.py, visualizer.html, and multiple test/script files.
Docstring Coverage ✅ Passed Docstring coverage is 98.15% 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch finalize-pr3-fixes-review

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a dual circuit breaker system separating Google and Vendor model families, offloads blocking telemetry disk writes to a thread pool executor, and implements a dataset visualizer along with several benchmarking and classification scripts. Review feedback highlights a critical validation bug in the annotation saving endpoint that rejects stable hash keys, a potential TypeError crash in the prompt character length calculation when message content is null, and a potential crash in the direct classification script if the model returns an empty response.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread router/main.py
Comment on lines +2660 to +2665
for k, item in payload.items():
if not k.isdigit():
raise HTTPException(
status_code=400,
detail=f"Invalid payload key '{k}': keys must be numeric strings (dataset indexes)."
)

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.

high

The validation if not k.isdigit(): blocks the newly introduced stable annotation keys (which are generated as djb2 hashes starting with 'h', e.g., 'h5e3f'). This causes any request to save new annotations to fail with a 400 Bad Request error. The validation should be updated to accept both legacy numeric keys and the new stable hash keys.

Suggested change
for k, item in payload.items():
if not k.isdigit():
raise HTTPException(
status_code=400,
detail=f"Invalid payload key '{k}': keys must be numeric strings (dataset indexes)."
)
for k, item in payload.items():
is_stable_key = k.startswith('h') and all(c in '0123456789abcdefABCDEF' for c in k[1:])
if not (k.isdigit() or is_stable_key):
raise HTTPException(
status_code=400,
detail=f"Invalid payload key '{k}': keys must be numeric strings or stable hash keys starting with 'h'."
)

Comment thread router/main.py
# Success telemetry
latency_ms = (time.time() - start_time) * 1000.0
# Approximate prompt tokens based on messages characters
prompt_chars = sum(len(m.get("content", "")) for m in messages)

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.

high

If any message in the history has a content of None (which is common for assistant messages that only contain tool_calls), m.get("content", "") will return None because the key exists but its value is null. Calling len(None) will then raise a TypeError and crash the completions endpoint. Filtering the messages to only sum the length of string contents avoids this issue.

Suggested change
prompt_chars = sum(len(m.get("content", "")) for m in messages)
prompt_chars = sum(len(m.get("content")) for m in messages if isinstance(m.get("content"), str))

)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
return data["choices"][0]["message"].get("content", "").strip()

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.

medium

If the model returns an empty response or an error, data["choices"] might be empty or missing, leading to an unhandled IndexError or KeyError which crashes the script. Adding a guard clause to check for empty choices makes the classification more robust.

Suggested change
return data["choices"][0]["message"].get("content", "").strip()
choices = data.get("choices", [])
if not choices:
return "ERROR"
return choices[0].get("message", {}).get("content", "").strip()

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues, and left some high level feedback:

  • The visualizer frontend now uses a djb2 hash (promptKey) for annotation keys, but the /dashboard/save-annotations endpoint currently validates keys with k.isdigit(), so the server will reject the new hashed keys; consider relaxing the key validation or aligning the frontend to send numeric keys only so annotations can actually be persisted.
  • There are now multiple very similar implementations of "extract first real user prompt" and trivial-prompt filtering across extract_prompts.py, extract_gapfill.py, and extract_complex.py; consolidating these into shared helpers would reduce duplication and make future fixes to the extraction logic easier to apply consistently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The visualizer frontend now uses a djb2 hash (`promptKey`) for annotation keys, but the `/dashboard/save-annotations` endpoint currently validates keys with `k.isdigit()`, so the server will reject the new hashed keys; consider relaxing the key validation or aligning the frontend to send numeric keys only so annotations can actually be persisted.
- There are now multiple very similar implementations of "extract first real user prompt" and trivial-prompt filtering across `extract_prompts.py`, `extract_gapfill.py`, and `extract_complex.py`; consolidating these into shared helpers would reduce duplication and make future fixes to the extraction logic easier to apply consistently.

## Individual Comments

### Comment 1
<location path="router/static/visualizer.html" line_range="350-354" />
<code_context>
+  render();
+}
+
+function saveAnnotations() {
+  // Save to localStorage
+  localStorage.setItem('classifier-annotations', JSON.stringify(annotations));
+  // Auto-sync by posting to dashboard save endpoint
+  fetch('/dashboard/save-annotations', {
+    method: 'POST',
+    headers: {'Content-Type': 'application/json'},
</code_context>
<issue_to_address>
**issue (bug_risk):** Visualizer sends annotation keys that violate backend `save_annotations` key validation.

The frontend posts the `annotations` map using `promptKey()`-generated IDs like `h<hex>`, but `/dashboard/save-annotations` currently enforces `key.isdigit()` and returns 400 for these keys (`Invalid payload key 'h...'`). This breaks autosync/export. We should either switch the client to numeric index keys (and keep the hash as metadata) or relax the backend validation to accept non-numeric keys and validate indices within the payload instead.
</issue_to_address>

### Comment 2
<location path="test_circuit_breaker.py" line_range="122-131" />
<code_context>
     print("✓ Success resets breaker to Tier 0 from any tier")


+def test_backward_compatibility():
+    """Master breaker record_failure and record_success affect both breakers."""
+    reset_breakers()
+    b = get_breaker()
+    
+    b.record_failure()
+    assert b.google.tier == 1
+    assert b.vendor.tier == 1
+    assert not b.is_allowed()  # both blocked
+
+    b.record_success()
+    assert b.google.tier == 0
+    assert b.vendor.tier == 0
+    assert b.is_allowed()
+    print("✓ Master record_failure and record_success maintain compatibility")
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test to cover `is_allowed()` semantics when only one sub-breaker is blocked or recovered.

The new test covers propagation of `record_failure`/`record_success` and the max-tier behavior, but it doesn’t fully exercise the `is_allowed()` OR logic when breakers are asymmetric. You partially hit `google`-blocked/vendor-open in `test_first_failure_trips_to_tier1`, but there’s no explicit check for the inverse (vendor blocked, google open). Please add a small test (or parametrized variant) that asserts `b.is_allowed()` in both asymmetric states to lock in the intended OR semantics for `DualCircuitBreaker.is_allowed` and protect against regressions.

Suggested implementation:

```python
    assert b.google.tier == 0
    assert b.google.is_allowed()
    print("✓ Success resets breaker to Tier 0 from any tier")


def test_asymmetric_is_allowed_semantics():
    """`DualCircuitBreaker.is_allowed` should be OR over sub-breakers."""
    reset_breakers()
    b = get_breaker()

    # Case 1: google blocked, vendor open -> overall allowed
    b.google.tier = 1
    b.google.cooldown_until = time.time() + 60
    b.google.probe_granted = False

    b.vendor.tier = 0
    b.vendor.cooldown_until = 0
    b.vendor.probe_granted = False

    assert not b.google.is_allowed()
    assert b.vendor.is_allowed()
    assert b.is_allowed()  # OR semantics: allowed if any sub-breaker allows

    # Case 2: vendor blocked, google open -> overall allowed
    b.vendor.tier = 1
    b.vendor.cooldown_until = time.time() + 60
    b.vendor.probe_granted = False

    b.google.tier = 0
    b.google.cooldown_until = 0
    b.google.probe_granted = False

    assert b.google.is_allowed()
    assert not b.vendor.is_allowed()
    assert b.is_allowed()  # OR semantics: allowed if any sub-breaker allows



Simulates consecutive quota failures and verifies:
  - Independent google and vendor breakers
  - Tier 1 cooldown (5 min) after 1st failure
  - Tier 2 cooldown (30 min) after 2nd failure  
  - Tier 3 cooldown (5 hours) after 3rd failure
  - Probe behavior: one allowed attempt after cooldown
  - Reset to Tier 0 on success
  - Stay at Tier 3 on repeated failure
  - Backward compatibility of master breaker methods

```

This test relies on `reset_breakers`, `get_breaker`, and the `google`/`vendor` breaker interface (`tier`, `cooldown_until`, `probe_granted`, `is_allowed`) already present in the test module. If these helpers or attributes differ slightly in your codebase, adjust the field names or reset logic accordingly so that the sub-breakers are definitively blocked / open in each case.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread router/static/visualizer.html
Comment thread test_circuit_breaker.py
Comment on lines +122 to +131
def test_backward_compatibility():
"""Master breaker record_failure and record_success affect both breakers."""
reset_breakers()
b = get_breaker()

b.record_failure()
assert b.google.tier == 1
assert b.vendor.tier == 1
assert not b.is_allowed() # both blocked

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.

suggestion (testing): Add a test to cover is_allowed() semantics when only one sub-breaker is blocked or recovered.

The new test covers propagation of record_failure/record_success and the max-tier behavior, but it doesn’t fully exercise the is_allowed() OR logic when breakers are asymmetric. You partially hit google-blocked/vendor-open in test_first_failure_trips_to_tier1, but there’s no explicit check for the inverse (vendor blocked, google open). Please add a small test (or parametrized variant) that asserts b.is_allowed() in both asymmetric states to lock in the intended OR semantics for DualCircuitBreaker.is_allowed and protect against regressions.

Suggested implementation:

    assert b.google.tier == 0
    assert b.google.is_allowed()
    print("✓ Success resets breaker to Tier 0 from any tier")


def test_asymmetric_is_allowed_semantics():
    """`DualCircuitBreaker.is_allowed` should be OR over sub-breakers."""
    reset_breakers()
    b = get_breaker()

    # Case 1: google blocked, vendor open -> overall allowed
    b.google.tier = 1
    b.google.cooldown_until = time.time() + 60
    b.google.probe_granted = False

    b.vendor.tier = 0
    b.vendor.cooldown_until = 0
    b.vendor.probe_granted = False

    assert not b.google.is_allowed()
    assert b.vendor.is_allowed()
    assert b.is_allowed()  # OR semantics: allowed if any sub-breaker allows

    # Case 2: vendor blocked, google open -> overall allowed
    b.vendor.tier = 1
    b.vendor.cooldown_until = time.time() + 60
    b.vendor.probe_granted = False

    b.google.tier = 0
    b.google.cooldown_until = 0
    b.google.probe_granted = False

    assert b.google.is_allowed()
    assert not b.vendor.is_allowed()
    assert b.is_allowed()  # OR semantics: allowed if any sub-breaker allows



Simulates consecutive quota failures and verifies:
  - Independent google and vendor breakers
  - Tier 1 cooldown (5 min) after 1st failure
  - Tier 2 cooldown (30 min) after 2nd failure  
  - Tier 3 cooldown (5 hours) after 3rd failure
  - Probe behavior: one allowed attempt after cooldown
  - Reset to Tier 0 on success
  - Stay at Tier 3 on repeated failure
  - Backward compatibility of master breaker methods

This test relies on reset_breakers, get_breaker, and the google/vendor breaker interface (tier, cooldown_until, probe_granted, is_allowed) already present in the test module. If these helpers or attributes differ slightly in your codebase, adjust the field names or reset logic accordingly so that the sub-breakers are definitively blocked / open in each case.

@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: 7

🧹 Nitpick comments (2)
router/main.py (1)

2651-2700: ⚡ Quick win

Full file overwrite may lose concurrent edits.

The endpoint writes the entire payload to annotations.json, replacing any existing content. If two browser tabs are editing simultaneously or the client sends a partial subset, data loss can occur.

If this is intentional (client-side merge before POST), document the contract. Otherwise, consider server-side merge:

💡 Optional: Server-side merge pattern
 try:
+    ann_path = DATA_DIR / "annotations.json"
+    existing = {}
+    if ann_path.exists():
+        try:
+            existing = json.loads(ann_path.read_text())
+        except Exception:
+            pass  # Start fresh if corrupted
     body = {k: (v.model_dump() if hasattr(v, "model_dump") else v.dict()) for k, v in payload.items()}
-    ann_path = DATA_DIR / "annotations.json"
+    existing.update(body)  # Merge incoming annotations
-    await _atomic_write_json_async(str(ann_path), body)
+    await _atomic_write_json_async(str(ann_path), existing)
🤖 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 `@router/main.py` around lines 2651 - 2700, The save_annotations endpoint
performs a full file overwrite when writing to annotations.json, which can cause
data loss during concurrent edits or when clients send partial payloads. Either
add clear documentation at the endpoint level explaining that the client must
perform any necessary merging before sending the request, or implement
server-side merge logic in the save_annotations function by first reading the
existing annotations.json file, merging the new payload with existing data
(preserving annotations not included in the current request), and then writing
the merged result back to disk using _atomic_write_json_async.
test_circuit_breaker.py (1)

19-21: ⚡ Quick win

Standardize imports to one module path for breaker singletons.

This file imports circuit_breaker via sys.path injection to /router, while verify_breaker.py imports router.circuit_breaker. In single-process runners, that can create two module instances and split breaker state.

Suggested fix
-sys.path.insert(0, str(Path(__file__).resolve().parent / 'router'))
-
-from circuit_breaker import get_breaker, TIER_COOLDOWNS, MAX_TIER
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+from router.circuit_breaker import get_breaker, TIER_COOLDOWNS, MAX_TIER
🤖 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 `@test_circuit_breaker.py` around lines 19 - 21, Remove the sys.path.insert
line that adds the router directory to the path, and change the import statement
from importing directly from circuit_breaker to importing from
router.circuit_breaker instead. This will ensure the test_circuit_breaker.py
file uses the same import path as verify_breaker.py, preventing duplicate module
instances and maintaining shared breaker singleton state across the test suite.
🤖 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 @.github/workflows/test.yml:
- Around line 13-19: Replace the mutable action version tags with full-length
commit SHAs in the workflow file to secure the supply chain. Specifically,
update actions/checkout@v4 to use its pinned SHA and actions/setup-python@v5 to
use its pinned SHA. Additionally, add persist-credentials: false to the checkout
step parameters to explicitly disable credential persistence and reduce
credential exposure risk.

In `@router/static/visualizer.html`:
- Around line 350-359: The saveAnnotations() function sends annotations
containing hash-based keys (e.g., h12345abc) generated by promptKey() directly
to the server endpoint /dashboard/save-annotations, but the server validation
expects only numeric string keys. Modify the saveAnnotations() function to
filter or transform the annotations object before sending it in the fetch
request body. Either exclude the hash-keyed entries when posting to the server
endpoint, or create a mapped version of the annotations with numeric keys only.
Keep the localStorage persistence as-is since it stores the full annotation
structure locally.

In `@scripts/benchmark_classifier.py`:
- Around line 73-93: The overall accuracy calculation at line 92 uses total as
the denominator, but this includes rows that were skipped in the conditional
check at lines 73-75 where unknown expected labels are not scored. To fix this,
calculate the overall accuracy by summing all the correct predictions and
dividing by the sum of all totals from the per_tier dictionary (which only
counts scored rows), rather than dividing by the total row count. This ensures
the accuracy percentage reflects only the rows that were actually evaluated.

In `@scripts/extract_complex.py`:
- Around line 25-28: The loop iterating through `existing_data.get('prompts',
[])` currently fails when a prompt record is missing the 'prompt' key, causing
the entire preload path to abort and disabling dedupe for the run. Guard against
malformed dataset rows by adding a check inside the for loop to verify that the
'prompt' key exists in each prompt record before accessing it with
`p['prompt'].strip().lower()`, and skip any records that don't have this key so
the deduplication set can still be built from valid prompts.

In `@scripts/extract_gapfill.py`:
- Around line 23-26: The code directly accesses p['prompt'] within the loop
iterating over existing_data.get('prompts', []) which can raise a KeyError if
the 'prompt' key is missing from any record, causing extraction to fail. Replace
the direct dictionary access p['prompt'] with the .get() method using an
appropriate default value, or add a try-except block to gracefully handle
malformed records by skipping them with continue statements when required keys
are absent.

In `@scripts/retry_errors.py`:
- Around line 72-104: The error detection logic that checks if p.get('tier') ==
'ERROR' or p.get('clf_tier') == 'ERROR' needs to be updated to also match error
strings that start with 'ERROR:' (using startswith instead of exact equality),
since reclassify_all.py emits errors in the format 'ERROR: ...'. Additionally,
when updating the classification result at the line that sets
prompts_list[idx]['tier'] = tier, you must also update the clf_tier field to
ensure new-schema rows don't retain stale error values. Finally, in the counts
recomputation section where new_counts is calculated, update the Counter logic
to use clf_tier as the primary field (checking clf_tier first, then tier, then
llm_tier) to properly reflect classifier retry results in the new schema.

In `@test_circuit_breaker.py`:
- Line 70: In the assertion statement at line 70, remove the unnecessary
equality comparison with the boolean literal True. Instead of comparing
b.google.probe_granted == True, directly assert b.google.probe_granted as a
boolean value. This aligns with PEP 8 style guidelines and resolves the E712
linting violation by using the more Pythonic approach of asserting the variable
itself rather than comparing it to a boolean literal.

---

Nitpick comments:
In `@router/main.py`:
- Around line 2651-2700: The save_annotations endpoint performs a full file
overwrite when writing to annotations.json, which can cause data loss during
concurrent edits or when clients send partial payloads. Either add clear
documentation at the endpoint level explaining that the client must perform any
necessary merging before sending the request, or implement server-side merge
logic in the save_annotations function by first reading the existing
annotations.json file, merging the new payload with existing data (preserving
annotations not included in the current request), and then writing the merged
result back to disk using _atomic_write_json_async.

In `@test_circuit_breaker.py`:
- Around line 19-21: Remove the sys.path.insert line that adds the router
directory to the path, and change the import statement from importing directly
from circuit_breaker to importing from router.circuit_breaker instead. This will
ensure the test_circuit_breaker.py file uses the same import path as
verify_breaker.py, preventing duplicate module instances and maintaining shared
breaker singleton state across the test suite.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57b1bf66-96e1-4740-935e-add1deabe678

📥 Commits

Reviewing files that changed from the base of the PR and between 859fb74 and aa9fe10.

📒 Files selected for processing (19)
  • .github/dependabot.yml
  • .github/workflows/test.yml
  • .jules/bolt.md
  • pod.yaml
  • router/Containerfile
  • router/agy_proxy.py
  • router/circuit_breaker.py
  • router/main.py
  • router/static/visualizer.html
  • scripts/benchmark_classifier.py
  • scripts/classify_direct.py
  • scripts/extract_complex.py
  • scripts/extract_gapfill.py
  • scripts/extract_prompts.py
  • scripts/reclassify_all.py
  • scripts/retry_errors.py
  • test_a2_verify.py
  • test_circuit_breaker.py
  • verify_breaker.py

Comment thread .github/workflows/test.yml
Comment thread router/static/visualizer.html
Comment thread scripts/benchmark_classifier.py
Comment thread scripts/extract_complex.py
Comment thread scripts/extract_gapfill.py Outdated
Comment thread scripts/retry_errors.py
Comment thread test_circuit_breaker.py Outdated
@sheepdestroyer

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a dataset visualizer tool with a frontend HTML interface, backend endpoints for serving static files and saving human annotations, and helper scripts for prompt extraction, classification, and benchmarking. It also refactors the circuit breaker into a dual-breaker system (separating Google and vendor models) and optimizes telemetry persistence by offloading and throttling disk writes asynchronously to avoid blocking the event loop. A critical issue was identified in the annotation saving endpoint, where the key validation rejects the newly introduced stable hash keys starting with 'h', which would break the save functionality.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread router/main.py
Comment on lines +2660 to +2665
for k, item in payload.items():
if not k.isdigit():
raise HTTPException(
status_code=400,
detail=f"Invalid payload key '{k}': keys must be numeric strings (dataset indexes)."
)

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.

high

The validation check not k.isdigit() will reject the new stable djb2 prompt hash keys (which start with 'h' followed by a hex string, e.g., "h5381"). This completely breaks the annotation saving feature when using the new stable keys.\n\nUpdate the validation to accept both legacy numeric index keys and the new stable hash keys.

    for k, item in payload.items():\n        is_valid_key = k.isdigit() or (k.startswith('h') and all(c in '0123456789abcdefABCDEF' for c in k[1:]))\n        if not is_valid_key:\n            raise HTTPException(\n                status_code=400,\n                detail=f"Invalid payload key '{k}': keys must be numeric strings or hex hashes starting with 'h'."\n            )

@sheepdestroyer
sheepdestroyer merged commit f227c5e into master Jun 18, 2026
1 check passed
Comment thread router/main.py Dismissed
Comment thread router/main.py Dismissed
Comment thread router/main.py Dismissed
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