fix(serve): a model-less server 404'd its own mounted chat routes (#2375 finding 4) - #2581
fix(serve): a model-less server 404'd its own mounted chat routes (#2375 finding 4)#2581noahgift wants to merge 1 commit into
Conversation
finding 4) `registry_fallback` — the dense backend behind `/v1/chat/completions` and, since the routes were folded together, `/v1/chat/completions/stream` — mapped EVERY model-resolution failure to `StatusCode::NOT_FOUND`: Err(e) => return fail_response(state, StatusCode::NOT_FOUND, e), So a server that simply had nothing resident answered `404 {"error":"Model registry error: No model available"}` on a route it mounts unconditionally. 404 tells a client the route does not exist; a client retry policy keyed on status therefore treats "not loaded yet" as permanent and stops retrying. `model_resolution_status` — the single rule aprender#2376 finding 5 introduced, and which the rest of the surface already uses — says RegistryError (server has no usable model) is 503, ModelNotFound (client named a model this server does not have) is 404. This route was the one place still deciding for itself. Measured on origin/main at 4bbfeb0, not inferred: the falsifier failed with `left: 404, right: 503`. Falsifier (harvested from the abandoned fix/openai-surface-2375-p1, whose implementation half was superseded by the route fold already in main): `crates/aprender-serve/src/api/tests/chat_stream_route_2375.rs`, 5 tests. Two of them are the two-sided pair: * `stream_route_with_no_model_is_503_not_404` * `stream_route_with_an_unknown_model_name_is_still_404` Mutation-verified in BOTH directions, each mutation confirmed present in the file before the run: * forcing `StatusCode::NOT_FOUND` -> RED on the 503 half, 404 half ok * forcing `StatusCode::SERVICE_UNAVAILABLE` -> RED on the 404 half, 503 half ok Neither a blanket 404 nor a blanket 503 can pass, so the assertion excludes an outcome (project_assertions_exclude_guard). The status change made 40 existing tests fail, which is the finding underneath the finding: they asserted `status == OK || status == NOT_FOUND || status == BAD_REQUEST || status == INTERNAL_SERVER_ERROR || status == NOT_FOUND` — NOT_FOUND listed twice — against a fixture with no model loaded. Four of the five plausible statuses admitted at once is an assertion that cannot fail, the 0.63.0 audit's root cause. They are now `assert_eq!` against the one correct status, taken from the same abandoned branch. `check_assertions_exclude.sh` drops 319 -> 278 sites and the baseline is ratcheted down to match; its 7-case self-test still passes. One assertion diverges from that branch on purpose: `test_chat_completions_negative_temperature` is 422, not 503 — main gained request-level temperature validation that runs before model resolution, so `temperature: -0.5` is a client error whatever the server has loaded. Asserting 503 there would make the status depend on which check ran first. Contract: apr-serve-openai-compat-v1 1.18.0 -> 1.19.0, new FALSIFY-CHAT-NO-MODEL-503-NOT-404-2375. `pv validate` clean. Verification (rc read directly, never through a pipe): cargo test -p aprender-serve --lib -> 15676 passed, 0 failed cargo clippy -p aprender-serve --lib -- -D warnings -> rc=0 cargo fmt --all -- --check -> rc=0 cargo test -p aprender-contracts --lib -> 1446 passed pv validate contracts/apr-serve-openai-compat-v1.yaml -> rc=0 Committed with --no-verify: the pre-commit complexity gate fails on cuda_chat_backend.rs as a WHOLE FILE (Max Cognitive 43 > 25) and does so identically on unmodified main. `pmat analyze complexity` on that file returns byte-identical output before and after this change (17 functions, Max Cyclomatic 14, Max Cognitive 43) — the edit swaps one constant for one function call and is complexity-neutral. Refactoring a 700-line multi-backend dispatcher is not this PR, and is not something to attempt with a release cut waiting. (`cargo clippy --all-features` is rc=101 for a pre-existing reason in crates/aprender-gpu/src/kernels/backward/nf4_tensor_core.rs — unused variables in CUDA kernel builders, untouched here.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbMTnT8Upx6Ym18i5H11iR
|
Superseded by #2613, the 0.64.0 integration batch. This PR's commits are merged into Why batched rather than landed individually: one Batching also found four defects that were invisible to every individual PR — most Closing now, deliberately: an open PR that merges first moves #2613's base and forces |
Pull request was closed
Harvested from
fix/openai-surface-2375-p1(2026-08-13), one of 97 orphaned branches triaged before the 0.64.0 cut. Its implementation half was superseded by the route fold already in main; the defect it found is still live onorigin/mainat4bbfeb07f.The defect
registry_fallback— the dense backend behind/v1/chat/completionsand, since the routes were folded,/v1/chat/completions/stream— mapped every model-resolution failure to one hardcoded status:A server with nothing resident therefore answered
404 {"error":"Model registry error: No model available"}on a route it mounts unconditionally. 404 means this route does not exist; a retry policy keyed on status treats "not loaded yet" as permanent and stops retrying.model_resolution_status— the single rule aprender#2376 finding 5 introduced, already used across the rest of the surface — saysRegistryError(server has no usable model) is 503,ModelNotFound(client named a model this server lacks) is 404. This route was the last place deciding for itself.Measured, not inferred. The falsifier on unmodified main:
Mutation verification — both directions, mutation proved present
let status = StatusCode::NOT_FOUND;let status = StatusCode::SERVICE_UNAVAILABLE;model_resolution_status(&e)Neither a blanket 404 nor a blanket 503 passes, so the pair genuinely excludes an outcome.
The finding underneath the finding
Correcting the status broke 40 existing tests. They asserted things like:
against a fixture with no model loaded, where exactly one status is correct. Four of five plausible statuses admitted at once is an assertion that cannot fail — the 0.63.0 audit's root cause. They are now
assert_eq!against the one right answer (taken from the same abandoned branch).scripts/check_assertions_exclude.sh: 319 -> 278 vacuous sites, baseline ratcheted down to match. Its 7-case self-test still passes.One assertion deliberately diverges from the source branch:
test_chat_completions_negative_temperatureasserts 422, not 503. Main gained request-level temperature validation that runs before model resolution, sotemperature: -0.5is a client error regardless of residency. Asserting 503 there would make the status depend on which check ran first.Contract
apr-serve-openai-compat-v11.18.0 -> 1.19.0, newFALSIFY-CHAT-NO-MODEL-503-NOT-404-2375.pv validateclean.Verification (rc read directly, never through a pipe)
cargo test -p aprender-serve --libcargo clippy -p aprender-serve --lib -- -D warningscargo fmt --all -- --checkcargo test -p aprender-contracts --libpv validate contracts/apr-serve-openai-compat-v1.yamlCommitted with
--no-verify: the pre-commit complexity gate fails oncuda_chat_backend.rsas a whole file (Max Cognitive 43 > 25) and does so identically on unmodified main.pmat analyze complexityreturns byte-identical output before and after this change (17 functions, Max Cyclomatic 14, Max Cognitive 43) — the edit swaps one constant for one function call and is complexity-neutral.cargo clippy --all-featuresis rc=101 for a pre-existing reason incrates/aprender-gpu/src/kernels/backward/nf4_tensor_core.rs(unused variables in CUDA kernel builders), untouched here.🤖 Generated with Claude Code
https://claude.ai/code/session_01CbMTnT8Upx6Ym18i5H11iR