Skip to content

Commit 23d2eb3

Browse files
patcherai-opensource-internal[bot]claude
andcommitted
test(openai): Add tests for choices=None guard in OpenAI integration
Covers non-streaming, sync streaming, async streaming, and unit-level token usage calculation to verify no crash when a provider returns choices=None (e.g. OpenRouter upstream error responses). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1387d00 commit 23d2eb3

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

tests/integrations/openai/test_openai.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2388,6 +2388,189 @@ def count_tokens(msg):
23882388
)
23892389

23902390

2391+
def test_completions_token_usage_choices_none():
2392+
"""When response.choices is None (e.g. OpenRouter upstream error), no crash and no output tokens."""
2393+
span = mock.MagicMock()
2394+
2395+
def count_tokens(msg):
2396+
return len(str(msg))
2397+
2398+
response = mock.MagicMock()
2399+
response.usage = mock.MagicMock()
2400+
response.usage.completion_tokens = None
2401+
response.usage.prompt_tokens = None
2402+
response.usage.total_tokens = None
2403+
response.choices = None
2404+
messages = []
2405+
streaming_message_responses = None
2406+
2407+
with mock.patch(
2408+
"sentry_sdk.integrations.openai.record_token_usage"
2409+
) as mock_record_token_usage:
2410+
_calculate_completions_token_usage(
2411+
messages=messages,
2412+
response=response,
2413+
span=span,
2414+
streaming_message_responses=streaming_message_responses,
2415+
streaming_message_total_token_usage=None,
2416+
count_tokens=count_tokens,
2417+
)
2418+
mock_record_token_usage.assert_called_once_with(
2419+
span,
2420+
input_tokens=None,
2421+
input_tokens_cached=None,
2422+
output_tokens=None,
2423+
output_tokens_reasoning=None,
2424+
total_tokens=None,
2425+
)
2426+
2427+
2428+
def test_nonstreaming_chat_completion_choices_none(sentry_init, capture_events):
2429+
"""When choices=None (e.g. OpenRouter upstream error), no crash and span is still created."""
2430+
sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0)
2431+
events = capture_events()
2432+
2433+
response = mock.MagicMock()
2434+
response.id = "chat-id"
2435+
response.model = "gpt-3.5-turbo"
2436+
response.choices = None
2437+
response.usage = mock.MagicMock()
2438+
response.usage.completion_tokens = None
2439+
response.usage.prompt_tokens = None
2440+
response.usage.total_tokens = None
2441+
2442+
client = OpenAI(api_key="z")
2443+
client.chat.completions._post = mock.Mock(return_value=response)
2444+
2445+
with start_transaction(name="openai tx"):
2446+
result = client.chat.completions.create(
2447+
model="some-model",
2448+
messages=[{"role": "user", "content": "hello"}],
2449+
)
2450+
2451+
assert result is response
2452+
(tx,) = events
2453+
assert tx["type"] == "transaction"
2454+
assert len(tx["spans"]) == 1
2455+
span = tx["spans"][0]
2456+
assert span["op"] == "gen_ai.chat"
2457+
2458+
2459+
def test_streaming_chat_completion_choices_none(
2460+
sentry_init,
2461+
capture_events,
2462+
get_model_response,
2463+
server_side_event_chunks,
2464+
):
2465+
"""When a streaming chunk has choices=None, no crash and token usage is still recorded."""
2466+
sentry_init(
2467+
integrations=[OpenAIIntegration(include_prompts=False)],
2468+
traces_sample_rate=1.0,
2469+
send_default_pii=False,
2470+
)
2471+
events = capture_events()
2472+
2473+
# Use model_construct to bypass Pydantic validation and set choices=None
2474+
chunk_with_none_choices = ChatCompletionChunk.model_construct(
2475+
id="1",
2476+
choices=None,
2477+
created=100000,
2478+
model="model-id",
2479+
object="chat.completion.chunk",
2480+
usage=CompletionUsage(
2481+
prompt_tokens=5,
2482+
completion_tokens=0,
2483+
total_tokens=5,
2484+
),
2485+
)
2486+
client = OpenAI(api_key="z")
2487+
returned_stream = get_model_response(
2488+
server_side_event_chunks(
2489+
[chunk_with_none_choices],
2490+
include_event_type=False,
2491+
)
2492+
)
2493+
2494+
with mock.patch.object(
2495+
client.chat._client._client,
2496+
"send",
2497+
return_value=returned_stream,
2498+
):
2499+
with start_transaction(name="openai tx"):
2500+
response_stream = client.chat.completions.create(
2501+
model="some-model",
2502+
messages=[{"role": "user", "content": "hello"}],
2503+
stream=True,
2504+
)
2505+
for _ in response_stream:
2506+
pass
2507+
2508+
(tx,) = events
2509+
assert tx["type"] == "transaction"
2510+
span = tx["spans"][0]
2511+
assert span["op"] == "gen_ai.chat"
2512+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
2513+
2514+
2515+
@pytest.mark.asyncio
2516+
async def test_streaming_chat_completion_choices_none_async(
2517+
sentry_init,
2518+
capture_events,
2519+
get_model_response,
2520+
async_iterator,
2521+
server_side_event_chunks,
2522+
):
2523+
"""When an async streaming chunk has choices=None, no crash and span is still created."""
2524+
sentry_init(
2525+
integrations=[OpenAIIntegration(include_prompts=False)],
2526+
traces_sample_rate=1.0,
2527+
send_default_pii=False,
2528+
)
2529+
events = capture_events()
2530+
2531+
chunk_with_none_choices = ChatCompletionChunk.model_construct(
2532+
id="1",
2533+
choices=None,
2534+
created=100000,
2535+
model="model-id",
2536+
object="chat.completion.chunk",
2537+
usage=CompletionUsage(
2538+
prompt_tokens=5,
2539+
completion_tokens=0,
2540+
total_tokens=5,
2541+
),
2542+
)
2543+
client = AsyncOpenAI(api_key="z")
2544+
returned_stream = get_model_response(
2545+
async_iterator(
2546+
server_side_event_chunks(
2547+
[chunk_with_none_choices],
2548+
include_event_type=False,
2549+
)
2550+
)
2551+
)
2552+
2553+
with mock.patch.object(
2554+
client.chat._client._client,
2555+
"send",
2556+
return_value=returned_stream,
2557+
):
2558+
with start_transaction(name="openai tx"):
2559+
response_stream = await client.chat.completions.create(
2560+
model="some-model",
2561+
messages=[{"role": "user", "content": "hello"}],
2562+
stream=True,
2563+
)
2564+
async for _ in response_stream:
2565+
pass
2566+
2567+
(tx,) = events
2568+
assert tx["type"] == "transaction"
2569+
span = tx["spans"][0]
2570+
assert span["op"] == "gen_ai.chat"
2571+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
2572+
2573+
23912574
@pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available")
23922575
def test_responses_token_usage_from_response():
23932576
"""Token counts including cached and reasoning tokens are extracted from Responses API."""

0 commit comments

Comments
 (0)