Skip to content

Judge gate: defence in depth around the verdict parser (API response_format, fences in the judged response) #1042

Open

Description

@cemililik

Decided in triage (comment): point 1 is implemented as a config-driven judge_response_format field; point 2 is closed (not planned); point 3 is split: reply truncation is #1044, and the rest is documented as a known limit (docstring and judge manual, EN + TR).

Summary

Follow-up to #127. PR #1038 makes _parse_judge_json fail closed when a judge reply is ambiguous (several score objects, duplicate keys, non-object replies). The defence-in-depth measures that #127 recommended next to the parser fix were deliberately left out of that PR to keep it to one concern. This issue tracks them.

What is still open

  1. Ask the API judge for a JSON object. _call_api_judge (forgelm/judge.py) sends no response_format. OpenAI-compatible endpoints that support response_format={"type": "json_object"} would then return a bare object, which the parser accepts on its first, strictest path. Endpoints that reject the parameter must keep working, so decide how it is enabled (config-driven, or a documented fallback) before coding.
  2. Neutralise fence markers in the judged response. The judged response is already passed through _neutralize_delimiter_tags before it enters the rubric. Triple-backtick fences in that text are not neutralised, so a fenced block inside the model's answer reaches the judge verbatim.
  3. Decide how a single quoted score object is treated. The parser accepts a reply that contains exactly one score-bearing object. That object cannot be told apart from one quoted out of the judged response when the judge gave no JSON verdict of its own. Decide whether the verdict can be bound to the judge's own output (for example by position) or whether the current rule is the documented, accepted limit, and record the decision in the code and the judge manual.

Acceptance criteria

  • Each of the three points above is implemented, or closed with a written reason on this issue.
  • Every behaviour change has a regression test that fails on the current development and passes after the change. Tests need no GPU, network access or model downloads.
  • If the judge manual (docs/usermanuals/en/evaluation/judge.md) or the configuration reference changes, the Turkish mirror is updated in the same PR and the bilingual guards pass.
  • A ### Changed or ### Fixed entry under ## [Unreleased] in CHANGELOG.md names the behaviour.
  • Any regex that is added follows docs/standards/regex.md.

Context

Found while reviewing PR #1038 (fix for #127). Start from forgelm/judge.py (_call_api_judge, _neutralize_delimiter_tags, _parse_judge_json) and tests/test_judge_functions.py.

