Skip to content

v0.3.6 — replay a large request body on failover

Choose a tag to compare

@oliveagle oliveagle released this 07 Nov 15:19
· 142 commits to main since this release

What's in v0.3.6

Patch release on top of v0.3.5. No wire-format or configuration changes —
one gateway bug fix, one routing refinement, and a test-harness fix.

A large request body can now fail over (the multi-minute silent turn)

Pingora keeps at most 64 KiB of the client body for a replay and silently drops
the rest. A real agent request is much larger — codex sends ~100 KB of
instructions and tool schemas — so any failover after the body had been
consumed handed the next upstream a truncated body. The upstream waited for the
rest, the client waited for a response, and neither side ever moved:

1794062827116  status 0  dur=391337ms  coding_home_olefedora_gpu0  /v1/responses

Pingora only hands the body to request_body_filter when it has a retry buffer
or the body is empty (if buffer.is_some() || session.is_body_empty()), and
get_retry_buffer() returns None exactly when the buffer truncated — so for a
large body the filter was never called at all.

pingora-proxy 0.8.0 (and 0.9.1) still has that condition, so it is vendored in
vendor/pingora-proxy with one added term: when the downstream body is already
complete, the filter is driven once with an empty final chunk. The gateway keeps
the client body itself and re-feeds it whole, re-translated for the provider
that now answers (whose model name differs). See vendor/README.md and
vendor/pingora-proxy-maxllm.patch.

Measured after the fix:

94 KB body, plain route          200 in 0.82 s   (was: hang, 391 s)
94 KB body, forced failover      200 in 2.94 s   (403 primary → 200 fallback)

A body-level auto sentinel is rewritten in place

On a route without auto_model the sentinel only exists in the body
({"model": "auto"}), and the provider has to be picked before the body is
read. When the picked provider is itself a pool member — a pinned session, or a
route whose own provider is listed in the pool — the body is now rewritten to
that entry's model instead of forwarding the literal sentinel. Previously the
upstream rejected it and the request failed over to a different member on
every turn, throwing away the upstream prompt cache for the conversation; and a
member that does not validate model names (an Ollama / llama.cpp style server)
would silently serve whatever model it happened to host.

Tests

  • the integration harness now claims gateway ports inside the test binary and
    fails loudly when a gateway dies during startup — portpicker handed the
    same port to two parallel tests, so a test could read a 200 from another
    test's gateway (random failures in whichever test lost the race);
  • the unclassifiable-429 test pins an unreachable laya endpoint instead of
    silently using the developer's own decision server on port 18405.

cargo test --workspace green (5 consecutive integration runs),
cargo clippy --workspace --all-targets -- -D warnings clean.

Upgrading

No config change. Replace the binary and restart:

sha256sum maxllm-server-linux-x86_64-musl
# bc09f0c52fc59805c6a6cef5df33638b5f396f95ede3aec43e10893ccfb775a8
systemctl --user restart maxllm