fix(evaluation): Limit payload size for prompts - #1194
Conversation
A prompt-improvement run dumped the whole score-trace file into the
drafting request, so a large judged run overflowed the provider's 1M
input-token ceiling and failed the job with an HTTP 400:
prompt_generation_failed: Anthropic returned HTTP 400
prompt is too long: 1121367 tokens > 1000000 maximum
Each trace is now projected down to the fields the brief actually names,
long text fields are clamped, and rows are filled to a character budget
worst-scoring-first — the brief tells the model to focus on the failing
rows, so those are the ones a cap has to keep. The brief states when
rows were dropped and the count is logged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The character cap alone bounds the payload by dropping whole rows, which loses questions the model never gets to see. A judged run repeats every question `duplication_factor` times, and the 5th repeat of a question carries far less signal than a question that is missing entirely. The brief is now measured with `messages.count_tokens` — same model and output schema as the real call, since both are billed as input — and degraded until it fits: first one repeat per question at a time, from the run's own duplication factor down to a floor of 3 (below that the repeats no longer show whether the judge is stable), and only then whole rows, worst-scoring-first against a budget rescaled from the measured count. Which repeats survive is decided by file order, not score: repeats exist to measure the judge's spread on one question, so dropping the worst of them would erase the signal the judge brief is told to read. A trace with no `question_id` is keyed uniquely and never grouped, so older datasets uploaded without ids are not collapsed into a single group. Counting is a network call from a Celery worker, so a failure there is non-fatal: the flat character cap applies on every attempt regardless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
OpenAPI changes ⚪ No API surface changesNote This PR does not modify the API contract.
|
Counting is a network call from a worker with a 300s soft time limit, and the ladder can make up to four of them. A brief under 200K characters cannot reach the 900K-token budget even at a pessimistic three tokens per character, so counting it is a wasted round trip — and that is where most runs land, leaving the path at its previous single call. Also covers the case the repeat ladder cannot help with: v1 traces from datasets uploaded without question ids are keyed uniquely and never grouped, so the rescaled character budget is the only thing that can bring an oversized brief down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Third commit added after a latency review of the ladder:
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Issue
Closes #PLEASE_TYPE_ISSUE_NUMBER
Summary
Checklist
Before submitting a pull request, please ensure that you mark these task.
fastapi run --reload app/main.py— not exercised against a live run; covered by tests instead (see Notes).Notes
Tests —
app/tests/api/routes/test_improve_prompt.py, 43 passing. Four new cases: the worst-first cap on an oversized payload (with an unscoreable"N/A"row in the mix, which a naive sort raises on), the 5→4→3 ladder keeping all questions, row-dropping taking over at the floor, and_primary_scoreranking unscoreable rows last. Each was mutation-checked — breaking the sort, the repeat limit, or the floor branch fails the corresponding test.Why not the Files API — a
documentblock with afile_idstill expands into context and bills as input tokens, so it does not move this ceiling. The only surface that would iscontainer_upload+ the code-execution tool (file lands in the sandbox, the model queries it with Python), but prompt rewriting needs to read the failing answers rather than aggregate them, so they would come back into context anyway — with an unbounded agentic loop inside a Celery soft time limit. Not worth it here.Known gap, unchanged by this PR —
crud/evaluations/summary.pysends every trace whole and has the same latent ceiling (it carries its ownponytail:note saying so). Left alone deliberately: it needs a different projection, so sharing one would mean an abstraction with two diverging implementations. The wiki now points at both.Original PR description
Issue
No issue filed — reported in-session:
Summary
Prompt iteration dumped the entire score-trace file into the drafting request
(
json.dumps(traces), no bound). A large judged run — every question repeatedduplication_factortimes, three metrics each, a paragraph of judge reasoningper metric — overflowed the provider's 1M input-token ceiling, so Anthropic
returned a 400 and the job was marked FAILED.
Two commits, in the order they should be read:
1. Cap the payload. Each trace is projected down to the fields the brief
actually names (
trace_idand, on v1,question_idand the judgecommentwere shipped and referenced nowhere), long text fields are clamped, and rows are
filled to a character budget worst-scoring-first — the brief tells the model to
focus on the failing rows, so those are the ones a cap has to keep.
2. Degrade repeats before rows. Dropping rows loses questions the model never
sees at all; the 5th repeat of a question carries much less signal. The brief is
now measured with
messages.count_tokens(same model and output schema as thereal call, since both are billed as input) and degraded until it fits:
worst-scoring-first, against a character budget rescaled from the measured count.
Which repeats survive is decided by file order, not score: repeats exist to show
the judge's spread on one question, so dropping the worst of them would erase the
signal the judge brief is told to read. A trace without a
question_idis keyeduniquely and never grouped, so a dataset uploaded without ids can't collapse into
one group and get cut to a handful of rows.
The brief states how many traces it is not seeing, and each degradation step logs
a line that distinguishes repeat-cutting from row-cutting.
Checklist
fastapi run --reload app/main.py— not exercised against a live run;covered by tests instead (see Notes).
Notes
Tests —
app/tests/api/routes/test_improve_prompt.py, 43 passing. Four newcases: the worst-first cap on an oversized payload (with an unscoreable
"N/A"row in the mix, which a naive sort raises on), the 5→4→3 ladder keeping all
questions, row-dropping taking over at the floor, and
_primary_scorerankingunscoreable rows last. Each was mutation-checked — breaking the sort, the repeat
limit, or the floor branch fails the corresponding test.
Why not the Files API — a
documentblock with afile_idstill expands intocontext and bills as input tokens, so it does not move this ceiling. The only
surface that would is
container_upload+ the code-execution tool (file lands inthe sandbox, the model queries it with Python), but prompt rewriting needs to read
the failing answers rather than aggregate them, so they would come back into
context anyway — with an unbounded agentic loop inside a Celery soft time limit.
Not worth it here.
Known gap, unchanged by this PR —
crud/evaluations/summary.pysends everytrace whole and has the same latent ceiling (it carries its own
ponytail:notesaying so). Left alone deliberately: it needs a different projection, so sharing
one would mean an abstraction with two diverging implementations. The wiki now
points at both.
🤖 Generated with Claude Code