Skip to content

Publish recorded benchmark results, and name the OCR model and tokens for every run - #31

Open
damodaha wants to merge 2 commits into
Jawafdehi:mainfrom
damodaha:feat/benchmark-recorded-results
Open

Publish recorded benchmark results, and name the OCR model and tokens for every run#31
damodaha wants to merge 2 commits into
Jawafdehi:mainfrom
damodaha:feat/benchmark-recorded-results

Conversation

@damodaha

@damodaha damodaha commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

User description

Why

CI cannot measure this benchmark. A runner has no vision backend, so a live build could only ever exercise the no-OCR configuration — 16 runs with 32 skipped — and converting the full corpus three ways takes far longer than a Pages build should. The published page therefore showed a degraded subset of what the benchmark can actually say.

This records a complete local run and replays it. CI now publishes 16 documents / 48 runs with no credentials at all, verified by running the workflow's exact command with the environment stripped.

Recorded results

--write-snapshot records each run's status, text, timing, memory and OCR usage into site/snapshot.json; --snapshot replays it, converting nothing and calling no backend. Availability comes from the recording rather than the environment — deriving it locally would drop the very OCR columns the snapshot exists to publish.

Checks and outcomes are recomputed from the recorded text, so a catalog edit takes effect without re-recording. That is how the npc-press-note expectation below applies to an already-committed snapshot.

The property this rests on is that a replay is indistinguishable from the run it recorded. Verified against the real build: identical summary, metrics, checks and artifact hashes, and byte-identical transcripts and diagnostics across all 48 runs. That is why the recording stores the raw stderr rather than the composed diagnostic file — composing it twice would append the error lines a second time. A test covers it, using a fixture that includes a failing run, because with nothing to append the composition is the identity function and the test would not bite.

Provenance is split deliberately: build is the commit that published the page, measured the commit whose behaviour the numbers describe. measured.stale flags a divergence, missing_runs names catalog runs the recording predates, and the dashboard renders both above the summary. Run metadata attributes the conversion to the recorded environment, not the publishing one. A live build sets measured to null.

summarize.py writes the numbers, per-configuration outcomes, models and token totals to $GITHUB_STEP_SUMMARY, so a regression shows on the Actions run rather than only in the deployed page.

OCR model and tokens

Two things made OCR reporting misleading:

  • _ocr_usage_record returned null when a run made no vision call — which is most runs, since Likhit only calls a model for pages a text layer cannot serve. That made "spent nothing" indistinguishable from "nobody was counting", and the dashboard drew a blank for both. Zero is now a measurement; null means only that the counter was unreachable.
  • The model id lived inside the per-run usage record, so it was absent for exactly those runs, and for any backend without a counter at all.

The model is now recorded per configuration and shown beside the configuration selector:

Vision model us.anthropic.claude-opus-5 · 2 calls · 11,413 tokens (9,616 in / 1,797 out)
Vision model qwen2.5vl:7b · no OCR call — Likhit read this document without the model
No vision model — this configuration reads the text layer only.

The committed recording covers all 16 runs of both OCR backends — 3 with real spend, 13 as measured zeros:

Configuration Model Vision calls Tokens
Likhit (no OCR)
Likhit (with OCR) us.anthropic.claude-opus-5 5 25,743
Likhit (offline OCR) qwen2.5vl:7b 6 20,526

site/ocr_usage_proxy.py is a reference implementation of the counter contract the README documented but nothing shipped: it forwards to any OpenAI-compatible endpoint and accumulates the usage block each response carries. One per backend, so a hosted run cannot be credited with a locally served model's spend.

Documentation

The OCR section of both the README and the landing page now shows the two setups people actually reach for — a vision model on your own machine, and Bedrock behind a translating proxy. Bedrock gets explicit treatment because Likhit speaks only the OpenAI chat API; there is no Bedrock support in the package, so pointing it straight at Bedrock cannot work and the generic "custom base URL" option would lead a reader astray.

Both also record when OCR runs: only for pages a text layer cannot serve, so a configured backend does not mean paying for one per document — 13 of the 16 benchmark documents convert with no vision call. A test pins the README and the page to each other and to _resolve_ocr_env, so a renamed variable fails rather than shipping.

