Clarify STOP authority on the native-token fast path without changing rendering
Maintainers usually reply within 3 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 54/100
- Issue type
- Documentation
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- python
- Domain
- ai, backend-api-design
Research direction
Read _stop_suffix and _tokenize_history in src/art/trajectories/_tokenize.py, then inspect the no-tokenizer test at tests/unit/trajectories/test_tokenize.py and the process path in _parallel.py. Document that native-only output with finish_reason="stop" does not prove a missing STOP token, while preserving rendering and the no-import contract; verify the cited reproduction and existing tests.
Written by the indexing model from the issue text.
Description
On main d734e25fca0c343ed06a5fa491a15736ff954cf3, complete native Chat Completions token IDs can be tokenized without a tokenizer, while STOP remains unidentified for a materialized response with finish_reason="stop" and no integer stop_reason. Supplying the correct EOS authority changes only the terminal STOP bit in the example below.
This is an authority/API gap, not evidence that marking with a known tokenizer is wrong. Native IDs and finish_reason="stop" alone do not identify which token, if any, belongs to the terminating sequence. In this mode an absent STOP bit must not be interpreted as proof that the token is not a stop.
Minimal public reproduction (no model or tokenizer download):
from datetime import UTC, datetime
from openai.types.chat import ChatCompletion
import art.trajectories as tr
class StopAuthority:
eos_token_id = 9
def __call__(self, text, **kwargs):
assert text == "END"
return [8, 9]
now = datetime(2026, 1, 1, tzinfo=UTC)
response = ChatCompletion.model_validate({
"id": "public-stop-example", "object": "chat.completion", "created": 0,
"model": "public/example", "choices": [{
"index": 0, "finish_reason": "stop", "stop_reason": None,
"message": {"role": "assistant", "content": "answer"},
"prompt_token_ids": [1], "token_ids": [2, 9],
"logprobs": {"content": [
{"token": f"token_id:{t}", "logprob": -0.5,
"bytes": [], "top_logprobs": []} for t in [2, 9]
]},
}],
})
trajectory = tr.Trajectory(exchanges=tr.TrajectoryExchanges(chat_completions=[
tr.ChatCompletionsExchange(
request={"model": "public/example", "messages": [
{"role": "user", "content": "question"}
]}, response=response, start_time=now, end_time=now,
)
]))
unknown = trajectory.tokenize()
known = trajectory.tokenize(tokenizer=StopAuthority())
assert unknown.tokens == known.tokens == [1, 2, 9]
assert [int(f) for f in unknown.flags] == [1, 23, 23]
assert [int(f) for f in known.flags] == [1, 23, 31]
The minimal authority object intentionally has no renderer. The checked fixture also verifies identical logprobs and SAMPLED first-occurrence masks. This does not establish equivalence for consumers that select STOP or for rendering with a full tokenizer.
Relevant current contracts:
_stop_suffixreturns zero without a tokenizer unless an exact non-bool integer stop reason proves the final token._sampled_stop_suffixalso retains the separate no-materialized-output case._tokenize_historypassestokenizer=Noneinto the exact native builder before resolving any renderer. This preserves the intentional no-import contract tested bytest_exact_tokens_form_one_append_only_history_without_tokenizer, including W&B artifact model names.- Passing a full tokenizer is not a general render-neutral workaround:
_history_needs_synthetic_stopand other renderer-selection predicates inspect it. - Parallel tokenization's process path explicitly supplies
tokenizer=None; explicit tokenizer objects currently select the thread path. A future separate authority must survive that boundary and remain bound to the correct model, rather than silently being omitted or shared across unrelated models.
A small safe next step is documenting the incomplete-STOP meaning of the native-only path. If callers need complete STOP flags without changing render selection, consider an explicit, per-model STOP authority distinct from the renderer tokenizer, carried through public/parallel APIs into suffix/marker checks only. Do not eagerly load metadata, guess EOS IDs, mark every final sampled token, or change synthetic-stop rendering to resolve this gap.
Validation: the real ART/Pydantic reproduction and eight existing tests passed on the pinned source (EOS/tool stop, string stop, explicit native integer stop, metadata-only output, native-only no-load, and length-stop rendering). Local execution bypassed only top-level ART package startup and disabled heavy imports/network; no model, provider, GPU, or frozen-run source was used or changed. No production patch accompanies this issue.
- Dominant language
- Python
- Stars
- 10.8k
- Forks
- 989
- Avg merge
- 11h 38m
- Merged PRs (30d)
- 104
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OpenPipe/ART
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 3 days
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
OpenPipe/ART#949 · 5 comments ·
Maintainers usually reply within 3 days
-
Difficulty 5/5 Over a week Newbie friendliness 10/100
Maintainers usually reply within 3 days
-
Difficulty 5/5 Over a week Newbie friendliness 42/100
Maintainers usually reply within 3 days
-
enhancement
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 3 days
Similar issues
-
needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
openvinotoolkit/openvino_notebooks#3665 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
docs
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
benchmark-gap
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day