feat(sglang): tokens in/out with logprobs - #8119
Merged
Merged
Conversation
…from #7699 Cherry-picks the following from jthomson04/tokenize-endpoint: - POST /v1/tokenize and /v1/detokenize HTTP endpoints - Tokenizer trait: encode_with_special_tokens(), convert_ids_to_tokens() - return_tokens_as_token_ids parameter for chat completions - Multi-instance tokenize fix (discovery watcher) - Jail logprobs preservation through tool-call jailing
…robs Add return_tokens_as_token_ids support to the SGLang decode handler, mirroring what PR #7699 added for vLLM. When enabled, logprob token fields are returned as "token_id:<id>" instead of decoded text. Changes: - decode_handler.py: Read return_tokens_as_token_ids from output_options, pass through _process_token_stream to _extract_logprobs, format token strings accordingly - sglang_processor.py: Forward return_tokens_as_token_ids through _build_dynamo_preproc output_options - vllm/handlers.py: Remove debug print left in cherry-picked code
Add the return_tokens_as_token_ids field to NvCreateCompletionRequest and implement get_return_tokens_as_token_ids() so the completions endpoint has parity with chat completions for token-based logprobs.
The /v1/completions path was building the logprob tokens list from the decoded token strings, ignoring return_tokens_as_token_ids. Plumb the flag through DeltaGeneratorOptions and emit "token_id:<id>" strings in the tokens field when set, mirroring what chat_completions/delta.rs already does for its selected-token field. Fixes 100% fallback_tokenize overhead on clients that rely on the "token_id:N" format to skip client-side retokenization.
SGLang's tokenizer manager detokenizes top-k tokens per-position serially, causing O(N) latency per generated token. Silently dropping the top_logprobs feature is worse than surfacing the limitation, so raise a clear ValueError when callers request logprobs>=1 (or prompt_logprobs>=1) and pin top_logprobs_num=0 as a belt-and-suspenders guard. Escape hatch: DYN_SGL_ALLOW_TOP_LOGPROBS=1 restores the previous passthrough for use once upstream batches detokenize_top_logprobs_tokens. Update CLAUDE.md to document the gate.
Contributor
AndyDai-nv
reviewed
May 8, 2026
AndyDai-nv
left a comment
Contributor
There was a problem hiding this comment.
LGTM — extending /v1/completions with return_tokens_as_token_ids is the cleanest path for downstream RL frameworks (Miles could directly drop its fallback_tokenize=True re-tokenization on this).
One minor and non-blocking thing, marking as Comment since I haven't run it locally yet
ishandhanani
approved these changes
May 11, 2026
Aphoh
enabled auto-merge (squash)
May 11, 2026 21:56
3 tasks
krishung5
added a commit
that referenced
this pull request
May 11, 2026
Merge with main pulls in #9058's `choice.stop_reason = None;` against #8119's `ChatChoiceStream` (which no longer has that field). Path- filtered CI on main's next commit (#9230, sglang-only) skipped rust- clippy so the broken combination landed silently — exposed here by the merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
krishung5
added a commit
that referenced
this pull request
May 11, 2026
Merge with main pulls in #9058's `choice.stop_reason = None;` against #8119's `ChatChoiceStream` (which no longer has that field). Path- filtered CI on main's next commit (#9230, sglang-only) skipped rust- clippy so the broken combination landed silently — exposed here by the merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
krishung5
added a commit
that referenced
this pull request
May 11, 2026
…arsing_stream Follow-up to dropping the orphaned `choice.stop_reason = None;` in `preprocessor.rs`: the test helper `mock_multi_choice_content_chunk` still constructs `ChatChoiceStream` with a `stop_reason: None` initializer, but #8119 removed that field. Other test files (tool_choice.rs, test_streaming_usage.rs, …) construct `BackendOutput` which still has `stop_reason` — those are left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
krishung5
added a commit
that referenced
this pull request
May 11, 2026
…arsing_stream Follow-up to dropping the orphaned `choice.stop_reason = None;` in `preprocessor.rs`: the test helper `mock_multi_choice_content_chunk` still constructs `ChatChoiceStream` with a `stop_reason: None` initializer, but #8119 removed that field. Other test files (tool_choice.rs, test_streaming_usage.rs, …) construct `BackendOutput` which still has `stop_reason` — those are left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
grahamking
added a commit
that referenced
this pull request
May 12, 2026
grahamking
added a commit
that referenced
this pull request
May 12, 2026
grahamking
added a commit
that referenced
this pull request
May 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Keeps the logprob/token-id pieces needed by RL workflows while preserving OpenAI-compatible response shapes.
This PR:
return_tokens_as_token_idsfor OpenAI chat/completions logprob responsesstop: [576]/v1/completionschosen-token logprobs whenlogprobs=0DYN_SGL_ALLOW_TOP_LOGPROBS=1stop_reasonas opt-innvext.stop_reasonmetadata instead of adding non-OpenAI fields tochoices[]What's included
return_tokens_as_token_idsrequest/output option plumbing in the Rust OpenAI protocol layer.token_id:<id>strings for logprob token fields when requested./v1/completionstreatslogprobs=0as chosen-token logprobs only.logprobs >= 1/prompt_logprobs >= 1by default to avoid upstream per-position top-token detokenization cost. SetDYN_SGL_ALLOW_TOP_LOGPROBS=1to opt in while tracking the upstream fix in Batch detokenization across positions in detokenize_top_logprobs_tokens sgl-project/sglang#24447.finish_reason.matchedvalues are carried as Dynamo stop reasons for strings, token IDs, and token-ID arrays.nvext.stop_reason, so the response only reports user-provided stop triggers.stop: [576]is accepted as a Dynamo token-id stop extension and is threaded through internalStopConditions/ SGLang sampling params.stop_reasonis returned only when requested withnvext.extra_fields: ["stop_reason"], and is emitted at response-levelnvext.stop_reason.docs/components/frontend/nvext.mddocumentsstop_reasonand the distinction between token-id logprob display and token-id stop input.What's not included
/v1/tokenizeendpoint./v1/detokenizeendpoint.stop_reasonresponse shape forn > 1; Dynamo currently serves this as a response-level field for single-choice requests, andn > 1will need an indexed/per-choicenvextshape."token_id:576"as token-id stop input; that string remains a literal string stop sequence.Stop contract
Dynamo keeps the public response shape aligned with the OpenAI completions API while accepting token-id stops as a Dynamo extension:
stop: "..."stops on that string.stop: ["A", "B"]stops on either string.stop: [32, 34]stops on either token ID.stop: "token_id:576"andstop: ["token_id:576"]are treated as literal string stops, not token-id shorthand.stop: 576is rejected; scalar numeric stops are not accepted.choices[].finish_reasonremains the OpenAI-compatible value, for example"stop".nvext.extra_fields: ["stop_reason"], Dynamo returns the backend matched stop reason at response-levelnvext.stop_reason: string stops return strings, and token-id stops return numbers.choices[].stop_reasonis intentionally omitted because it is not part of the normal OpenAI completions API. Dynamo currently servesnvext.stop_reasonas a response-level single-choice field;n > 1will require an indexed/per-choicenvextshape.API notes
When
return_tokens_as_token_ids: true, logprob token fields usetoken_id:<id>instead of decoded text:{ "logprobs": 0, "return_tokens_as_token_ids": true }Example emitted token field:
{"token": "token_id:12345", "logprob": -0.5}That display format is output-only. To stop on a token ID, use an integer stop array:
{ "prompt": [1, 2, 3], "stop": [32, 34], "nvext": { "extra_fields": ["stop_reason"] } }Do not use
"stop": ["token_id:576"]for token-id stops; Dynamo treats it as a literal string stop sequence.Example response shape for a matched string or token-id stop:
{ "choices": [ { "finish_reason": "stop" } ], "nvext": { "stop_reason": 576 } }choices[].stop_reasonis intentionally omitted because it is not part of the normal OpenAI completions API.Validated against the temporary Dynamo
/v1/completionsstack:stop: [" The"]where" The"is decoded token ID576finish_reason="stop", response-levelnvext.stop_reason=" The"stop: " The"finish_reason="stop", response-levelnvext.stop_reason=" The"stop: ["token_id:576"]stop: "token_id:576"stop: [32, 34]stop: 576stop_token_ids: [576]nvext: {"stop_token_ids": [576]}Test plan
cargo fmt --all -- --checkgit diff --checkpython3 -m py_compile components/src/dynamo/frontend/sglang_processor.py components/src/dynamo/sglang/protocol.pypython3 -m py_compile components/src/dynamo/frontend/tests/test_sglang_processor_unit.py components/src/dynamo/sglang/tests/test_sglang_decode_handler.pycargo test -p dynamo-protocols stopcargo test -p dynamo-llm stop_contract --no-default-featurescargo test -p dynamo-llm test_stop --no-default-featurescargo test -p dynamo-llm user_stop_token_reports_distinct_trigger --no-default-featurescargo test -p dynamo-llm stop_reason --no-default-featuresPYTHONPATH=components/src pytest components/src/dynamo/frontend/tests/test_sglang_processor_unit.py -k "stop_token_id_array_maps_to_stop_token_ids or string_stops_remain_string_stops or token_id_display_string_remains_string_stop"locally, blocked by missingsglangpackage in this environmentPYTHONPATH=components/src pytest components/src/dynamo/sglang/tests/test_sglang_decode_handler.py -k "user_stop_token_ids or openai_stop_sampling_params"locally, blocked by missingsglangpackage in this environment