Behaviour notes for the reviewer

  • npc-press-note under offline OCR is intermittently slow, not broken. It exhausted the 2400s budget on one recording and completed in 1073s on the next. Marked known_issue, so a timeout is reported instead of failing the build while a run that completes is promoted back to pass on its own — which is what the committed recording shows. Without this the run would block every Pages deploy.
  • A document whose every run is skipped is now dropped rather than published with an empty runs array, which the schema forbids. Latent before this change: any document declaring only OCR runs would have hit it.
  • ocr_usage semantics changed for consumers. null now means only "not counted". The schema already permitted both shapes, so schema_version stays 1. measured is added as optional rather than required, so artifacts published before it existed still validate.
  • A layout regression I introduced and fixed. The new backend line shifted .detail-content's grid rows so the tab bar and body shared a cell, leaving Metadata and Checks unclickable. Playwright surfaced it as an intercepted click; measuring the geometry confirmed it was real. The subtlety is that #document-source is display: none, so it takes no row. A test now parses the DOM and asserts one row per in-flow child, catching a row too few, a row too many, and a new child added without one.
  • The snapshot holds the extracted text of every run (~700 KB). That is the evidence behind the numbers, and it is the same text already published to Pages, so committing it exposes nothing new.
  • A replayed build still downloads and hash-checks the sources. Only the conversions are replayed, so the build still needs network access to the publisher URLs.

Verification

  • uv run ruff check . clean; ruff format --check . clean (63 files)
  • uv run pytest321 passed, 5 skipped, 4 xfailed
  • uv run ty check — 3 diagnostics, all pre-existing on main
  • Both commits pass independently; I checked out the first and ran the suite there
  • CI's exact command with a stripped environment: 16 documents / 48 runs / 0 failed / 0 skipped, gate exits 0
  • Headless Chromium: no console errors, all three configurations' model lines correct, zero-call and spend states distinct, every tab clickable, page scrolls
  • Every new test verified to bite by breaking what it covers

PR Type

Enhancement, Tests, Documentation


Description

  • Replay recorded benchmark snapshots

  • Publish OCR model, token metadata

  • Add CI benchmark summary

  • Document OCR setup recipes


Diagram Walkthrough

flowchart LR
  snapshot["Recorded snapshot"] -- "replayed by" --> generator["site/generate.py"]
  generator["site/generate.py"] -- "writes" --> results["Pages results"]
  results["Pages results"] -- "rendered by" --> dashboard["Dashboard banner"]
  results["Pages results"] -- "summarized by" --> summary["Actions summary"]
Loading

File Walkthrough

Relevant files
Tests
1 files
test_benchmark_site.py
Add snapshot replay and OCR tests                                               
+945/-6 
Enhancement
6 files
generate.py
Implement benchmark snapshot replay recording                       
+260/-36
ocr_usage_proxy.py
Add OpenAI token counting proxy                                                   
+201/-0 
summarize.py
Add Actions benchmark summary renderer                                     
+182/-0 
app.js
Render recorded provenance and OCR usage                                 
+162/-15
styles.css
Style OCR recipes and provenance UI                                           
+216/-1 
schema.json
Extend schema with measurement provenance                               
+54/-0   
Documentation
3 files
index.html
Add OCR recipes and provenance banner                                       
+129/-4 
README.md
Document snapshots and token accounting                                   
+93/-5   
README.md
Document OCR recipes and benchmark link                                   
+54/-0   
Configuration changes
1 files
benchmark-pages.yml
Replay snapshots and publish summary                                         
+12/-0   
Bug fix
1 files
catalog.json
Mark local OCR timeout known issue                                             
+2/-1     
Data
1 files
snapshot.json
Add committed benchmark result snapshot                                   
+750/-0 


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

is_auto_command: True
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
enable_ai_metadata: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_description]

publish_labels: False
add_original_user_description: True
generate_ai_title: False
use_bullet_points: True
extra_instructions: 
enable_pr_type: True
final_update_message: True
enable_help_text: False
enable_help_comment: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 6
inline_file_summary: False
use_description_markers: False
enable_large_pr_handling: True
include_generated_by_header: True
max_ai_calls: 4
async_ai_calls: True

Summary by CodeRabbit

  • New Features

    • Added recorded benchmark snapshots for reproducible replay and validation.
    • Added OCR setup guidance for local models, hosted APIs, and Bedrock gateways.
    • Added OCR model, token usage, and cost details to benchmark results.
    • Added provenance banners showing measured commit, timestamp, version, and stale status.
    • Added workflow summaries with benchmark outcomes, usage, and failures.
  • Bug Fixes

    • Zero-call OCR runs now display as measured zero usage.
    • Unavailable or missing benchmark runs are clearly reported.
  • Documentation

    • Expanded benchmark, OCR usage, snapshot, and replay documentation.

