Skip to content

feat(prism): dual-gate plagiarism with OpenRouter Grok 4.5 sole borderline verdict - #47

Merged
echobt merged 3 commits into
BaseIntelligence:mainfrom
alpha1122x:feat/prism-plagiarism-llm-adjudicator
Jul 27, 2026
Merged

feat(prism): dual-gate plagiarism with OpenRouter Grok 4.5 sole borderline verdict#47
echobt merged 3 commits into
BaseIntelligence:mainfrom
alpha1122x:feat/prism-plagiarism-llm-adjudicator

Conversation

@alpha1122x

@alpha1122x alpha1122x commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a dual-gate Prism plagiarism path: deterministic ranker still hard-rejects exact/near hash clones; quarantine/attach ranks go only to an OpenRouter LLM adjudicator for the final verdict.
  • Default model is x-ai/grok-4.5 (OpenRouter slug matching opencode openrouter/x-ai/grok-4.5).
  • Fail-closed when LLM is required but unavailable; production can enable via PRISM_PLAGIARISM_LLM_* settings.

Changes

  • New plagiarism_adjudicator.py: structured OpenRouter tool-call SubmitPlagiarismVerdict via httpx.
  • config.py: LLM enable/required/model/key-file settings (PRISM_OPENROUTER_*, PRISM_PLAGIARISM_LLM_*).
  • queue.py: wire dual-gate after deterministic rank; map LLM verdict to pass/reject + reasons.
  • Tests: adjudicator unit coverage + gateway wiring updated for LLM-required path (25 tests in suite).

Test plan

  • uv run pytest packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py (and related plagiarism wiring) — 25 passed
  • Live prod smoke: clone → plagiarized=True used_llm=True model=x-ai/grok-4.5
  • CI green on this head SHA
  • Post-merge prod recheck (health + plagiarism gate with OpenRouter secret mount)

CI

  • Waiting for required checks to go green on this head SHA.

Notes

  • LLM calls use OpenRouter; deploy needs PRISM_OPENROUTER_API_KEY_FILE (or env) and PRISM_PLAGIARISM_LLM_ENABLED=true / REQUIRED=true as appropriate — no secrets in this PR.
  • Deterministic exact-match hard reject is unchanged; LLM only owns borderline ranks.

Summary by CodeRabbit

  • New Features
    • Added OpenRouter-based LLM plagiarism adjudication for borderline, quarantine, and held duplicate cases.
    • Introduced configurable LLM settings (enable/required flags, timeout, temperature, token/retry limits, and source excerpt size) plus OpenRouter model/base URL and API key inputs.
  • Bug Fixes
    • Fail-closed behavior: if adjudication can’t run or results can’t be validated, submissions are treated as plagiarized.
    • Exact source-hash matches continue to hard-reject immediately.
  • Tests
    • Added a full test suite covering verdicts, prompt construction, fail-closed scenarios, and worker integration behavior.

echobt added 2 commits July 27, 2026 10:55
…ole verdict

Borderline/attach pairs no longer hard-reject from static scores alone. Exact
source-hash clones still hard-reject; quarantine/attach outcomes call the
OpenRouter adjudicator (httpx tool-calling) which alone allows or rejects.
OpenRouter key loaded from /run/secrets/openrouter_api_key for durable prod use.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74e9eb1c-4ca5-419f-afd9-4521db96b2f7

📥 Commits

Reviewing files that changed from the base of the PR and between abdcb0c and 4494307.

📒 Files selected for processing (3)
  • packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py
  • packages/challenges/prism/src/prism_challenge/queue.py
  • packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/challenges/prism/src/prism_challenge/queue.py
  • packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py
  • packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py

📝 Walkthrough

Walkthrough

Prism adds OpenRouter-backed plagiarism adjudication for borderline duplicate submissions. Exact source-hash matches remain deterministic rejects; borderline cases use structured tool-call verdicts, fail closed on errors, persist adjudication metadata, and are covered by unit and worker integration tests.

Changes

Plagiarism adjudication flow

