Skip to content

Commit 64356d0

Browse files
fix: request exact token ids and fail loud in FireworksV1CompletionsClient
The /v1/completions sampling path corrupted RL training via retokenization drift. The request never set return_token_ids, so choices[].token_ids came back empty and the client silently re-encoded decoded text. Retokenization drops the trailing end-of-turn/EOS token, making completion_ids one shorter than token_logprobs and misaligning every per-token logprob (inference KLD spiked to ~60 vs ~0.028 with exact ids). - Default return_token_ids=True in the request payload (overridable via request_params). - Remove the tokenizer.encode() re-encode fallback; raise when exact ids are absent instead of silently returning corrupted data. - Assert len(completion_token_ids) == len(completion_logprobs) at the boundary to catch any residual drift. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 08e9b85 commit 64356d0

2 files changed

Lines changed: 165 additions & 3 deletions

File tree

eval_protocol/integrations/fireworks_v1_completions_client.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ async def create_completion_from_prompt_ids(
358358
}
359359
if not self.logprobs:
360360
request_payload.pop("logprobs", None)
361+
# Always request the exact generated token ids so downstream RL training
362+
# can align per-token logprobs to token ids position-by-position.
363+
# Re-encoding decoded text drops the trailing end-of-turn/EOS token and
364+
# misaligns logprobs, corrupting inference KLD. Keep overridable via
365+
# request_params, but default the flag on.
366+
request_payload.setdefault("return_token_ids", True)
361367