oopsy added 2 commits August 8, 2026 02:45
The OCR section listed the authentication options abstractly but not the two
setups people actually reach for. Add both, verified against the code that reads
them (`_resolve_ocr_env`): a vision model served on your own machine, and Bedrock
behind a translating proxy.

Bedrock gets explicit treatment because likhit speaks only the OpenAI chat API --
there is no Bedrock support in the package -- so pointing it straight at Bedrock
cannot work, and a reader following the generic "custom base URL" option would
find that out the hard way.

Also record when OCR runs at all. It fires only for pages a text layer cannot
serve, so a configured backend does not mean paying for one per document: 13 of
the benchmark's 16 documents convert without a single vision call. And note that
leaving OCR unconfigured degrades rather than fails, quoting the log line likhit
actually emits.
… tokens

CI cannot measure this benchmark. A runner has no vision backend, so a live build
could only ever exercise the no-OCR configuration -- 16 runs with 32 skipped --
and converting the full corpus three ways takes far longer than a Pages build.
Record a complete local run instead and replay it.

`--write-snapshot` records every run's status, text, timing, memory and OCR usage;
`--snapshot` replays them, converting nothing and calling no backend. Availability
comes from the recording rather than the environment, or CI would drop the very
OCR columns the snapshot exists to publish. Everything downstream is recomputed
from the recorded text, so a catalog edit takes effect without re-recording --
which is how the `npc-press-note` expectation below applies to an existing
snapshot.

The property this rests on is that a replay is indistinguishable from the run it
recorded, verified against the real build: identical summary, metrics, checks and
artifact hashes, and byte-identical transcripts and diagnostics across all 48
runs. That is why the recording keeps the raw stderr rather than the composed
diagnostic file -- composing it twice would append the error lines again.

Provenance is deliberately split: `build` is the commit that published the page,
`measured` the commit whose behaviour the numbers describe. `measured.stale` flags
a divergence and `missing_runs` names catalog runs the recording predates; the
dashboard renders both above the summary, and run metadata attributes the
conversion to the recorded environment rather than the publishing one. A live
build sets `measured` to null.

OCR reporting was misleading in two ways, both fixed here:

- `_ocr_usage_record` returned null for a run that made no vision call, which is
  most of them -- likhit only calls a model for pages a text layer cannot serve.
  That made "spent nothing" indistinguishable from "nobody was counting", and the
  dashboard drew a blank for both. Zero is now a measurement; null means only
  that the counter was unreachable.
- The model id lived inside the per-run usage record, so it was absent for
  exactly those runs, and for any backend without a counter. It is now recorded
  per configuration and shown beside the configuration selector.

`ocr_usage_proxy.py` is a reference implementation of the counter contract the
README documented but nothing shipped: it forwards to any OpenAI-compatible
endpoint and accumulates the usage block each response carries. One per backend,
so a hosted run cannot be credited with a locally served model's spend.

`summarize.py` writes the published numbers, per-configuration outcomes, models
and token totals to the Actions job summary, so a regression is visible on the run
itself rather than only in the deployed page.

Two behaviour notes:

- `npc-press-note` under offline OCR exhausted the 2400s budget on one recording
  and completed in 1073s on the next, so it is intermittently slow rather than
  broken. Marked `known_issue`: a timeout is reported instead of failing the
  build, and a run that completes is promoted back to pass on its own, which is
  what the committed recording shows.
- A document whose every run is skipped is now dropped rather than published with
  an empty `runs` array, which the schema forbids. Latent before this change.

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The benchmark site now supports versioned snapshot recording and replay, OCR usage tracking, model metadata, measured-result provenance, dashboard disclosure, and GitHub Actions summaries. Documentation and tests cover OCR setup, replay validation, unavailable backends, and reporting behavior.

Changes

Benchmark site

