Skip to content

Commit d87745d

Browse files
committed
fix: OpenAI Types Normalized with Cast
There were a few assumptions being made about openai types in the tests and the openai wrappers that raised linting errors. Some of these errors were rightly raised, especially around streaming types, and have been patched.
1 parent f5d746f commit d87745d

File tree

6 files changed

+90
-38
lines changed

6 files changed

+90
-38
lines changed

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515

1616
import asyncio
1717
import inspect
18+
from collections.abc import AsyncIterator, Iterator
1819
from timeit import default_timer
19-
from typing import Any, Optional
20-
21-
from openai import Stream
20+
from typing import Any, Optional, cast
2221

2322
from opentelemetry._logs import Logger, LogRecord
2423
from opentelemetry.context import get_current
@@ -537,7 +536,7 @@ class StreamWrapper:
537536

538537
def __init__(
539538
self,
540-
stream: Stream,
539+
stream: Iterator[Any] | AsyncIterator[Any],
541540
span: Span,
542541
logger: Logger,
543542
capture_content: bool,
@@ -663,14 +662,18 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
663662

664663
def close(self):
665664
try:
666-
close_result = self.stream.close()
665+
close_fn = getattr(self.stream, "close", None)
666+
if not callable(close_fn):
667+
return
668+
669+
close_result = close_fn()
667670
if inspect.isawaitable(close_result):
668671
try:
669672
loop = asyncio.get_running_loop()
670673
except RuntimeError:
671-
asyncio.run(close_result)
674+
asyncio.run(cast(Any, close_result))
672675
else:
673-
loop.create_task(close_result)
676+
loop.create_task(cast(Any, close_result))
674677
finally:
675678
self.cleanup()
676679

@@ -682,7 +685,7 @@ def __aiter__(self):
682685

683686
def __next__(self):
684687
try:
685-
chunk = next(self.stream)
688+
chunk = next(cast(Iterator[Any], self.stream))
686689
self.process_chunk(chunk)
687690
return chunk
688691
except StopIteration:
@@ -695,7 +698,7 @@ def __next__(self):
695698

696699
async def __anext__(self):
697700
try:
698-
chunk = await self.stream.__anext__() # type: ignore[attr-defined]
701+
chunk = await anext(cast(AsyncIterator[Any], self.stream))
699702
self.process_chunk(chunk)
700703
return chunk
701704
except StopAsyncIteration:

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/responses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _create_log_record(
107107
event_name: str, body: Mapping[str, Any] | None
108108
) -> LogRecord:
109109
return LogRecord(
110-
event_name=event_name, # type: ignore[call-arg]
110+
event_name=event_name,
111111
attributes=_GEN_AI_SYSTEM_ATTRIBUTES,
112112
body=dict(body) if body else None,
113113
)

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/responses_patch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def _emit_output_log(self) -> None:
276276
context = set_span_in_context(self.span, get_current())
277277
self.logger.emit(
278278
LogRecord(
279-
event_name="gen_ai.output", # type: ignore[call-arg]
279+
event_name="gen_ai.output",
280280
attributes={
281281
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value
282282
},
@@ -340,7 +340,7 @@ def __aiter__(self):
340340

341341
async def __anext__(self):
342342
try:
343-
event = await self.stream.__anext__() # type: ignore[attr-defined]
343+
event = await self.stream.__anext__()
344344
self._process_event(event)
345345
return event
346346
except StopAsyncIteration:

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def set_span_attributes(span, attributes):
173173
but tests may pass dicts or other lightweight objects.
174174
"""
175175
if hasattr(attributes, "model_dump"):
176-
for field, value in attributes.model_dump(by_alias=True).items(): # type: ignore[attr-defined]
176+
for field, value in attributes.model_dump(by_alias=True).items():
177177
set_span_attribute(span, field, value)
178178
return
179179

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_responses_basic_with_content(
7474
# First log should be the input message
7575
input_log = logs[0]
7676
assert input_log.log_record.event_name == "gen_ai.user.input"
77-
assert input_log.log_record.body["content"] == input_text # type: ignore[index]
77+
assert input_log.log_record.body["content"] == input_text
7878
assert_log_parent(input_log, span)
7979

8080
output_logs = [

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses_handlers.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
"""Resilience tests for Responses input/output conversion helpers."""
1616

1717
from dataclasses import dataclass
18-
from typing import Any, Optional
18+
from typing import Any, Optional, cast
1919

2020
import pytest
21+
from openai.types.responses import ResponseOutputItem
22+
from openai.types.responses import ResponseInputItemParam
2123

2224
from opentelemetry.instrumentation.openai_v2.responses import (
2325
output_to_event,
@@ -26,6 +28,7 @@
2628
from opentelemetry.instrumentation.openai_v2.responses_patch import (
2729
_log_responses_inputs,
2830
)
31+
from opentelemetry._logs import Logger
2932

3033

3134
@dataclass
@@ -49,6 +52,13 @@ def emit(self, record: Any) -> None:
4952
self.emitted.append(record)
5053

5154

55+
def _as_output_item(value: Any) -> ResponseOutputItem:
56+
# These tests intentionally pass minimal "shape-only" objects to exercise
57+
# resilience for new/unknown output types. Cast keeps type-checkers happy
58+
# without weakening the production signature.
59+
return cast(ResponseOutputItem, value)
60+
61+
5262
@pytest.mark.parametrize(
5363
"input_data",
5464
[
@@ -145,10 +155,16 @@ def emit(self, record: Any) -> None:
145155
def test_input_type_handler_does_not_crash(input_data):
146156
"""Each known input type should be processed without exceptions."""
147157
assert (
148-
responses_input_to_event(input_data, capture_content=True) is not None
158+
responses_input_to_event(
159+
cast(ResponseInputItemParam, input_data), capture_content=True
160+
)
161+
is not None
149162
)
150163
assert (
151-
responses_input_to_event(input_data, capture_content=False) is not None
164+
responses_input_to_event(
165+
cast(ResponseInputItemParam, input_data), capture_content=False
166+
)
167+
is not None
152168
)
153169

154170

@@ -166,19 +182,19 @@ def test_string_input_does_not_crash():
166182
def test_responses_create_input_none_is_noop_for_logging():
167183
logger = _CapturingLogger()
168184

169-
_log_responses_inputs(logger, {"input": None}, capture_content=True)
170-
_log_responses_inputs(logger, {"input": None}, capture_content=False)
185+
_log_responses_inputs(cast(Logger, logger), {"input": None}, capture_content=True)
186+
_log_responses_inputs(cast(Logger, logger), {"input": None}, capture_content=False)
171187

172-
assert logger.emitted == []
188+
assert not logger.emitted
173189

174190

175191
def test_responses_create_input_omitted_is_noop_for_logging():
176192
logger = _CapturingLogger()
177193

178-
_log_responses_inputs(logger, {}, capture_content=True)
179-
_log_responses_inputs(logger, {}, capture_content=False)
194+
_log_responses_inputs(cast(Logger, logger), {}, capture_content=True)
195+
_log_responses_inputs(cast(Logger, logger), {}, capture_content=False)
180196

181-
assert logger.emitted == []
197+
assert not logger.emitted
182198

183199

184200
@pytest.mark.parametrize(
@@ -204,7 +220,9 @@ def test_responses_create_input_omitted_is_noop_for_logging():
204220
)
205221
def test_unexpected_input_does_not_crash(unexpected_input):
206222
# Should not raise
207-
responses_input_to_event(unexpected_input, capture_content=True)
223+
responses_input_to_event(
224+
cast(ResponseInputItemParam, unexpected_input), capture_content=True
225+
)
208226

209227

210228
def test_deeply_nested_content_does_not_crash():
@@ -222,7 +240,9 @@ def test_deeply_nested_content_does_not_crash():
222240
],
223241
}
224242
assert (
225-
responses_input_to_event(nested_input, capture_content=True)
243+
responses_input_to_event(
244+
cast(ResponseInputItemParam, nested_input), capture_content=True
245+
)
226246
is not None
227247
)
228248

@@ -240,29 +260,37 @@ def test_future_union_types_are_best_effort():
240260
"name": "some_tool",
241261
"arguments": {"x": 1},
242262
}
243-
event = responses_input_to_event(future_call, capture_content=True)
263+
event = responses_input_to_event(
264+
cast(ResponseInputItemParam, future_call), capture_content=True
265+
)
244266
assert event is not None
245267
assert event.event_name == "gen_ai.assistant.input"
246-
assert event.body and event.body.get("type") == "future_provider_tool_call"
268+
body: Any = event.body
269+
assert body and body.get("type") == "future_provider_tool_call"
247270

248271
# New tool call output (tool -> model): routed by `*_call_output`
249272
future_call_output = {
250273
"type": "future_provider_tool_call_output",
251274
"call_id": "call_123",
252275
"output": "ok",
253276
}
254-
event = responses_input_to_event(future_call_output, capture_content=True)
277+
event = responses_input_to_event(
278+
cast(ResponseInputItemParam, future_call_output), capture_content=True
279+
)
255280
assert event is not None
256281
assert event.event_name == "gen_ai.tool.input"
257-
assert event.body and event.body.get("id") == "call_123"
282+
body = event.body
283+
assert body and body.get("id") == "call_123"
258284

259285
# Another output naming style: routed by `*_output`
260286
future_output_alt = {
261287
"type": "future_provider_tool_output",
262288
"id": "id_999",
263289
"output": [{"type": "text", "text": "ok"}],
264290
}
265-
event = responses_input_to_event(future_output_alt, capture_content=True)
291+
event = responses_input_to_event(
292+
cast(ResponseInputItemParam, future_output_alt), capture_content=True
293+
)
266294
assert event is not None
267295
assert event.event_name == "gen_ai.tool.input"
268296

@@ -279,8 +307,14 @@ class MockOutput:
279307
content = [ContentPart()]
280308
index = 0
281309

282-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
283-
assert output_to_event(MockOutput(), capture_content=False) is not None # type: ignore[arg-type]
310+
assert (
311+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
312+
is not None
313+
)
314+
assert (
315+
output_to_event(_as_output_item(MockOutput()), capture_content=False)
316+
is not None
317+
)
284318

285319

286320
def test_function_call_output_does_not_crash():
@@ -292,7 +326,10 @@ class MockOutput:
292326
status = "completed"
293327
index = 0
294328

295-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
329+
assert (
330+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
331+
is not None
332+
)
296333

297334

298335
def test_reasoning_output_does_not_crash():
@@ -306,7 +343,10 @@ class MockOutput:
306343
status = "completed"
307344
summary = [Part()]
308345

309-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
346+
assert (
347+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
348+
is not None
349+
)
310350

311351

312352
@pytest.mark.parametrize(
@@ -328,7 +368,7 @@ def test_tool_call_outputs_do_not_crash(output_type):
328368
name="tool_name",
329369
status="completed",
330370
)
331-
assert output_to_event(mock, capture_content=True) is not None # type: ignore[arg-type]
371+
assert output_to_event(_as_output_item(mock), capture_content=True) is not None
332372

333373

334374
def test_unknown_output_type_does_not_crash():
@@ -337,14 +377,20 @@ class MockOutput:
337377
id = "id_123"
338378
status = "completed"
339379

340-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
380+
assert (
381+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
382+
is not None
383+
)
341384

342385

343386
def test_minimal_output_does_not_crash():
344387
class MockOutput:
345388
type = "message"
346389

347-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
390+
assert (
391+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
392+
is not None
393+
)
348394

349395

350396
def test_output_with_none_values_does_not_crash():
@@ -355,4 +401,7 @@ class MockOutput:
355401
content = None
356402
index = None
357403

358-
assert output_to_event(MockOutput(), capture_content=True) is not None # type: ignore[arg-type]
404+
assert (
405+
output_to_event(_as_output_item(MockOutput()), capture_content=True)
406+
is not None
407+
)

0 commit comments

Comments
 (0)