Layer / File(s) Summary
Adjudication contract and configuration
packages/challenges/prism/src/prism_challenge/config.py, packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py, packages/challenges/prism/tests/test_gateway_absence_and_deterministic_admission.py
PrismSettings exposes OpenRouter and plagiarism-LLM controls, while structured configuration, verdict, and tool schemas define the adjudicator interface.
OpenRouter adjudication engine
packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py, packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py
The adjudicator builds prompts, calls OpenRouter, validates tool-call responses, normalizes verdicts, resolves key files, retries failures, and rejects when required adjudication cannot complete.
Static review verdict integration
packages/challenges/prism/src/prism_challenge/queue.py, packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py
The worker sends borderline pairs through LLM adjudication, persists the result, retains exact-hash hard rejection, and applies LLM verdicts to review outcomes.

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

Sequence Diagram(s)

sequenceDiagram
  participant PrismWorker
  participant adjudicate_plagiarism
  participant OpenRouter
  PrismWorker->>adjudicate_plagiarism: provide borderline comparison data
  adjudicate_plagiarism->>OpenRouter: request SubmitPlagiarismVerdict tool call
  OpenRouter-->>adjudicate_plagiarism: return structured verdict
  adjudicate_plagiarism-->>PrismWorker: return normalized adjudication
  PrismWorker->>PrismWorker: persist verdict and determine acceptance
Loading
🚥 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 clearly summarizes the main change: a dual-gate plagiarism workflow with OpenRouter-based borderline verdicts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate 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.

… gate

Make package-scoped prism-checks pass: collections.abc Mapping, import
sort, drop unused test imports, wrap long getattr line.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py (1)

211-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the exact-hash short-circuit at the worker level too.

This test asserts classify_duplicate behavior only; the "no LLM for exact hash" guarantee lives in _review_static_submission. A worker-level test that monkeypatches adjudicate_plagiarism and asserts it is never called would pin the actual dual-gate contract.

🤖 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 `@packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py` around
lines 211 - 244, Add a worker-level test for _review_static_submission that
submits an exact source-hash match, monkeypatches adjudicate_plagiarism, and
asserts the patched function is never called while the submission is rejected.
Keep the existing classify_duplicate unit test unchanged and verify the worker’s
dual-gate short-circuit directly.
packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py (1)

360-392: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Retries have no backoff; a 429 burst will be retried immediately and then fail closed.

Since a failed adjudication rejects a submission, an immediate retry on rate limiting is likely to waste the only retry. Consider a short sleep (with jitter) between attempts, and skipping retries for non-retryable 4xx.

🤖 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
`@packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py`
around lines 360 - 392, Add retry backoff to the attempt loop surrounding the
HTTP call and verdict extraction: after a failed attempt, sleep briefly with
jitter before the next retry, especially for rate-limit responses. Detect
non-retryable 4xx failures and stop retrying them immediately, while preserving
the existing fail-closed behavior and final exception handling.
🤖 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
`@packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py`:
- Line 419: Fix the Ruff E501 violation in the configuration call by formatting
the max_source_chars argument across multiple lines while preserving its
existing value and fallback behavior.
- Line 25: Update the typing imports in plagiarism_adjudicator.py to remove
Mapping from typing and import it from collections.abc instead, while preserving
the existing Any and Protocol imports and all usage sites.

In `@packages/challenges/prism/src/prism_challenge/queue.py`:
- Around line 854-863: Update the adjudication call in the queue flow to run the
synchronous adjudicate_plagiarism function via await asyncio.to_thread,
forwarding the existing keyword arguments unchanged and preserving its returned
adjudication result.

In `@packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py`:
- Around line 251-309: Remove the dead ctx construction and its associated
PrismContext, component evaluator, and other unused imports in this test.
Preserve the PrismWorker import because it is required by the worker
instantiation, and keep the test’s actual database, repository, and worker setup
unchanged.

---