Layer / File(s) Summary
OCR measurement and model recording
site/ocr_usage_proxy.py, site/generate.py, site/schema.json, README.md, site/README.md, site/catalog.json, tests/test_benchmark_site.py
Adds a concurrent OCR usage proxy, zero-call measurements, per-configuration model metadata, OCR setup documentation, and related validation tests.
Snapshot recording and replay
site/generate.py, site/schema.json, site/README.md, tests/test_benchmark_site.py
Adds versioned snapshot validation, live recording, backend-free replay, missing-run reporting, measured provenance, CLI options, and replay coverage tests.
Result reporting and dashboard disclosure
.github/workflows/benchmark-pages.yml, site/summarize.py, site/static/*, README.md, tests/test_benchmark_site.py
Adds Actions job summaries, measured-result banners, OCR configuration guidance, model and usage details, backend metadata, responsive styling, and reporting tests.

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

Possibly related PRs

  • Jawafdehi/likhit#26: Shares the benchmark generator, workflow, schema, dashboard, and test components extended by this change.
  • Jawafdehi/likhit#30: Shares benchmark configuration handling and OCR usage and model accounting.

Suggested labels: Review effort 5/5

Suggested reviewers: jawafdehi-pr-agent

Poem

I hop through snapshots, neat and bright,
Counting OCR tokens by moonlit light.
Replay the runs with backend at rest,
Show measured banners and summaries best.
The benchmark burrow now blooms with care.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: publishing recorded benchmark results and reporting OCR models and token usage.
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 unit tests (beta)
  • Create PR with unit tests

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.

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Truncated Response

Proxy reads only MAX_BODY_BYTES from upstream response, then returns status as success with truncated body. If upstream response exceeds 64 MiB, client receives corrupt JSON instead of clear failure. Read one extra byte, reject oversized responses, or stream safely.

    with urllib.request.urlopen(request, timeout=1800) as response:
        payload = response.read(MAX_BODY_BYTES)
        status = response.status
        content_type = response.headers.get(
            "Content-Type", "application/json"
        )
except urllib.error.HTTPError as error:
    # Pass the upstream's own error through: likhit's client needs to
    # see the real status to fail the way it would without the proxy.
    payload = error.read()
    status, content_type = (
        error.code,
        error.headers.get("Content-Type", "application/json"),
    )
except (urllib.error.URLError, TimeoutError, OSError) as error:
    self._respond(
        502,
        json.dumps({"error": f"upstream unreachable: {error}"}).encode(),
        "application/json",
    )
    return

usage = _usage_of(payload) if status < 400 else None
if usage is not None:
    counter.record(usage)
self._respond(status, payload, content_type)

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
is_auto_command: True
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_reviewer]

require_ticket_analysis_review: False
require_score_review: False
require_tests_review: True
require_estimate_effort_to_review: True
require_can_be_split_review: False
require_security_review: True
require_estimate_contribution_time_cost: False
require_todo_scan: False
publish_output_no_suggestions: True
persistent_comment: True
extra_instructions: 
num_max_findings: 3
final_update_message: True
enable_review_labels_security: True
enable_review_labels_effort: True
require_all_thresholds_for_incremental_review: False
minimal_commits_for_incremental_review: 0
minimal_minutes_for_incremental_review: 0
enable_intro_text: True
enable_help_text: False

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Bound upstream error bodies

Upstream error bodies are read unbounded. A bad upstream can exhaust proxy memory.
Apply the same response limit here.

site/ocr_usage_proxy.py [147-154]

 except urllib.error.HTTPError as error:
     # Pass the upstream's own error through: likhit's client needs to
     # see the real status to fail the way it would without the proxy.
-    payload = error.read()
+    payload = error.read(MAX_BODY_BYTES + 1)
+    if len(payload) > MAX_BODY_BYTES:
+        payload = b'{"error":"upstream error response too large"}'
     status, content_type = (
         error.code,
         error.headers.get("Content-Type", "application/json"),
     )
Suggestion importance[1-10]: 8

__

Why: Valid security hardening. error.read() is unbounded, allowing memory exhaustion from malicious upstream errors.

Medium
Possible issue
Reject oversized upstream responses

Oversized upstream responses are silently truncated. Client receives corrupt JSON as
success. Read MAX_BODY_BYTES + 1, return 502/413 when exceeded.

site/ocr_usage_proxy.py [141-146]

 with urllib.request.urlopen(request, timeout=1800) as response:
-    payload = response.read(MAX_BODY_BYTES)
+    payload = response.read(MAX_BODY_BYTES + 1)
+    if len(payload) > MAX_BODY_BYTES:
+        self._respond(
+            502,
+            b'{"error":"upstream response too large"}',
+            "application/json",
+        )
+        return
     status = response.status
     content_type = response.headers.get(
         "Content-Type", "application/json"
     )
Suggestion importance[1-10]: 7

__

Why: Valid bug. response.read(MAX_BODY_BYTES) can truncate upstream JSON, then proxy returns corrupt success.

Medium
General
Validate recorded run entries

Malformed runs entries crash with AttributeError, despite snapshot loading promising
shape refusal. Validate each recorded run before replay. Raise ValueError with the
snapshot key.

site/generate.py [666-678]

-def _replayed_payload(recorded: dict[str, Any]) -> tuple[dict[str, Any], str]:
+def _replayed_payload(recorded: dict[str, Any], key: str = "<snapshot run>") -> tuple[dict[str, Any], str]:
     """Rebuild the (payload, diagnostics) pair a recorded run originally produced.
 
     The recorded diagnostics are the raw subprocess stderr, *before* the error and
     traceback lines are folded in -- so a replayed run composes its diagnostic
     artifact exactly once, byte-identically to the run that was recorded.
     """
 
+    if not isinstance(recorded, dict):
+        raise ValueError(f"{key} is not a recorded run object")
     payload = {
         field: recorded[field]
         for field in _RECORDED_PAYLOAD_FIELDS
         if recorded.get(field) is not None
     }
Suggestion importance[1-10]: 5

__

Why: Valid type-checking issue. Improved code only adds a default key; call site still would not pass actual snapshot key.

Low

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
is_auto_command: True
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_code_suggestions]

commitable_code_suggestions: False
dual_publishing_score_threshold: -1
focus_only_on_problems: True
extra_instructions: 
enable_help_text: False
enable_chat_text: False
persistent_comment: True
max_history_len: 4
publish_output_no_suggestions: True
suggestions_score_threshold: 0
new_score_mechanism: True
new_score_mechanism_th_high: 9
new_score_mechanism_th_medium: 7
auto_extended_mode: True
num_code_suggestions_per_chunk: 3
max_number_of_calls: 3
parallel_calls: True
final_clip_factor: 0.8
decouple_hunks: False
demand_code_suggestions_self_review: False
code_suggestions_self_review_text: **Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones.
approve_pr_on_self_review: False
fold_suggestions_on_self_review: True

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Agent Walkthrough 🤖

Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more.

Here is a list of tools you can use to interact with the PR Agent:

ToolDescriptionTrigger Interactively 💎

DESCRIBE

Generates PR description - title, type, summary, code walkthrough and labels
  • Run

REVIEW

Adjustable feedback about the PR, possible issues, security concerns, review effort and more
  • Run

IMPROVE

Code suggestions for improving the PR
  • Run

UPDATE CHANGELOG

Automatically updates the changelog
  • Run

HELP DOCS

Answers a question regarding this repository, or a given one, based on given documentation path
  • Run

ADD DOCS

Generates documentation to methods/functions/classes that changed in the PR
  • Run

ASK

Answering free-text questions about the PR

[*]

GENERATE CUSTOM LABELS

Generates custom labels for the PR, based on specific guidelines defined by the user

[*]

(1) Note that each tool can be triggered automatically when a new PR is opened, or called manually by commenting on a PR.

(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the /ask tool, you need to comment on a PR: /ask "<question content>". See the relevant documentation for each tool for more details.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Auto-approved PR

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

🧹 Nitpick comments (5)
README.md (1)

186-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the fenced block.

markdownlint reports MD040 for this fence. The block contains log output, so text is the correct tag.

♻️ Proposed fix
-```
+```text
 PDF converter: OCR appears necessary, but OCR is not configured.
 Set OPENAI_API_KEY or GEMINI_API_KEY, plus MARKITDOWN_OCR_MODEL, to enable markitdown-ocr.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @README.md around lines 186 - 189, Update the fenced block containing the PDF
converter OCR message to specify the text language tag, preserving its existing
log output content.


</details>

<!-- cr-comment:v1:3fac2a4eb1138458d2ff7e54 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>tests/test_benchmark_site.py (1)</summary><blockquote>

`1546-1547`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Pass an unreachable `run_case` to this validation test.**

`generate` is called without `run_case`, so it falls back to `_default_run_case`. Validation currently raises before any conversion, so the test passes. If that ordering regressed, this test would run real subprocess conversions of the synthetic corpus before failing, which turns a clear assertion failure into a slow and confusing one.

<details>
<summary>♻️ Proposed change</summary>

```diff
     with pytest.raises(ValueError, match=expected):
-        generator.generate(tmp_path / "site", include_public=False, snapshot=snapshot)
+        generator.generate(
+            tmp_path / "site",
+            include_public=False,
+            snapshot=snapshot,
+            run_case=_unreachable_run_case,
+        )
🤖 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 `@tests/test_benchmark_site.py` around lines 1546 - 1547, Update the validation
test around generator.generate to pass an unreachable run_case callback
explicitly, ensuring validation fails before any subprocess conversion is
attempted. Keep the existing ValueError assertion and synthetic corpus setup
unchanged.
site/generate.py (2)

644-663: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider validating the per-run record type too.

_load_snapshot checks that runs is a dict, but not that each value is a dict. A hand-edited snapshot with a string value reaches _replayed_payload, where recorded[field] raises TypeError and recorded.get raises AttributeError deep in the build. That is the same failure class the comment on Line 659 already guards against one level up.

♻️ Proposed check
     for field in ("build", "configurations", "runs"):
         # Typed, not merely present: a `runs` string would pass a presence check
         # and then fail deep in the build with an AttributeError.
         if not isinstance(snapshot.get(field), dict):
             raise ValueError(f"{path} is missing required field {field!r}")
+    for key, recorded in snapshot["runs"].items():
+        if not isinstance(recorded, dict):
+            raise ValueError(f"{path} records run {key!r} as {type(recorded).__name__}")
     return snapshot
🤖 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 `@site/generate.py` around lines 644 - 663, Update _load_snapshot to validate
that every value in the snapshot’s runs mapping is a dict before returning the
snapshot. Raise the same ValueError style used for malformed top-level fields,
so invalid per-run records are rejected before _replayed_payload processes them.

975-979: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enforce the snapshot/write-snapshot exclusion in generate, not only in the parser.

The comment on Line 1187 states that reading a recording while writing one produces a copy with a fresh timestamp and no new measurement. build_parser enforces that for the CLI. generate does not: a caller can pass both snapshot and write_snapshot, and the resulting file records the replaying build's commit and a new recorded_at over recorded numbers. Move the invariant into the function so it holds for every caller.

♻️ Proposed guard
     catalog = _load_catalog()
+    if snapshot is not None and write_snapshot is not None:
+        raise ValueError(
+            "snapshot and write_snapshot are mutually exclusive: recording a "
+            "replay would produce a copy with a fresh timestamp and no new "
+            "measurement"
+        )
     snapshot_data = _load_snapshot(snapshot) if snapshot is not None else None
🤖 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 `@site/generate.py` around lines 975 - 979, Update generate to reject calls
where both snapshot and write_snapshot are provided, before loading the catalog
or snapshot. Preserve the existing CLI parser validation, but enforce the same
mutual-exclusion invariant directly in generate for all callers.
site/ocr_usage_proxy.py (1)

171-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate the --upstream scheme at startup.

--upstream is accepted as any string. If an operator omits the scheme, urllib.request.urlopen raises ValueError: unknown url type. That exception is not in the caught tuple in _proxy, so every request fails with a 500 and no clear cause. Reject the value at startup instead.

♻️ Proposed validation
     args = parser.parse_args(argv)
 
+    if not args.upstream.startswith(("http://", "https://")):
+        parser.error("--upstream must start with http:// or https://")
+
     counter = Counter()
🤖 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 `@site/ocr_usage_proxy.py` around lines 171 - 189, Validate args.upstream in
main before constructing ThreadingHTTPServer, requiring an explicit supported
URL scheme such as http or https and rejecting missing or unsupported schemes
through the argument parser with a clear error. Keep build_handler and request
forwarding unchanged for valid upstream values.
🤖 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 `@site/generate.py`:
- Around line 1008-1017: The replay configuration handling must explicitly
record catalog configurations absent from snapshot_data["configurations"]
instead of treating them as unavailable and silently dropping their runs. Update
the availability/models construction and related missing-run tracking around
recorded_configurations, preserving the existing visibility behavior used by the
Line 1051 path so unknown configurations are reported in measured.missing_runs
and remain available to dashboard reporting.

In `@site/ocr_usage_proxy.py`:
- Around line 121-126: Update the 413 response path in do_POST to close the
HTTP/1.1 connection by including the appropriate Connection: close response
header, preventing unread request-body bytes from being parsed as the next
request. Leave the existing handling of requests without Content-Length
unchanged.

In `@site/README.md`:
- Around line 100-107: Update the earlier Configurations paragraph to accurately
describe the deployed site's replay behavior, including that availability is
read from recorded results and all published columns can be shown;
alternatively, add a clear forward reference to the later replay section. Keep
the CI configuration details consistent with the explanation in the replay
section.

In `@site/summarize.py`:
- Around line 89-109: Update _configuration_rows() to track whether any
ocr_usage record was present for each configuration, separately from its call
count. Render the measured usage note whenever usage was recorded, including
zero calls and tokens; when no usage record exists, render an explicit
unavailable-usage state instead of omitting OCR information.

---

Nitpick comments:
In `@README.md`:
- Around line 186-189: Update the fenced block containing the PDF converter OCR
message to specify the text language tag, preserving its existing log output
content.

In `@site/generate.py`:
- Around line 644-663: Update _load_snapshot to validate that every value in the
snapshot’s runs mapping is a dict before returning the snapshot. Raise the same
ValueError style used for malformed top-level fields, so invalid per-run records
are rejected before _replayed_payload processes them.
- Around line 975-979: Update generate to reject calls where both snapshot and
write_snapshot are provided, before loading the catalog or snapshot. Preserve
the existing CLI parser validation, but enforce the same mutual-exclusion
invariant directly in generate for all callers.

In `@site/ocr_usage_proxy.py`:
- Around line 171-189: Validate args.upstream in main before constructing
ThreadingHTTPServer, requiring an explicit supported URL scheme such as http or
https and rejecting missing or unsupported schemes through the argument parser
with a clear error. Keep build_handler and request forwarding unchanged for
valid upstream values.

In `@tests/test_benchmark_site.py`:
- Around line 1546-1547: Update the validation test around generator.generate to
pass an unreachable run_case callback explicitly, ensuring validation fails
before any subprocess conversion is attempted. Keep the existing ValueError
assertion and synthetic corpus setup unchanged.
🪄 Autofix

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: 8f639538-fabb-41a1-b89d-038897c03891

📥 Commits

Reviewing files that changed from the base of the PR and between 4e22e09 and 967307c.

📒 Files selected for processing (13)
  • .github/workflows/benchmark-pages.yml
  • README.md
  • site/README.md
  • site/catalog.json
  • site/generate.py
  • site/ocr_usage_proxy.py
  • site/schema.json
  • site/snapshot.json
  • site/static/app.js
  • site/static/index.html
  • site/static/styles.css
  • site/summarize.py
  • tests/test_benchmark_site.py

Comment thread site/generate.py
Comment on lines +1008 to +1017
else:
recorded_configurations = snapshot_data["configurations"]
availability = {
name: bool(recorded_configurations.get(name, {}).get("available"))
for name in catalog["configurations"]
}
models = {
name: recorded_configurations.get(name, {}).get("model")
for name in catalog["configurations"]
}

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 | 🟡 Minor | ⚡ Quick win

A configuration missing from the snapshot is skipped silently.

On replay, recorded_configurations.get(name, {}) resolves an unrecorded configuration to available=False. Its runs are then dropped at Line 1043 before the snapshot lookup at Line 1046, so they never reach missing_runs. The result is a rise in summary.skipped with nothing named in measured.missing_runs and nothing for the dashboard to report.

This is the same situation the comment on Line 1051 treats as requiring visibility. test_committed_snapshot_covers_every_published_run_and_configuration catches it for the committed file, but the generator itself still degrades quietly for any other snapshot. Consider recording the unknown configuration explicitly.

🐛 Proposed fix
         else:
             recorded_configurations = snapshot_data["configurations"]
+            unrecorded = sorted(
+                set(catalog["configurations"]) - set(recorded_configurations)
+            )
+            if unrecorded:
+                raise ValueError(
+                    f"the snapshot does not cover configurations {unrecorded}; "
+                    f"re-record on a machine where their backends work"
+                )
             availability = {
📝 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
else:
recorded_configurations = snapshot_data["configurations"]
availability = {
name: bool(recorded_configurations.get(name, {}).get("available"))
for name in catalog["configurations"]
}
models = {
name: recorded_configurations.get(name, {}).get("model")
for name in catalog["configurations"]
}
else:
recorded_configurations = snapshot_data["configurations"]
unrecorded = sorted(
set(catalog["configurations"]) - set(recorded_configurations)
)
if unrecorded:
raise ValueError(
f"the snapshot does not cover configurations {unrecorded}; "
f"re-record on a machine where their backends work"
)
availability = {
name: bool(recorded_configurations.get(name, {}).get("available"))
for name in catalog["configurations"]
}
models = {
name: recorded_configurations.get(name, {}).get("model")
for name in catalog["configurations"]
}
🤖 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 `@site/generate.py` around lines 1008 - 1017, The replay configuration handling
must explicitly record catalog configurations absent from
snapshot_data["configurations"] instead of treating them as unavailable and
silently dropping their runs. Update the availability/models construction and
related missing-run tracking around recorded_configurations, preserving the
existing visibility behavior used by the Line 1051 path so unknown
configurations are reported in measured.missing_runs and remain available to
dashboard reporting.

Comment thread site/ocr_usage_proxy.py
Comment on lines +121 to +126
def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's spelling
length = int(self.headers.get("Content-Length") or 0)
if length > MAX_BODY_BYTES:
self._respond(413, b'{"error":"request too large"}', "application/json")
return
self._proxy(self.rfile.read(length) if length else b"")

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 | 🟡 Minor | ⚡ Quick win

Close the connection after a 413, or drain the body.

protocol_version is HTTP/1.1, so the connection stays open by default. On the 413 path the request body is never read from self.rfile. The unread bytes remain in the socket and the next request parse reads them as a request line. Send Connection: close on that response so the client cannot reuse a corrupted connection.

Note also that a request without Content-Length (chunked transfer encoding) forwards an empty body. That is acceptable for the OpenAI-compatible clients used here, but it is silent.

🔒️ Proposed fix for the 413 path
         def _respond(self, status: int, body: bytes, content_type: str) -> None:
             self.send_response(status)
             self.send_header("Content-Type", content_type)
             self.send_header("Content-Length", str(len(body)))
+            if status == 413:
+                # The request body was never read, so the connection cannot be
+                # reused: the leftover bytes would be parsed as the next request.
+                self.send_header("Connection", "close")
+                self.close_connection = True
             self.end_headers()
             self.wfile.write(body)
🤖 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 `@site/ocr_usage_proxy.py` around lines 121 - 126, Update the 413 response path
in do_POST to close the HTTP/1.1 connection by including the appropriate
Connection: close response header, preventing unread request-body bytes from
being parsed as the next request. Leave the existing handling of requests
without Content-Length unchanged.

Comment thread site/README.md
Comment on lines +100 to +107
A replay performs no conversion and calls no backend. It takes each run's status,
text, timing, memory and OCR usage from the record, and takes **availability from
the record too** — otherwise CI would skip the very OCR columns the snapshot
exists to publish. Everything downstream is recomputed from the recorded text, so
editing a check threshold in `catalog.json` changes a replayed result without
re-recording. Sources are still downloaded and hash-checked, so a replayed build
still needs network access to the publisher URLs; only the conversions are
replayed.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This section contradicts the Configurations section above it.

Lines 35-36 still state that CI supplies neither backend, so the published build runs likhit alone and reports the other two as unavailable. This new section states the opposite for the deployed site: a replay takes availability from the record and publishes all three columns. A reader who stops at the first statement draws the wrong conclusion about what the published page shows.

Update the earlier paragraph to describe the replay, or add a forward reference to this section there.

🤖 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 `@site/README.md` around lines 100 - 107, Update the earlier Configurations
paragraph to accurately describe the deployed site's replay behavior, including
that availability is read from recorded results and all published columns can be
shown; alternatively, add a clear forward reference to the later replay section.
Keep the CI configuration details consistent with the explanation in the replay
section.

Comment thread site/summarize.py
Comment on lines +89 to +109
spend: dict[str, dict[str, int]] = {
name: {"calls": 0, "tokens": 0} for name in artifact["configurations"]
}
for document in artifact["documents"]:
for run in document["runs"]:
usage = run.get("ocr_usage")
if usage and run["config"] in spend:
spend[run["config"]]["calls"] += usage["calls"]
spend[run["config"]]["tokens"] += usage["total_tokens"]

rows = []
for name, config in artifact["configurations"].items():
counts = tally[name]
total = sum(counts.values())
if config.get("available"):
note = f"{total} run(s)"
if config.get("model"):
note += f" · `{config['model']}`"
calls = spend[name]["calls"]
if calls:
note += f" · {calls} vision call(s), {spend[name]['tokens']:,} tokens"

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

Report measured zero-call OCR usage in the CI summary.

_configuration_rows() only appends OCR usage when calls is non-zero. A configuration with recorded zero calls therefore renders the same note as one with unavailable usage data.

Track whether any ocr_usage record was present. If usage was measured, render 0 vision call(s), 0 tokens. If usage was absent, render an explicit unavailable-usage state.

🤖 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 `@site/summarize.py` around lines 89 - 109, Update _configuration_rows() to
track whether any ocr_usage record was present for each configuration,
separately from its call count. Render the measured usage note whenever usage
was recorded, including zero calls and tokens; when no usage record exists,
render an explicit unavailable-usage state instead of omitting OCR information.

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.

1 participant