fix(reviewer): show the model the schema instead of naming it #86

Merged
rcsheets merged 1 commit from fix/gllm-show-the-schema into main 2026-07-27 23:53:50 +00:00
Owner

Closes #85.

What was happening

Two reviews on brooktrails/infra posted a JSON blob as the review body:

{"review_id": "auto-pr-226", "verdict": "approved", "summary": "...", "blocking": false}

verdict and verdict_descriptive decoded correctly beside it, finish_reason was stop, no truncation, no prompt override. The model put an invented JSON payload into the body string.

Why

v0.41.2 (#84) gave the gllm path this closing instruction:

Respond with a single JSON object conforming to the submit_review schema.

The model has never seen that schema. gllm compiles it into a decoding grammar and does not put it in the model's context — unlike tool calling, where the provider renders the tool definition, description text and all. The instruction named an object the model had no way to know, so it invented a plausible submit_review payload. The grammar still forced the real keys at the top level, so the invention landed inside body.

The previous instruction ("use the submit_review tool") was wrong in a way the model ignored — it wrote prose, and the grammar shaped it correctly. The new one is more directive about JSON, so the model took the schema name seriously. Distribution matches:

version gllm reviews JSON bodies
0.33.1 – 0.41.0 17 0
0.41.2 4 2

The fix

Render the schema into the prompt, gllmSchema applied, so the text the model reads and the grammar its output is decoded against cannot describe different objects. That subsumes the matching-names property the old instruction was built around — there are no longer two names to keep in agreement.

submission drops to the tool sentence alone; JSON mode no longer has a per-pass line of its own.

Also states in the body description that the field holds US-English Markdown prose and not a nested payload — the failure written as a rule, in the one place all three backends read.

Measured against the live endpoint

inference.brooktrails.org, mistral-small-4, same schema and diff, counting bodies that start with {:

prompt nested JSON
current (names the schema) 3 / 4
schema in prompt, hand-written field description 0 / 4
schema in prompt, rendered 0 / 4
final prompt, rendered from this branch 0 / 6

Quality moves too. The schema description text reaches this path for the first time, including the body-as-scratchpad contract, and the bodies go from one-line boilerplate:

No alarming issues detected in the changes.

to reasoning about the actual diff:

The change adds an instance label matcher to the RGW GET throughput metric query. Without this matcher, the rate calculation could aggregate data across multiple RGW instances…

Notes

  • Tool mode is untouched — same sentences byte for byte. Anthropic and vLLM do see the reworded body description, which is the intended reach of that change.
  • Property order in the rendered schema is alphabetical (map[string]any + encoding/json sorts keys). The intended emission order survives only in required, which is a []string. x-gllm-ordered is what would make it binding; still a follow-up.
  • docs/gllm-schema-notes.md gains a section on why the schema is sent twice.

Tests

  • TestJSONSchemaPromptsCarryTheirSchema — every pass's prompt against its builder.
  • TestGLLMPromptSchemaMatchesRequest — the prompt's schema against the schema in the wire request it was sent with (replaces the name-matching version).
  • TestJSONSchemaReviewPromptShowsTierVerdicts — the enum shown matches the tier.
  • TestToolModePromptsUnchanged unmodified and passing.

gofmt, go vet, go build ./..., go test -race ./... all clean.

🤖 Generated with Claude Code

Closes #85. ## What was happening Two reviews on `brooktrails/infra` posted a JSON blob as the review body: ```json {"review_id": "auto-pr-226", "verdict": "approved", "summary": "...", "blocking": false} ``` `verdict` and `verdict_descriptive` decoded correctly beside it, `finish_reason` was `stop`, no truncation, no prompt override. The model put an invented JSON payload into the `body` string. ## Why v0.41.2 (#84) gave the gllm path this closing instruction: > Respond with a single JSON object conforming to the submit_review schema. The model has never seen that schema. gllm compiles it into a decoding grammar and does not put it in the model's context — unlike tool calling, where the provider renders the tool definition, `description` text and all. The instruction named an object the model had no way to know, so it invented a plausible `submit_review` payload. The grammar still forced the real keys at the top level, so the invention landed *inside* `body`. The previous instruction ("use the submit_review tool") was wrong in a way the model ignored — it wrote prose, and the grammar shaped it correctly. The new one is more directive about JSON, so the model took the schema name seriously. Distribution matches: | version | gllm reviews | JSON bodies | |---|---|---| | 0.33.1 – 0.41.0 | 17 | 0 | | 0.41.2 | 4 | **2** | ## The fix Render the schema into the prompt, `gllmSchema` applied, so the text the model reads and the grammar its output is decoded against cannot describe different objects. That subsumes the matching-names property the old instruction was built around — there are no longer two names to keep in agreement. `submission` drops to the tool sentence alone; JSON mode no longer has a per-pass line of its own. Also states in the `body` description that the field holds US-English Markdown prose and not a nested payload — the failure written as a rule, in the one place all three backends read. ## Measured against the live endpoint `inference.brooktrails.org`, `mistral-small-4`, same schema and diff, counting bodies that start with `{`: | prompt | nested JSON | |---|---| | current (names the schema) | 3 / 4 | | schema in prompt, hand-written field description | 0 / 4 | | schema in prompt, rendered | 0 / 4 | | **final prompt, rendered from this branch** | **0 / 6** | Quality moves too. The schema `description` text reaches this path for the first time, including the body-as-scratchpad contract, and the bodies go from one-line boilerplate: > No alarming issues detected in the changes. to reasoning about the actual diff: > The change adds an `instance` label matcher to the RGW GET throughput metric query. Without this matcher, the rate calculation could aggregate data across multiple RGW instances… ## Notes - Tool mode is untouched — same sentences byte for byte. Anthropic and vLLM do see the reworded `body` description, which is the intended reach of that change. - Property order in the rendered schema is alphabetical (`map[string]any` + `encoding/json` sorts keys). The intended emission order survives only in `required`, which is a `[]string`. `x-gllm-ordered` is what would make it binding; still a follow-up. - `docs/gllm-schema-notes.md` gains a section on why the schema is sent twice. ## Tests - `TestJSONSchemaPromptsCarryTheirSchema` — every pass's prompt against its builder. - `TestGLLMPromptSchemaMatchesRequest` — the prompt's schema against the schema in the wire request it was sent with (replaces the name-matching version). - `TestJSONSchemaReviewPromptShowsTierVerdicts` — the enum shown matches the tier. - `TestToolModePromptsUnchanged` unmodified and passing. `gofmt`, `go vet`, `go build ./...`, `go test -race ./...` all clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(reviewer): show the model the schema instead of naming it
All checks were successful
ci / check (pull_request) Successful in 42s
ac0bae792f
v0.41.2 gave the gllm path a closing instruction that reads "Respond
with a single JSON object conforming to the submit_review schema." The
model has never seen that schema. gllm compiles it into a decoding
grammar and does not put it in the model's context — unlike tool
calling, where the provider renders the definition, descriptions and
all. So the instruction named an object the model had no way to know,
and it filled the gap by inventing a plausible submit_review payload.

The grammar still forced the real keys at the top level, so the
invention landed inside the body string. Two reviews reached Forgejo as

  {"review_id": "auto-pr-226", "verdict": "approved", "summary": ...}

with verdict and verdict_descriptive decoding correctly beside it and
finish_reason=stop. Nothing in the pipeline was broken; the model was
answering a question it could not have answered. 2 of the 4 gllm reviews
on 0.41.2 came out this way, against 0 of the 17 on every version
before it.

Render the schema into the prompt instead, gllmSchema applied, so the
text the model reads and the grammar its output is decoded against
cannot describe different objects. That subsumes the matching-names
property the old instruction was built around: there are no longer two
names to keep in agreement. submission drops to the tool sentence alone,
since JSON mode no longer has a per-pass line of its own.

Against mistral-small-4 the old prompt reproduced the nested JSON in 3
of 4 samples and the new one in 0 of 14. Review quality moves too — the
schema descriptions reach this path for the first time, and the
body-as-scratchpad contract they carry is visible in the output, which
goes from one-line boilerplate to actual reasoning about the diff.

Also say plainly in the body description that the field holds
US-English Markdown prose and not a nested payload. That is the failure
stated as a rule, in the one place all three backends read.

Tool mode is untouched: same sentences, byte for byte, and Anthropic and
vLLM continue to get their schemas via tool definitions. They do see the
reworded body description, which is the intended reach of that change.
Collaborator

Automated review by pr-reviewer v0.41.2 | Safety Check | Mistral Small | tracking id r-67eebe-216b39
This is an AI-generated review and may contain mistakes.

Status: Completed


Verdict: LGTM — All changes are safe, well-tested, and address the reported issue without introducing new risks. No security concerns, breaking changes, or accidentally committed files detected.

No issues found. Changes are safe and properly tested.

*Automated review by [pr-reviewer](https://git.brooktrails.org/brooktrails/pr-reviewer) v0.41.2 | Safety Check | Mistral Small | tracking id `r-67eebe-216b39`* *This is an AI-generated review and may contain mistakes.* **Status:** ✅ Completed --- **✅ Verdict: LGTM** — All changes are safe, well-tested, and address the reported issue without introducing new risks. No security concerns, breaking changes, or accidentally committed files detected. No issues found. Changes are safe and properly tested.
rcsheets deleted branch fix/gllm-show-the-schema 2026-07-27 23:53:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
brooktrails/pr-reviewer!86
No description provided.