362368
max_retries = 40
363369
base_delay = 10.0
@@ -396,16 +402,22 @@ async def create_completion_from_prompt_ids(
396402
completion_token_ids = _normalize_token_id_sequence(
397403
choice.get("token_ids") or raw_output.get("completion_token_ids") or []
398404
)
405+
if not completion_token_ids:
406+
raise RuntimeError(
407+
"Fireworks /v1/completions returned no exact completion token IDs "
408+
"(choices[].token_ids and raw_output.completion_token_ids both empty) "
409+
"even though return_token_ids=True was requested. "
410+
"Refusing to re-encode decoded text: retokenization drops the "
411+
"end-of-turn token and misaligns per-token logprobs, corrupting "
412+
f"inference KLD. choice keys={list(choice.keys())}"
413+
)
399414
choice_prompt_token_ids = _normalize_token_id_sequence(
400415
choice.get("prompt_token_ids") or raw_output.get("prompt_token_ids") or normalized_prompt_token_ids
401416
)
402417

403418
completion_text = self.decode_token_ids(token_ids=completion_token_ids)
404419
if not completion_text:
405420
completion_text = str(choice.get("text") or "")
406-
if not completion_token_ids and completion_text:
407-
tokenizer = self._get_tokenizer()
408-
completion_token_ids = list(tokenizer.encode(completion_text, add_special_tokens=False))
409421

410422
# -- Extract logprobs -----------------------------------------------
411423
completion_logprobs: List[float] = []
@@ -423,6 +435,16 @@ async def create_completion_from_prompt_ids(
423435
elif isinstance(choice_logprobs, list):
424436
completion_logprobs = [float(lp) if lp is not None else 0.0 for lp in choice_logprobs]
425437

438+
# Catch any residual token-id / logprob drift at the boundary rather than
439+
# as a downstream KLD anomaly.
440+
if completion_logprobs and len(completion_token_ids) != len(completion_logprobs):
441+
raise RuntimeError(
442+
"Fireworks /v1/completions returned mismatched completion token "
443+
f"ids ({len(completion_token_ids)}) and logprobs "
444+
f"({len(completion_logprobs)}). Per-token logprobs cannot be "
445+
"aligned to token ids; refusing to return corrupted data."
446+
)
447+
426448
# -- Build message via parser or raw --------------------------------
427449
if self.tool_call_parser is not None:
428450
parsed_output = self.tool_call_parser(completion_text, completion_token_ids, active_tools)

tests/test_fireworks_v1_completions_client.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,146 @@ def encode(self, text, add_special_tokens=False):
124124
asyncio.run(client.close())
125125

126126

127+
class _FakeResponse:
128+
def __init__(self, payload: Dict[str, Any]):
129+
self._payload = payload
130+
131+
def model_dump(self) -> Dict[str, Any]:
132+
return self._payload
133+
134+
135+
def _install_fake_completion(client, monkeypatch, payload):
136+
captured: Dict[str, Any] = {}
137+
138+
async def fake_create(**kwargs):
139+
captured.update(kwargs)
140+
return _FakeResponse(payload)
141+
142+
class _FakeCompletions:
143+
create = staticmethod(fake_create)
144+
145+
class _FakeClient:
146+
completions = _FakeCompletions()
147+
148+
async def close(self):
149+
return None
150+
151+
monkeypatch.setattr(client, "_client", _FakeClient())
152+
return captured
153+
154+
155+
def test_request_payload_sets_return_token_ids(monkeypatch):
156+
client = FireworksV1CompletionsClient(
157+
model_id="test-model",
158+
tokenizer_name_or_path="Qwen/Qwen3-0.6B",
159+
)
160+
monkeypatch.setattr(client, "decode_token_ids", lambda token_ids: "hello")
161+
captured = _install_fake_completion(
162+
client,
163+
monkeypatch,
164+
{
165+
"choices": [
166+
{
167+
"token_ids": [5, 6, 7],
168+
"finish_reason": "stop",
169+
"logprobs": {"token_logprobs": [-0.1, -0.2, -0.3]},
170+
}
171+
],
172+
},
173+
)
174+
result = asyncio.run(client.create_completion_from_prompt_ids(prompt_token_ids=[1, 2]))
175+
assert captured["return_token_ids"] is True
176+
assert "raw_output" not in captured
177+
assert result["completion_ids"] == [5, 6, 7]
178+
assert len(result["completion_ids"]) == len(result["completion_logprobs"])
179+
asyncio.run(client.close())
180+
181+
182+
def test_request_params_can_override_flags(monkeypatch):
183+
client = FireworksV1CompletionsClient(
184+
model_id="test-model",
185+
tokenizer_name_or_path="Qwen/Qwen3-0.6B",
186+
request_params={"return_token_ids": False},
187+
)
188+
monkeypatch.setattr(client, "decode_token_ids", lambda token_ids: "hi")
189+
captured = _install_fake_completion(
190+
client,
191+
monkeypatch,
192+
{"choices": [{"token_ids": [9], "finish_reason": "stop"}]},
193+
)
194+
asyncio.run(client.create_completion_from_prompt_ids(prompt_token_ids=[1]))
195+
assert captured["return_token_ids"] is False
196+
asyncio.run(client.close())
197+
198+
199+
def test_uses_exact_token_ids_without_reencode(monkeypatch):
200+
client = FireworksV1CompletionsClient(
201+
model_id="test-model",
202+
tokenizer_name_or_path="Qwen/Qwen3-0.6B",
203+
)
204+
monkeypatch.setattr(client, "decode_token_ids", lambda token_ids: "text")
205+
206+
def _fail_encode():
207+
raise AssertionError("tokenizer must not be used to re-encode completion text")
208+
209+
monkeypatch.setattr(client, "_get_tokenizer", lambda: _fail_encode())
210+
_install_fake_completion(
211+
client,
212+
monkeypatch,
213+
{
214+
"choices": [
215+
{
216+
"token_ids": [10, 20, 30, 40],
217+
"finish_reason": "stop",
218+
"logprobs": {"token_logprobs": [-0.1, -0.2, -0.3, -0.4]},
219+
}
220+
],
221+
},
222+
)
223+
result = asyncio.run(client.create_completion_from_prompt_ids(prompt_token_ids=[1]))
224+
assert result["completion_ids"] == [10, 20, 30, 40]
225+
asyncio.run(client.close())
226+
227+
228+
def test_raises_when_no_exact_token_ids(monkeypatch):
229+
client = FireworksV1CompletionsClient(
230+
model_id="test-model",
231+
tokenizer_name_or_path="Qwen/Qwen3-0.6B",
232+
)
233+
_install_fake_completion(
234+
client,
235+
monkeypatch,
236+
{"choices": [{"text": "hello world", "finish_reason": "stop"}]},
237+
)
238+
with pytest.raises(RuntimeError, match="no exact completion token IDs"):
239+
asyncio.run(client.create_completion_from_prompt_ids(prompt_token_ids=[1, 2]))
240+
asyncio.run(client.close())
241+
242+
243+
def test_raises_on_id_logprob_length_mismatch(monkeypatch):
244+
client = FireworksV1CompletionsClient(
245+
model_id="test-model",
246+
tokenizer_name_or_path="Qwen/Qwen3-0.6B",
247+
)
248+
monkeypatch.setattr(client, "decode_token_ids", lambda token_ids: "text")
249+
_install_fake_completion(
250+
client,
251+
monkeypatch,
252+
{
253+
"choices": [
254+
{
255+
"token_ids": [1, 2, 3],
256+
"finish_reason": "stop",
257+
"logprobs": {"token_logprobs": [-0.1, -0.2, -0.3, -0.4]},
258+
}
259+
],
260+
},
261+
)
262+
with pytest.raises(RuntimeError, match="mismatched completion token"):
263+
asyncio.run(client.create_completion_from_prompt_ids(prompt_token_ids=[1]))
264+
asyncio.run(client.close())
265+
266+
127267
def test_thinking_kwargs_respects_enable_thinking():
128268
client_none = FireworksV1CompletionsClient(
129269
model_id="test", tokenizer_name_or_path="Qwen/Qwen3-0.6B",

0 commit comments

Comments
 (0)