4.2 KiB
v2.3 task 01: control-plane requests
Branch: v2.3 (git switch -c v2.3 master if it does not exist, else git switch v2.3; git status --short must be empty, otherwise stop)
Commit subject: Control-plane requests follow the lease but take no slot and write no row
Goal
A client that manages its own llama-server slot makes small calls beside its chat stream: it
polls GET /slots?model=X while it waits, reads GET /props?model=X, tokenizes, and sends
POST /v1/chat/completions/control on a second connection while its own stream holds a slot.
Today /slots and /tokenize are 404, a GET is leased under the route's default model instead
of ?model=, and every call takes a limiter slot — so /control can queue behind its own
stream, or get 503 when the queue is full. After this task those calls follow the lease like any
request but never wait for or take a slot, skip the context guard, and write no accounting row.
Files
- Copy:
internal/proxy/control_test.go, and the replacementinternal/proxy/proxy_test.go(overwrites the v1 copy: the/r/slots → 404row becomes/r/slots/0and/r/metrics; the v2.3 copy is now the protected one) - Create:
internal/proxy/control.go— the control-call test and the model-from-query rule live here (proxy.gois at 325 lines) - Modify:
internal/proxy/proxy.go,internal/proxy/forward.go,docs/implementer-log.md
Rules
- Model. The body's top-level
"model"wins; else the query parametermodel(r.URL.Query().Get("model")); else the route'sdefault_model. This is used for the lease key and the limiter pair, exactly where the body model is used today. - Paths.
allowedPathalso admitsrest == "/slots"andrest == "/tokenize"(exact match on the path; the query string is not part ofrest)./slots/0,/slots/0?action=…and anything else stay404 {"error":"not found"}. - Control calls are: method
GETorHEAD(any allowed path), or methodPOSTwithrestexactly/tokenizeor/v1/chat/completions/control. Everything else — in particularPOST /v1/chat/completions— is not a control call. - A control call is routed and leased exactly as today (same
lease.Acquire, same wake path when no host is healthy), then forwarded withoutlim.Acquire, without the context guard, and without an accounting row (writeRecordis not called for it). Its log line isDebug, notInfo(a client polls/slotsevery 5 s). It still gets theX-Crossbar-Host/X-Crossbar-Leaseheaders and still marks a host down on a transport error, like any forward. - Do not duplicate
forward. Pass what it needs to know (for example acontrol bool, or a small options struct if the parameter list gets long) and skip the row and theInfolog inside it.
Facts you need
peekModelalready reads and restores the body;GET/HEADreturn""there. The query fallback goes after it, in one place.- The rig in
helpers_test.gopasses the store as the recorder, and the row is written after the answer is sent — the tests wait for rows; do not add sleeps to production code. waitUntilis defined inproxy_test.go;conversation(id, turn)builds a chat body.
Steps
- 1. Branch as above; copy the two given files.
- 2. See them fail:
go test -count=1 ./internal/proxy/ -run 'TestGetModel|TestControl|TestChatIsNotControl'→ 404 on/slotsand/tokenize, 503queue fullon control calls, 4 stray rows. - 3.
control.go: the control-call test and the model rule. 4.proxy.go: use them; branch inserveLeased(no limiter, no guard for a control call). 5.forward.go: no row,Debuglog for a control call. - 6.
gofmt -w;go test -race -count=3 ./internal/proxy/→ok. - 7.
make gate;make smoke. 8. Rowv2.3/01-control-plane; commit.
git add internal/proxy docs/implementer-log.md
git commit
Done when
- The new tests and every earlier proxy test pass under
-race -count=3; gate and smoke ok; given files byte-identical; no file over 400 lines.
Stop and report if
- Passing needs a change to any given test, or
forwardcannot skip the row without copying it.