feat: add public POST /tokenize endpoint #43
No reviewers
Labels
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
brooktrails/gllm!43
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/request-too-large-4xx-and-metrics"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Also fix a scheduler error handling bug and add size metrics.
Requests larger than a hard capacity limit were surfaced as HTTP 500. They are client-caused and unretryable as-is, so map them to 400. - Add scheduler.ErrRequestTooLarge, a shared sentinel wrapping both the per-batch-budget and whole-KV-cache rejections. writeEngineError maps it to 400 before the ERROR log, so oversized input no longer logs as an internal failure. - The rejection message now reports the tokenized size, the limit, and a proportional hint (shorten by ~N%), since clients send untokenized text and cannot predict the token count. - Add gllm_requests_too_large_total{reason=batch|kv_cache}, a lagging counter split by the binding constraint (kv_cache signals cache under-provisioning). Both label values pre-registered at 0. - Add gllm_request_prompt_tokens, a histogram of accepted-request prompt sizes observed once per completed request -- the leading indicator: a rising high percentile shows headroom shrinking before rejections begin. - Add kvcache.Manager.TokenCapacity() for the whole-cache limit in the message and metric. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>A vLLM-compatible tokenize-only endpoint so a client can size a request against the batch budget before sending it. Complements the 4xx hint on oversized requests: the server is the only authority on the token count (it applies the model's chat template and special tokens), so a client cannot reliably reproduce it locally. - POST /tokenize takes exactly one of {prompt, messages} and returns {count, tokens}. messages applies the chat template as /v1/chat/completions does; prompt encodes raw text as /v1/completions does. Runs no forward pass -- pure CPU, off the scheduler/KV path. - Engine.Tokenize/TokenizeChat wrap the exact Encode/EncodeChat calls generation uses, so the count is identical to what prefill will see. - Consistency test asserts Tokenize(x) length equals a real generation's PromptTokens for the same input; server test covers routing and the exactly-one-of validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat: add public POST /tokenize endpointto WIP: feat: add public POST /tokenize endpointWIP: feat: add public POST /tokenize endpointto feat: add public POST /tokenize endpointAutomated review by pr-reviewer v0.36.0 | Full Review | Claude Sonnet 5 | tracking id
r-604409-a1ff98This is an AI-generated review and may contain mistakes.
Status: ✅ Completed
✅ Verdict: Approve — The PR is correct, well-tested for its primary behavior change (the /tokenize count matching generation, and the 4xx error mapping fix), and stylistically consistent; the only gap is missing direct tests for the two new metrics' wiring through the engine, which is minor enough not to block.
Overall this is a well-scoped, well-tested PR that does three related things: adds
POST /tokenize, fixes the scheduler's "too large" error handling so it maps to a proper client error (4xx) instead of an opaque internal error, and adds a couple of Prometheus metrics (gllm_requests_too_large_total,gllm_request_prompt_tokens) to observe that condition. Going through each:/tokenizeendpointEngine.Tokenize/TokenizeChatreuse the exact encode pathsGenerate/GenerateChatuse, andTestTokenizeMatchesGeneratepins that the count is identical to what a real generation's prefill sees -- that's the right thing to test given the endpoint's whole value proposition is "trust this count."handleTokenizevalidates exactly-one-of prompt/messages, and routes correctly.checkModelgates it like the other endpoints. Good, consistent style with the rest ofopenai.go/server.go.TokenizeResponse.Tokensalways returns the full token id array. For a large prompt (e.g. thousands of tokens) that's a nontrivial response payload just to get a count -- vLLM's/tokenizedoes returntokenstoo, so this matches precedent, but it's worth a beat of thought since this is explicitly a "check before sending the real thing" endpoint and could itself be heavy for huge prompts. Not a blocker, just a note; no request size cap here either, but that already exists at the scheduler level for the request proper.Scheduler error-handling fix
Schedule()produced plainfmt.Errorfwith no wrapped sentinel, so the server's genericwriteEngineErrormapped them to 500 Internal Server Error even though they're unambiguously client errors (oversized input, unrecoverable). The newErrRequestTooLargesentinel +errors.Ischeck inwriteEngineErrorfixes that cleanly, matching the existing pattern forbackend.ErrNotImplemented. This is a real bug fix and a sensible one.tooLargeErrcomputing a percentage hint is a nice touch, though the divisionover/n*100looks like it should probably beover/limitor something proportional to what to trim off -- but on reflection, dividing byn(the request's own token count) does directly answer "shorten the input by X%", which is exactly what's promised in the message. Let me double check there's no divide-by-zero:nis the tokenized sequence length, which for the batch-budget path isseq.Len()(prompt+output) and for the KV-cache path is alsoseq.Len(); both call sites only reachtooLargeErrwhenn > limit >= 0... actuallylimitcould be 0 in theory for the batch path ifMaxTokensPerBatchis 0, butnitself being 0 would mean an empty sequence, which wouldn't triggern > limitunless limit is negative.n == 0seems structurally unreachable givenLen()requires at least prompt tokens, and prompts are non-empty by construction upstream (tokenizer emits at least BOS). This is a minor latent risk but not one this PR introduces or worsens; it's consistent with prior behavior lacking any hint at all.scheduler_test.go) properly switch from raw substring matching toerrors.Is(seq.Err, ErrRequestTooLarge), which is more robust to message wording changes and directly enforces the sentinel-wrapping contract. Good.TokenCapacity()added tokvcache.Manageris a clean, minimal addition purely to support building a better error message; no side effects, trivially correct.Metrics additions
RequestsTooLarge(CounterVec by reason) andRequestPromptTokens(Histogram) are added tometrics.Metrics, pre-registering bothreasonlabel values up front (a nice touch per the existing style of "continuous series for alerting" seen elsewhere in the codebase, e.g. this mirrors how other counters are described in AGENTS.md).e.muinstep(), following the exact same pattern already used forPreemptions-- consistent design, no new locking risk.RequestPromptTokens.Observeis called once per successfully completed generate inengine.go'sgenerate()at the<-seq.Donebranch, which is the right place (mirrors the existing "request complete" log call site) and only observes prompt tokens for requests that actually ran to completion (as documented). This does mean prompt token counts for rejected (too-large) requests are not captured in the histogram -- that's fine and intentional per the doc comment ("accepted requests"), but worth flagging as a slight gap: an operator watching this histogram to anticipate rejections won't see the rejected request's own size reflected in the percentile (only the accepted ones near the edge). That's a reasonable design tradeoff given accepted-request sizing is the intended signal, not a bug.RequestsTooLargeincrements correctly end-to-end through the engine, e.g. verifying label values or the histogram observation). The PR's own summary flagsinternal/metrics/metrics.go,internal/engine/engine.go,internal/kvcache/kvcache.go,internal/scheduler/scheduler.go, andinternal/server/openai.goas lacking direct tests for these changes. Some of this is indirectly covered (scheduler_test.go checks the sentinel wraps; server_test.go'sTestErrorMappingchecks the 400 mapping), but there's no test asserting thegllm_requests_too_large_total{reason=...}counter or thegllm_request_prompt_tokenshistogram actually get bumped by a real end-to-end oversized-request or completed-request flow through the engine. GivenTestPerfStatsCountersalready demonstrates the pattern for existing counters, a similar test extending coverage to these two would be low-cost and valuable, especially since metric-plumbing bugs (wrong label, off-by-one in mirroring cumulative counts) are exactly the kind of thing that silently rot unnoticed. I'd call this a gap worth addressing, though not one that blocks merge given the underlying logic (scheduler counting, mirroring pattern) is proven correct by existing analogous tests.Other observations
server.go'sGeneratorinterface andfakeEnginetest double are updated in lockstep forTokenize/TokenizeChat, so any other real or fake implementer would fail to compile if forgotten -- good, no silent breakage risk here per Go's interface satisfaction.TokenizeRequest/TokenizeResponseare new types, and the scheduler error message text changed slightly (e.g., "max batch tokens" -> "the max batch size") but no code outside the test suite depended on exact wording (confirmed by grep of the diff -- only the test assertions changed correspondingly).Nothing here looks incorrect or unsafe; the fixes are genuine improvements (proper 4xx mapping, richer error messages, and useful new metrics), and the added test coverage for the primary contract (
/tokenizecount matches generation) is exactly the kind of test called for. The one real ask is a bit more direct test coverage of the new metrics wiring, which is a coverage gap rather than a functional problem.📚 Context used in this review
Project guidance files:
CLAUDE.mdAGENTS.md7b95dd3640c67c2949f7Automated review by pr-reviewer v0.36.0 | Safety Check | Claude Sonnet 5 | tracking id
r-604fc4-c5cf45This is an AI-generated review and may contain mistakes.
Status: ✅ Completed
✅ Verdict: LGTM — Additive feature (new /tokenize endpoint) plus a sensible scheduler error-handling fix and new metrics; no security issues, accidental commits, or breaking changes spotted.
Reviewed the diff:
/tokenizeendpoint: validates exactly-one-of prompt/messages, routes to Tokenize/TokenizeChat, returns count+tokens. Straightforward, tested.tooLargeErrhelper with a wrappedErrRequestTooLargesentinel; server maps it to 400 instead of 500. Tests updated to useerrors.Is. Looks correct.tooLargeErrcomputespct := ceil((n-limit)/n * 100)— division byn(tokenized count), guarded implicitly sincen>limit>= 0 in both call sites, and n can't be 0 when this path is hit (already exceeds a limit), so no realistic divide-by-zero.Nothing here looks like an accidental commit, security issue, or an obvious bug that would break existing behavior.