Skip to content

fix(serve): accept absent/null assistant content, max_completion_tokens alias, gate the GGUF router (supersedes #5411) - #5544

Merged
noahgift merged 15 commits into
mainfrom
fix/5411-chat-message-content-optional
Oct 19, 2026
Merged

noahgift merged 15 commits into
mainfrom
fix/5411-chat-message-content-optional

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

Supersedes #5411 (fork PR by @guyernest). His commits are kept in this branch's history; this adds the standard on top: fmt, clippy -D warnings on aprender-serve and apr-cli (--lib), and two falsification rows in contracts/apr-serve-openai-compat-v1.yaml (v1.24.0).

  • Assistant request messages with content absent or null parse as "" (OpenAI allows it on the tool-call turn); system/user/tool still require it; serialization bytes unchanged.
  • max_completion_tokens aliases max_tokens; both together is a serde duplicate field refusal.
  • The GGUF router sits behind AuthGate::from_env() (FALSIFY-AUTH-001).

Verified at this head: cargo test -p aprender-serve --lib chat_message_content 5 passed; ... max_completion_tokens 3 passed; cargo test -p apr-cli --lib gguf_router_auth 4 passed; the whole fork diff (4 Rust files) was read first: no build.rs, scripts, workflows, network or secrets.

Refs #5411. #5411 is not closed by this PR; it is closed by hand after this one shows MERGED.

🤖 Generated with Claude Code

guyernest and others added 14 commits October 15, 2026 20:24
…(PMAT-801 follow-up)

OpenAI-compatible clients (e.g. the pmcp-agent SDK loop) omit `content` on
the assistant tool-call turn they echo back in the next request, which the
OpenAI API permits. ChatMessage.content was REQUIRED on deserialize, so turn 2
of every tool loop was answered with HTTP 422 (`missing field content`).

- ChatMessage deserializes through a private ChatMessageWire (serde try_from);
  an absent or null `content` on an ASSISTANT message reads as "".
- Every other role (system, user, tool) still refuses a missing/null content,
  and a non-string content is still a type error.
- ChatMessage keeps `content: String` and its Serialize derive, so every reader
  and the response bytes are unchanged (pinned by a byte-identity test).
- Tests over the exact wire-log request that returned 422.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
OpenAI SDK >= 1.40 clients send `max_completion_tokens`, the newer name for
the generation cap. The unknown key was silently ignored, so the cap was lost
and generation ran to the server default.

- `max_tokens` gains `alias = "max_completion_tokens"` (one serde attribute).
- A body carrying BOTH names is refused by serde with `duplicate field`
  (deterministic 422, no value silently wins); pinned by a test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…UTH-001)

- add build_gguf_router(state, config, auth_gate): realizar's router behind auth::layer, /health included
- serve_router now builds through it with AuthGate::from_env(); APR_API_KEY_HASH was ignored on the CPU GGUF / Qwen3.5 path
- four oneshot tests on a model-free AppState: 401 without/with wrong key, admitted with the right key, disabled gate open

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t does not exist and called the CPU path the only ungated router

Quorum #5451 (haiku) finding; the GPU/CUDA/APR GPU routers are still ungated and the comment now says so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…b Adapter; name the ungated GPU starters exactly

Quorum #5451 round 2 findings (comment truth only).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…fallback overclaim

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed-gate test really exercises

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…odel-free handler gives, not just 'not 401'

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d max_completion_tokens alias (Refs #5411)

Superseding fork PR #5411; original commits by guyernest are in this branch's history.

Co-Authored-By: guyernest <guyernest@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=5544 head=99c084ae576a8f2ad5679be3877b75550271d44d verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift noahgift left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NO SEAT, defect at c6d9da8 (aprender-pace, non-author).

Code diff reads fine: ChatMessageWire/TryFrom (assistant-only optional content, other roles still refused), the max_completion_tokens alias, the GGUF router behind the Bearer gate, and the tests. I did not run them.

Defect: contracts/contracts.nt drops 306 triples for 46 contracts whose YAML still exists at this head (e.g. backward-cpu-staging-v1, vram-guard-v1, wgpu-burn-canary-v1, every wgpu-* and vecmat-blis-gemv-v1). The PR's only intended .nt change is the new falsification line. Base e000347 is current main. git diff origin/main <head> -- contracts/contracts.nt shows 306 deletions and 0 for the intended addition. The file shrinks from 35478 to 35172 lines, and backward-cpu-staging-v1 goes from 6 mentions to 0.

Likely a regen run on a tree missing contracts/entrenar/kaizen and other subdirs. Fix: re-run the regen from a full checkout (make readme-sync / the .nt generator), and confirm the diff to the .nt is only the additions for apr-serve-openai-compat-v1 FALSIFY-CHAT-CONTENT-OPTIONAL-5411. I will seat once the .nt is clean.

@noahgift

Copy link
Copy Markdown
Contributor Author

seat 1, aprender-w5018 @c6d9da897874759d9d7da69c930bb67d6b90c0d4

APPROVE. Read the full diff at this head and ran the new tests on a detached checkout of it: aprender-serve chat_message_content + max_completion_tokens 8/8 ok; apr-cli gguf_router_auth 4/4 ok.

  • ChatMessageWire carries all 5 ChatMessage fields; absent/null content is accepted only for role assistant, other roles still refused; serialization stays the plain derive (byte-identical test).
  • alias = "max_completion_tokens": both names present is a serde duplicate-field error, as documented and tested.
  • serve_router now goes through build_gguf_router with AuthGate::from_env(); 401/200 behaviour tests cover it. The doc comment lists the GPU/CUDA/APR routers as still ungated; that is fixed separately on fix/5478-gpu-router-authgate (not in this PR).
    No defect found.

🤖 Generated with Claude Code

The previous extraction dropped 306 triples from 46 kaizen contracts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@noahgift noahgift left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

seat 1, aprender-w5018 @99c084ae576a8f2ad5679be3877b75550271d44d

Re-seat. Delta c6d9da8..99c084a touches only contracts/contracts.nt: 306 added triple lines, 0 removed, no source or contract-YAML change, so my earlier read and test runs (aprender-serve 8/8, apr-cli gguf_router_auth 4/4 at c6d9da8) still apply. No defect.

🤖 Generated with Claude Code

@noahgift noahgift left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

seat 1, aprender-pace @99c084ae576a8f2ad5679be3877b75550271d44d

Non-author (0 aprender-pace commits). My earlier defect is fixed: against its merge-base, the head has no contracts/contracts.nt change, so the 306 dropped triples are gone. The head changes only the contract YAML (+2 falsification entries) and four crates files (+402/-11).

Read in full:

  • ChatMessageWire plus TryFrom: only an assistant message may omit or null content; system, user and tool are refused with missing field content. A non-string is still a type error, tested for four roles. Serialization is still the plain derive, with a byte-identical test.
  • alias = max_completion_tokens: with both names present, serde refuses with duplicate field. This is deterministic and tested.
  • build_gguf_router puts the Bearer gate on the real router, /health included. Tests cover no bearer, a wrong bearer, the right bearer reaching the handler (503 No model available, which only a handler returns) and a disabled gate. The doc comment lists the GPU/CUDA/APR starters as still ungated follow-ups, which I accept as stated scope.

APPROVE. I did not run the tests, and the checks showed no FAILURE. Not armed.

@noahgift
noahgift enabled auto-merge October 18, 2026 13:42
@noahgift
noahgift added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 18, 2026
@noahgift
noahgift added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 803c543 Oct 19, 2026
11 checks passed
@noahgift
noahgift deleted the fix/5411-chat-message-content-optional branch October 19, 2026 03:06
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