feat(server): expose POST /v1/tokenize endpoint for client-side token counting - #477
rafaeldrincon wants to merge 1 commit into
Conversation
… counting
OpenAI-compatible clients currently have no way to pre-validate prompt
size against max_model_len: an oversized prompt is only rejected at
generation time (dropped with 'Input sequence length exceeds max_seq_len').
POST /v1/tokenize accepts both shapes:
- {"input": "text"} — raw encode via the frontend tokenizer, no chat
template (llama.cpp /tokenize-compatible)
- {"messages": [...]} — rendered through the SAME chat template a real
generation would use (via count_prompt_tokens), so the count matches
the usage.prompt_tokens a real request would report
Response: {"tokens": int, "token_ids": list[int]}.
The endpoint reuses the existing frontend-side TokenizeManager
(state.frontend_tokenizer()), the same instance /v1/messages/count_tokens
uses — a second read-only tokenizer process kept off the generation path.
Closes FlashML-org#473
|
Tested on an H100 with Qwen3.5-2B ( This adds a public endpoint, so the shape and the semantics need to be settled before merge. What diverges today
Target designA single {"prompt": "text", "add_special_tokens": true}
{"messages": [...], "tools": [...], "tool_choice": "auto", "chat_template_kwargs": {}, "reasoning_effort": "high", "thinking": {...}}One response, identical for both paths: {"tokens": [9707, 1879], "count": 2, "max_model_len": 32768}
|
Problem
OpenAI-compatible clients have no way to pre-validate prompt size against
max_model_len. An oversized prompt is only rejected at generation time (dropped withInput sequence length exceeds max_seq_len) — the client learns nothing until the request fails.llama.cpp exposes
POST /tokenizefor exactly this; clients (e.g. Understory, llama-swap) use it to count tokens and chunk prompts before submission. FreeToken already has the machinery (TokenizeManager,count_prompt_tokensfor/v1/messages/count_tokens) but no OpenAI-path endpoint.Solution
POST /v1/tokenizeaccepting both shapes:{"input": "text"}— raw encode via the frontend tokenizer, no chat template (llama.cpp-compatible){"messages": [...]}— rendered through the SAME chat template a real generation would use (viacount_prompt_tokens), so the count matchesusage.prompt_tokensof a real requestResponse:
{"tokens": int, "token_ids": list[int]}Reuses the existing frontend-side
TokenizeManager(state.frontend_tokenizer()) — the same read-only instance/v1/messages/count_tokensuses, kept off the generation path.Tests
tests/server/test_tokenize_endpoint.py— 3 tests, all passing on real hardware (RTX 3080 Ti, Python 3.13):Closes #473
Test environment