This commit is contained in:
ZJbiliy 2026-08-27 20:17:07 -05:00 • committed by GitHub
commit af7437e6b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 2 deletions

View file

@ -323,8 +323,15 @@ class ChunkProcessor:
model: Final = ChunkProcessor._get_model_from_chunks(chunks, first_chunk_model)
system_fingerprint: Final = chunk.get("system_fingerprint", None)
first_chunk_with_choices: Final = next((c for c in chunks if c.get("choices")), chunk)
role: Final = first_chunk_with_choices["choices"][0]["delta"]["role"]
# Fall back to None rather than `chunk`: if no chunk carries a non-empty
# `choices` array, indexing [0] on the first chunk raises IndexError.
first_chunk_with_choices = next((c for c in chunks if c.get("choices")), None)
role: str = "assistant"
if first_chunk_with_choices is not None:
_choices = first_chunk_with_choices["choices"]
if len(_choices) > 0:
# `delta` may be absent or omit `role` (e.g. content-only deltas).
role = _choices[0].get("delta", {}).get("role") or "assistant"
finish_reason = "stop"
for chunk in chunks:
if "choices" in chunk and len(chunk["choices"]) > 0:

View file

@ -1423,3 +1423,106 @@ def test_calculate_usage_fills_unknown_split_from_reasoning_estimate(
assert usage.completion_tokens == 100
assert usage.completion_tokens_details.reasoning_tokens == expected_reasoning_tokens
assert usage.completion_tokens_details.text_tokens == expected_text_tokens
def _empty_choices_chunk(**extra):
chunk = {
"id": "chatcmpl-empty-choices",
"object": "chat.completion.chunk",
"created": 1,
"model": "claude-opus-4-8",
"choices": [],
}
chunk.update(extra)
return chunk
@pytest.mark.parametrize(
"chunks",
[
pytest.param(
[_empty_choices_chunk(), _empty_choices_chunk()],
id="all_chunks_have_empty_choices",
),
pytest.param(
[
_empty_choices_chunk(usage={"prompt_tokens": 10}),
_empty_choices_chunk(usage={"completion_tokens": 0}),
],
id="usage_only_chunks",
),
],
)
def test_build_base_response_handles_empty_choices(chunks):
"""Empty `choices` arrays must not raise IndexError.
`next((c for c in chunks if c.get("choices")), chunk)` used to fall back to the
first chunk, whose `choices` may be `[]`, so `["choices"][0]` went out of range.
The resulting error is surfaced to the client mid-stream and the request never
reaches SpendLogs.
"""
processor = ChunkProcessor(chunks=list(chunks))
response = processor.build_base_response(list(chunks))
assert response.choices[0].message.role == "assistant"
@pytest.mark.parametrize(
"delta",
[
pytest.param({"content": "Hello"}, id="delta_without_role"),
pytest.param({}, id="delta_empty_dict"),
],
)
def test_build_base_response_handles_delta_without_role(delta):
"""A `delta` that omits `role` must not raise KeyError."""
chunks = [
{
"id": "chatcmpl-no-role",
"object": "chat.completion.chunk",
"created": 1,
"model": "claude-opus-4-8",
"choices": [{"index": 0, "delta": delta, "finish_reason": None}],
}
]
processor = ChunkProcessor(chunks=list(chunks))
response = processor.build_base_response(list(chunks))
assert response.choices[0].message.role == "assistant"
def test_build_base_response_still_reads_role_and_finish_reason():
"""Regression guard: well-formed chunks keep their role and finish_reason."""
chunks = [
_empty_choices_chunk(),
{
"id": "chatcmpl-normal",
"object": "chat.completion.chunk",
"created": 1,
"model": "claude-opus-4-8",
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": "Hi"},
"finish_reason": None,
}
],
},
{
"id": "chatcmpl-normal",
"object": "chat.completion.chunk",
"created": 2,
"model": "claude-opus-4-8",
"choices": [
{"index": 0, "delta": {"content": "!"}, "finish_reason": "stop"}
],
},
]
processor = ChunkProcessor(chunks=list(chunks))
response = processor.build_base_response(list(chunks))
assert response.choices[0].message.role == "assistant"
assert response.choices[0].finish_reason == "stop"