Skip to content

Commit d6227f3

Browse files
committed
Fix interception proxy capturing 0 token ids on current vLLM
vLLM with --return-tokens-as-token-ids encodes the token id in the logprobs "token" field as "token_id:<int>" rather than a separate "token_id" key, so the capture read zero token ids. Every env that trains on the captured trace (opencode, pi, ...) then loses the token ids GRPO needs. Parse the id from the token field. Adds a unit test.
1 parent 5298e0d commit d6227f3

2 files changed

Lines changed: 65 additions & 15 deletions

File tree

envs/opencode_env/sandbox/interception.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@ async def chat_completions(request: Request) -> Response:
130130
try:
131131
body = json.loads(raw_body)
132132
except json.JSONDecodeError:
133-
return JSONResponse(
134-
status_code=400, content={"error": "invalid json body"}
135-
)
133+
return JSONResponse(status_code=400, content={"error": "invalid json body"})
136134

137135
forwarded_body = _prepare_forwarded_body(body, cfg)
138136
headers = {
@@ -338,7 +336,7 @@ async def _stream() -> Any:
338336
yield line + "\n"
339337
if not line.startswith("data:"):
340338
continue
341-
data = line[len("data:"):].strip()
339+
data = line[len("data:") :].strip()
342340
if data == "[DONE]":
343341
continue
344342
try:
@@ -381,7 +379,11 @@ def _accumulate_stream_chunk(chunk: dict[str, Any], acc: dict[str, Any]) -> None
381379
tc_idx = tc.get("index", 0)
382380
bucket = acc["tool_calls_by_idx"].setdefault(
383381
(idx, tc_idx),
384-
{"id": None, "type": "function", "function": {"name": "", "arguments": ""}},
382+
{
383+
"id": None,
384+
"type": "function",
385+
"function": {"name": "", "arguments": ""},
386+
},
385387
)
386388
if tc.get("id"):
387389
bucket["id"] = tc["id"]
@@ -458,11 +460,11 @@ def _build_turn_record(
458460
token_ids: list[int] = []
459461
per_token_logps: list[float] = []
460462
for entry in content_lp:
461-
tokens.append(entry.get("token", ""))
462-
# OpenAI returns no raw token ids; vLLM returns them as ``token_id``.
463-
token_id = entry.get("token_id")
464-
if token_id is not None:
465-
token_ids.append(int(token_id))
463+
token = entry.get("token", "")
464+
tokens.append(token)
465+
# vLLM (--return-tokens-as-token-ids) returns the id in the token field as "token_id:<int>".
466+
if token.startswith("token_id:"):
467+
token_ids.append(int(token.split(":", 1)[1]))
466468
lp = entry.get("logprob")
467469
if lp is not None:
468470
per_token_logps.append(float(lp))
@@ -487,8 +489,7 @@ def _strip_logprobs(response_json: dict[str, Any]) -> dict[str, Any]:
487489
choices = out.get("choices")
488490
if isinstance(choices, list):
489491
out["choices"] = [
490-
{k: v for k, v in (ch or {}).items() if k != "logprobs"}
491-
for ch in choices
492+
{k: v for k, v in (ch or {}).items() if k != "logprobs"} for ch in choices
492493
]
493494
return out
494495

@@ -537,9 +538,7 @@ def start(self) -> None:
537538
lifespan="on",
538539
)
539540
self._server = uvicorn.Server(config)
540-
self._thread = threading.Thread(
541-
target=self._run_server, daemon=True
542-
)
541+
self._thread = threading.Thread(target=self._run_server, daemon=True)
543542
self._thread.start()
544543
# Wait for the server to accept connections.
545544
deadline = time.time() + 10
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""The interception proxy must capture per-token ids from a vLLM logprobs response.
8+
9+
vLLM with ``--return-tokens-as-token-ids`` encodes the id in the ``token`` field as
10+
``"token_id:<int>"`` (no separate ``token_id`` key), which the capture must parse so
11+
GRPO trains on real token ids."""
12+
13+
import os
14+
import sys
15+
16+
# Make ``envs/`` importable when running from the repository root.
17+
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
18+
if _REPO_ROOT not in sys.path:
19+
sys.path.insert(0, _REPO_ROOT)
20+
_ENVS_DIR = os.path.join(_REPO_ROOT, "envs")
21+
if _ENVS_DIR not in sys.path:
22+
sys.path.insert(0, _ENVS_DIR)
23+
24+
from opencode_env.sandbox.interception import _build_turn_record
25+
26+
27+
def _response(content):
28+
return {"choices": [{"finish_reason": "stop", "logprobs": {"content": content}}]}
29+
30+
31+
def test_captures_ids_from_vllm_token_id_prefix():
32+
# vLLM --return-tokens-as-token-ids: id lives in the ``token`` field as "token_id:N".
33+
content = [
34+
{"token": "token_id:15339", "logprob": -0.1},
35+
{"token": "token_id:1917", "logprob": -0.2},
36+
]
37+
rec = _build_turn_record(
38+
turn_idx=0, request_body={}, response_json=_response(content), latency_s=0.0
39+
)
40+
assert rec.completion_token_ids == [15339, 1917]
41+
assert rec.per_token_logps == [-0.1, -0.2]
42+
43+
44+
def test_plain_openai_has_logprobs_but_no_ids():
45+
# A plain OpenAI response (no ids) still yields logprobs, just no token ids.
46+
content = [{"token": "hi", "logprob": -0.3}]
47+
rec = _build_turn_record(
48+
turn_idx=0, request_body={}, response_json=_response(content), latency_s=0.0
49+
)
50+
assert rec.completion_token_ids == []
51+
assert rec.per_token_logps == [-0.3]

0 commit comments

Comments
 (0)