Nitpick comments:
In
`@packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py`:
- Around line 360-392: Add retry backoff to the attempt loop surrounding the
HTTP call and verdict extraction: after a failed attempt, sleep briefly with
jitter before the next retry, especially for rate-limit responses. Detect
non-retryable 4xx failures and stop retrying them immediately, while preserving
the existing fail-closed behavior and final exception handling.

In `@packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py`:
- Around line 211-244: Add a worker-level test for _review_static_submission
that submits an exact source-hash match, monkeypatches adjudicate_plagiarism,
and asserts the patched function is never called while the submission is
rejected. Keep the existing classify_duplicate unit test unchanged and verify
the worker’s dual-gate short-circuit directly.
🪄 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 Plus

Run ID: 1c36a061-d2a8-4a4d-9e5b-c55dfb3331b7

📥 Commits

Reviewing files that changed from the base of the PR and between a45f1ee and abdcb0c.

📒 Files selected for processing (5)
  • packages/challenges/prism/src/prism_challenge/config.py
  • packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py
  • packages/challenges/prism/src/prism_challenge/queue.py
  • packages/challenges/prism/tests/test_gateway_absence_and_deterministic_admission.py
  • packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py

Comment thread packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py Outdated
Comment thread packages/challenges/prism/src/prism_challenge/evaluator/plagiarism_adjudicator.py Outdated
Comment on lines +854 to +863
llm_cfg = plagiarism_llm_config_from_settings(self.settings)
adjudication = adjudicate_plagiarism(
current_code=current_code,
candidate_code=candidate_code,
comparison_report=pair_report,
deterministic_reason=duplicate.reason,
deterministic_outcome=duplicate.outcome,
candidate_submission_id=cand.submission_id,
config=llm_cfg,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Blocking HTTP call on the event loop: wrap adjudicate_plagiarism in asyncio.to_thread.

adjudicate_plagiarism is synchronous and issues an httpx.Client.post with timeout_seconds (default 90s) for up to max_retries + 1 attempts — up to ~180s of blocked event loop per borderline submission. In combined mode the API and worker share one loop (see combined_mode in packages/challenges/prism/src/prism_challenge/config.py Lines 352-360), so /health, /v1/registry, and /v1/weights/latest stall for the duration. asyncio is already imported in this module.

As per coding guidelines, "Keep the documented network surfaces available: /health, /v1/registry, /v1/weights/latest, ...".

🛠️ Proposed fix
                 llm_cfg = plagiarism_llm_config_from_settings(self.settings)
-                adjudication = adjudicate_plagiarism(
-                    current_code=current_code,
-                    candidate_code=candidate_code,
-                    comparison_report=pair_report,
-                    deterministic_reason=duplicate.reason,
-                    deterministic_outcome=duplicate.outcome,
-                    candidate_submission_id=cand.submission_id,
-                    config=llm_cfg,
-                )
+                adjudication = await asyncio.to_thread(
+                    partial(
+                        adjudicate_plagiarism,
+                        current_code=current_code,
+                        candidate_code=candidate_code,
+                        comparison_report=pair_report,
+                        deterministic_reason=duplicate.reason,
+                        deterministic_outcome=duplicate.outcome,
+                        candidate_submission_id=cand.submission_id,
+                        config=llm_cfg,
+                    )
+                )

(asyncio.to_thread forwards kwargs directly, so partial is optional — await asyncio.to_thread(adjudicate_plagiarism, current_code=..., ...) works as well. The existing tests monkeypatch prism_challenge.queue.adjudicate_plagiarism, so they keep working.)

🤖 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 `@packages/challenges/prism/src/prism_challenge/queue.py` around lines 854 -
863, Update the adjudication call in the queue flow to run the synchronous
adjudicate_plagiarism function via await asyncio.to_thread, forwarding the
existing keyword arguments unchanged and preserving its returned adjudication
result.

Source: Coding guidelines

Comment thread packages/challenges/prism/tests/test_plagiarism_llm_adjudicator.py
@echobt
echobt merged commit a13d42a into BaseIntelligence:main Jul 27, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants