Skip to content

Commit 3bdd9c1

Browse files
committed
Validate logprobs range and pin top-k logprobs round-trip
logprobs must be 0 to 20, but out of range values were only caught by the API. Reject them in the request validator, document the range in the create() docstrings, and add a test that top_logprobs survives model_dump() (#251, #443). Addresses #251.
1 parent cc9f253 commit 3bdd9c1

5 files changed

Lines changed: 129 additions & 4 deletions

File tree

src/together/resources/chat/completions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def create(
8383
seed (int, optional): A seed value to use for reproducibility.
8484
stream (bool, optional): Flag indicating whether to stream the generated completions.
8585
Defaults to False.
86-
logprobs (int, optional): Number of top-k logprobs to return
86+
logprobs (int, optional): Number of top tokens to return log probabilities for
87+
at each generation step, instead of only the sampled token.
88+
Must be in the range [0, 20].
8789
Defaults to None.
8890
echo (bool, optional): Echo prompt in output. Can be used with logprobs to return prompt logprobs.
8991
Defaults to None.
@@ -225,7 +227,9 @@ async def create(
225227
seed (int, optional): A seed value to use for reproducibility.
226228
stream (bool, optional): Flag indicating whether to stream the generated completions.
227229
Defaults to False.
228-
logprobs (int, optional): Number of top-k logprobs to return
230+
logprobs (int, optional): Number of top tokens to return log probabilities for
231+
at each generation step, instead of only the sampled token.
232+
Must be in the range [0, 20].
229233
Defaults to None.
230234
echo (bool, optional): Echo prompt in output. Can be used with logprobs to return prompt logprobs.
231235
Defaults to None.

src/together/resources/completions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def create(
7979
seed (int, optional): Seed value for reproducibility.
8080
stream (bool, optional): Flag indicating whether to stream the generated completions.
8181
Defaults to False.
82-
logprobs (int, optional): Number of top-k logprobs to return
82+
logprobs (int, optional): Number of top tokens to return log probabilities for
83+
at each generation step, instead of only the sampled token.
84+
Must be in the range [0, 20].
8385
Defaults to None.
8486
echo (bool, optional): Echo prompt in output. Can be used with logprobs to return prompt logprobs.
8587
Defaults to None.
@@ -203,7 +205,9 @@ async def create(
203205
seed (int, optional): Seed value for reproducibility.
204206
stream (bool, optional): Flag indicating whether to stream the generated completions.
205207
Defaults to False.
206-
logprobs (int, optional): Number of top-k logprobs to return
208+
logprobs (int, optional): Number of top tokens to return log probabilities for
209+
at each generation step, instead of only the sampled token.
210+
Must be in the range [0, 20].
207211
Defaults to None.
208212
echo (bool, optional): Echo prompt in output. Can be used with logprobs to return prompt logprobs.
209213
Defaults to None.

src/together/types/chat_completions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,20 @@ class ChatCompletionRequest(BaseModel):
150150
tool_choice: ToolChoice | ToolChoiceEnum | None = None
151151

152152
# Raise warning if repetition_penalty is used with presence_penalty or frequency_penalty
153+
# and raise an error if logprobs is outside the range supported by the API.
153154
@model_validator(mode="after")
154155
def verify_parameters(self) -> Self:
155156
if self.repetition_penalty:
156157
if self.presence_penalty or self.frequency_penalty:
157158
warnings.warn(
158159
"repetition_penalty is not advisable to be used alongside presence_penalty or frequency_penalty"
159160
)
161+
if self.logprobs is not None and not 0 <= self.logprobs <= 20:
162+
raise ValueError(
163+
f"logprobs must be an integer between 0 and 20, got {self.logprobs}. "
164+
"It sets the number of top tokens to return log probabilities for "
165+
"at each generation step, instead of only the sampled token."
166+
)
160167
return self
161168

162169

src/together/types/completions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@ class CompletionRequest(BaseModel):
4949
safety_model: str | None = None
5050

5151
# Raise warning if repetition_penalty is used with presence_penalty or frequency_penalty
52+
# and raise an error if logprobs is outside the range supported by the API.
5253
@model_validator(mode="after")
5354
def verify_parameters(self) -> Self:
5455
if self.repetition_penalty:
5556
if self.presence_penalty or self.frequency_penalty:
5657
warnings.warn(
5758
"repetition_penalty is not advisable to be used alongside presence_penalty or frequency_penalty"
5859
)
60+
if self.logprobs is not None and not 0 <= self.logprobs <= 20:
61+
raise ValueError(
62+
f"logprobs must be an integer between 0 and 20, got {self.logprobs}. "
63+
"It sets the number of top tokens to return log probabilities for "
64+
"at each generation step, instead of only the sampled token."
65+
)
5966
return self
6067

6168

tests/unit/test_logprobs.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Tests for the logprobs request parameter and top_logprobs response data.
2+
3+
The Together API accepts ``logprobs`` as an integer between 0 and 20: the
4+
number of top tokens to return log probabilities for at each generation
5+
step, instead of only the sampled token (see issue #251). When top-k
6+
logprobs are requested, each choice's ``logprobs`` part carries a
7+
``top_logprobs`` list with one ``{token: logprob}`` dict per generated
8+
token.
9+
"""
10+
11+
import warnings
12+
13+
import pytest
14+
from pydantic import ValidationError
15+
16+
from together.types import ChatCompletionRequest, CompletionRequest
17+
from together.types.chat_completions import ChatCompletionResponse
18+
19+
20+
MESSAGES = [{"role": "user", "content": "Say hello."}]
21+
MODEL = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
22+
23+
# Real response shape captured from the chat completions API with
24+
# ``logprobs=3`` (see issues #251 and #443): ``top_logprobs`` is a list
25+
# with one dict of the top-k alternatives per generated token.
26+
TOP_LOGPROBS = [
27+
{"Hello": -2.6e-06, "hello": -13.5, " Hello": -13.875},
28+
{".": -4.8e-05, "!": -10.0625, ".\n": -11.4375},
29+
]
30+
RESPONSE_PAYLOAD = {
31+
"id": "889ee12e7b0b3c67",
32+
"object": "chat.completion",
33+
"created": 1709240335,
34+
"model": MODEL,
35+
"choices": [
36+
{
37+
"index": 0,
38+
"finish_reason": "eos",
39+
"logprobs": {
40+
"tokens": ["Hello", "."],
41+
"token_logprobs": [-2.6e-06, -4.8e-05],
42+
"top_logprobs": TOP_LOGPROBS,
43+
},
44+
"message": {"role": "assistant", "content": "Hello."},
45+
}
46+
],
47+
"usage": {"prompt_tokens": 4, "completion_tokens": 2, "total_tokens": 6},
48+
}
49+
50+
51+
@pytest.mark.parametrize("logprobs", [-1, 21, 100])
52+
def test_chat_request_rejects_out_of_range_logprobs(logprobs: int) -> None:
53+
with pytest.raises(ValidationError, match="between 0 and 20"):
54+
ChatCompletionRequest(model=MODEL, messages=MESSAGES, logprobs=logprobs)
55+
56+
57+
@pytest.mark.parametrize("logprobs", [-1, 21, 100])
58+
def test_completion_request_rejects_out_of_range_logprobs(logprobs: int) -> None:
59+
with pytest.raises(ValidationError, match="between 0 and 20"):
60+
CompletionRequest(model=MODEL, prompt="Say hello.", logprobs=logprobs)
61+
62+
63+
@pytest.mark.parametrize("logprobs", [0, 1, 20])
64+
def test_chat_request_accepts_in_range_logprobs(logprobs: int) -> None:
65+
request = ChatCompletionRequest(model=MODEL, messages=MESSAGES, logprobs=logprobs)
66+
67+
# 0 is a valid value and must survive serialization of the payload.
68+
assert request.model_dump(exclude_none=True)["logprobs"] == logprobs
69+
70+
71+
@pytest.mark.parametrize("logprobs", [0, 1, 20])
72+
def test_completion_request_accepts_in_range_logprobs(logprobs: int) -> None:
73+
request = CompletionRequest(model=MODEL, prompt="Say hello.", logprobs=logprobs)
74+
75+
assert request.model_dump(exclude_none=True)["logprobs"] == logprobs
76+
77+
78+
def test_request_logprobs_defaults_to_omitted() -> None:
79+
request = ChatCompletionRequest(model=MODEL, messages=MESSAGES)
80+
81+
assert "logprobs" not in request.model_dump(exclude_none=True)
82+
83+
84+
def test_top_logprobs_survive_parsing_and_model_dump() -> None:
85+
"""Top-k alternatives must round-trip through the response models.
86+
87+
Guards against the mistyping reported in issue #443, where
88+
``top_logprobs`` declared as ``Dict[str, float]`` (instead of a list
89+
of per-token dicts) made ``model_dump()`` emit
90+
``PydanticSerializationUnexpectedValue`` warnings.
91+
"""
92+
response = ChatCompletionResponse(**RESPONSE_PAYLOAD)
93+
94+
assert response.choices is not None
95+
logprobs_part = response.choices[0].logprobs
96+
assert logprobs_part is not None
97+
assert logprobs_part.top_logprobs == TOP_LOGPROBS
98+
99+
with warnings.catch_warnings():
100+
warnings.simplefilter("error")
101+
dumped = response.model_dump()
102+
103+
assert dumped["choices"][0]["logprobs"]["top_logprobs"] == TOP_LOGPROBS

0 commit comments

Comments
 (0)