Activity

  1. added
    enhancementNew feature or request
    needs-triageNot triaged yet: every new issue starts with it; only a maintainer removes it
    on Oct 20, 2026
  2. cemililik commented on Oct 20, 2026

    @cemililik
    CollaboratorAuthor

    Triage, checked against development:

    Triage result: enhancement, area: eval-gates, severity: info, wave: 4. The parser fix (#127, high) carries the risk; this is hardening around it, plus a decision on a residual limit. Re-grade if the decision in point 3 shows that a quoted score object can still reach the verdict. Not a first-contribution task: points 1 and 3 need a maintainer decision first.

    Triage, checked against development on 2026-10-20.

  3. added
    severity: infoHardening or improvement suggestion
    wave: 4Roadmap wave 4: low / info / watch, when touching the module
    on Oct 20, 2026
  4. removed
    needs-triageNot triaged yet: every new issue starts with it; only a maintainer removes it
    on Oct 20, 2026
  5. added
    blockedWaits for another issue (see Blocked by); set and removed automatically, never by hand
    on Oct 20, 2026
  6. cemililik commented on Oct 20, 2026

    @cemililik
    CollaboratorAuthor

    Decisions on the three points

    I ran the cases below offline against the parser proposed in #1038 (head 1413ffd) and checked the JSON-mode behaviour of the common endpoints. Short version: 1 is implemented as described below, 2 is closed (not planned), 3 is split: the part that can be fixed is #1044, the rest is documented as a known limit.

    1. Ask the API judge for a JSON object — implement, config-driven

    New field evaluation.llm_judge.judge_response_format: auto (default) | json_object | none.

    • auto sends response_format={"type": "json_object"} only when no judge_api_base is set, i.e. the default OpenAI endpoint, where the default model gpt-4o supports it and DEFAULT_RUBRIC already contains the word "JSON" that OpenAI's JSON mode requires. With a custom judge_api_base nothing is sent, exactly as today.
    • json_object always sends it (the operator asserts the endpoint accepts it); none never does.
    • An HTTP 400/422 on a request that carried response_format aborts the evaluation like the 401/403 case, with a message that names the field and says to set it to none. There is no silent retry without it: quietly dropping a hardening measure is the kind of silent failure this project rules out.
    • _validate_rubric also requires a custom rubric to ask for JSON (the parser accepts nothing else, and JSON mode requires the word).
    • Why not send it everywhere: JSON mode is model-dependent (Azure answers some models with a 400), Anthropic's OpenAI-SDK layer silently ignores response_format (harmless, but no protection), and vLLM documents json_schema while json_object is not confirmed. Hence the opt-in for custom endpoints.
    • What it buys: the reply is one object, so a quoted planted object can only sit inside a string such as reason, and the "quote, then verdict" ambiguity is gone; a reply cut off at the token cap is invalid JSON and becomes None.
    • Touch points: JudgeConfig (the field needs a description=), config_template.yaml, docs/reference/configuration.md and its Turkish mirror, the judge manual and its mirror, run_judge_evaluation and its call site in the trainer, _call_api_judge, tests (payload with and without the field, the 400 abort, rubric validation), CHANGELOG. Follow the add-config-field checklist.

    2. Neutralise fence markers in the judged response — closed, not planned

    • Fences are not delimiters of the judge prompt. The rubric delimits untrusted text with <user_prompt> / <assistant_response>, and those are already escaped; a fence in the answer breaks out of nothing.
    • On the output side the parser also accepts unfenced objects, so removing fences gains nothing: a quoted {"score": 10, ...} with no fence is taken as the verdict just the same (see the table in Judge: a reply cut off at the 200-token cap can leave a lone quoted score object, which the parser takes as the verdict #1044).
    • Rewriting the text cannot be made complete: a planted key can be written "score", which JSON decodes to score, and the judge copies it verbatim.
    • It would alter every judged answer that contains code, which is exactly what a quality judge has to read.

    The defences that work sit on the output side: point 1, the parser rule from #1038, and the truncation check below.

    3. A single quoted score object — split

    The verdict cannot be bound to the judge's own output by position: with one object there is no position to compare. What can be shown is where a lone quoted object actually comes from:

    What stays in this issue for point 3 is the documentation: the docstring of _parse_judge_json and a short "How replies are parsed" section in the judge manual (English and Turkish) that states which replies count (a whole object; one score object in prose), which fail (several, duplicate keys, not an object, truncated), the residual limit above, and the advice to use the API judge with JSON mode for gating.

    Labels: documentation and i18n added; severity stays info (hardening and documentation; the defect moved to #1044). This issue stays linked as blocked by #127: points 1 and 3 touch the same function, tests and manual section as #1038.

    Triage decisions, checked against development on 2026-10-20.

  7. added
    documentationImprovements or additions to documentation
    i18nEnglish/Turkish documentation mirror
    on Oct 20, 2026
  8. github-actions commented on Oct 20, 2026

    @github-actions

    This issue is no longer blocked: every issue it waited for is closed as completed (#127). It can be picked up now; see Working on an issue.

  9. removed
    blockedWaits for another issue (see Blocked by); set and removed automatically, never by hand
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: eval-gatesCode area: eval-gatesdocumentationImprovements or additions to documentationenhancementNew feature or requesti18nEnglish/Turkish documentation mirrorseverity: infoHardening or improvement suggestionwave: 4Roadmap wave 4: low / info / watch, when touching the module

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions