Skip to content

Commit b8e7e4e

Browse files
committed
Refactor public API on GenAI utils
1 parent 048b4ad commit b8e7e4e

File tree

31 files changed

+1570
-1329
lines changed

31 files changed

+1570
-1329
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,5 @@ target
6464

6565
# opentelemetry-admin jobs
6666
opentelemetry-admin-jobs.txt
67+
68+
.claude*/

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# OpenTelemetry Python Contrib — Claude Guidelines
2+
3+
**Before writing any code in this repo, you must have read the nearest `AGENTS.md` file
4+
in that directory or its parents (up to this root).** This is a precondition, not a suggestion.
5+
Follow the guidelines there in addition to these.

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pylint==3.0.2
22
httpretty==1.1.4
3-
pyright==v1.1.404
3+
pyright==v1.1.408
44
sphinx==7.1.2
55
sphinx-rtd-theme==2.0.0rc4
66
sphinx-autodoc-typehints==1.25.2

instrumentation-genai/AGENTS.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# GenAI Instrumentation — Agent and Contributor Guidelines
2+
3+
Instrumentation packages here wrap specific libraries (OpenAI, Anthropic, etc.) and bridge
4+
them to the shared telemetry layer in `util/opentelemetry-util-genai`.
5+
6+
## 1. Instrumentation Layer Boundary
7+
8+
Do not call OpenTelemetry APIs (`tracer`, `meter`, `span`, event APIs) directly.
9+
Always go through `TelemetryHandler` and the invocation objects it returns.
10+
11+
This layer is responsible only for:
12+
13+
- Patching the library
14+
- Parsing library-specific input/output into invocation fields
15+
16+
Everything else (span creation, metric recording, event emission, context propagation)
17+
belongs in `util/opentelemetry-util-genai`.
18+
19+
## 2. Invocation Pattern
20+
21+
Use `start_*()` and control span lifetime manually:
22+
23+
```python
24+
invocation = handler.start_inference(provider, request_model, server_address=..., server_port=...)
25+
invocation.temperature = ...
26+
try:
27+
response = client.call(...)
28+
invocation.response_model_name = response.model
29+
invocation.finish_reasons = response.finish_reasons
30+
invocation.stop()
31+
except Exception as exc:
32+
invocation.fail(exc)
33+
raise
34+
```
35+
36+
## 3. Exception Handling
37+
38+
- Do not add `raise` statements in instrumentation/telemetry code — validation belongs in
39+
tests and callers, not in the instrumentation layer.
40+
- When catching exceptions from the underlying library to record telemetry, always re-raise
41+
the original exception unmodified.
42+
- Do not wrap, replace, or suppress exceptions — telemetry must be transparent to callers.
43+

instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/patch.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
gen_ai_attributes as GenAIAttributes,
2525
)
2626
from opentelemetry.util.genai.handler import TelemetryHandler
27-
from opentelemetry.util.genai.types import Error, LLMInvocation
27+
from opentelemetry.util.genai.inference_invocation import (
28+
LLMInvocation, # pyright: ignore[reportDeprecated] # TODO: migrate to InferenceInvocation
29+
)
30+
from opentelemetry.util.genai.types import Error
2831
from opentelemetry.util.genai.utils import (
2932
should_capture_content_on_spans_in_experimental_mode,
3033
)
@@ -89,7 +92,7 @@ def traced_method(
8992
else params.model
9093
)
9194

92-
invocation = LLMInvocation(
95+
invocation = LLMInvocation( # pyright: ignore[reportDeprecated]
9396
request_model=request_model,
9497
provider=ANTHROPIC,
9598
input_messages=get_input_messages(params.messages)
@@ -102,7 +105,7 @@ def traced_method(
102105
)
103106

104107
# Use manual lifecycle management for both streaming and non-streaming
105-
handler.start_llm(invocation)
108+
handler.start_llm(invocation) # pyright: ignore[reportDeprecated]
106109
try:
107110
result = wrapped(*args, **kwargs)
108111
if isinstance(result, AnthropicStream):
@@ -112,10 +115,10 @@ def traced_method(
112115

113116
wrapper = MessageWrapper(result, capture_content)
114117
wrapper.extract_into(invocation)
115-
handler.stop_llm(invocation)
118+
handler.stop_llm(invocation) # pyright: ignore[reportDeprecated]
116119
return wrapper.message
117120
except Exception as exc:
118-
handler.fail_llm(
121+
handler.fail_llm( # pyright: ignore[reportDeprecated]
119122
invocation, Error(message=str(exc), type=type(exc))
120123
)
121124
raise

instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/wrappers.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from typing import TYPE_CHECKING, Callable, Iterator, Optional
2020

2121
from opentelemetry.util.genai.handler import TelemetryHandler
22+
from opentelemetry.util.genai.inference_invocation import (
23+
LLMInvocation, # pyright: ignore[reportDeprecated] # TODO: migrate to InferenceInvocation
24+
)
2225
from opentelemetry.util.genai.types import (
2326
Error,
24-
LLMInvocation,
2527
MessagePart,
2628
OutputMessage,
2729
)
@@ -60,7 +62,7 @@ def __init__(self, message: Message, capture_content: bool):
6062
self._message = message
6163
self._capture_content = capture_content
6264

63-
def extract_into(self, invocation: LLMInvocation) -> None:
65+
def extract_into(self, invocation: LLMInvocation) -> None: # pyright: ignore[reportDeprecated]
6466
"""Extract response data into the invocation."""
6567
if self._message.model:
6668
invocation.response_model_name = self._message.model
@@ -103,7 +105,7 @@ def __init__(
103105
self,
104106
stream: Stream[RawMessageStreamEvent],
105107
handler: TelemetryHandler,
106-
invocation: LLMInvocation,
108+
invocation: LLMInvocation, # pyright: ignore[reportDeprecated]
107109
capture_content: bool,
108110
):
109111
self._stream = stream
@@ -213,7 +215,7 @@ def _stop(self) -> None:
213215
"response attribute extraction",
214216
)
215217
self._safe_instrumentation(
216-
lambda: self._handler.stop_llm(self._invocation),
218+
lambda: self._handler.stop_llm(self._invocation), # pyright: ignore[reportDeprecated]
217219
"stop_llm",
218220
)
219221
self._finalized = True
@@ -222,7 +224,7 @@ def _fail(self, message: str, error_type: type[BaseException]) -> None:
222224
if self._finalized:
223225
return
224226
self._safe_instrumentation(
225-
lambda: self._handler.fail_llm(
227+
lambda: self._handler.fail_llm( # pyright: ignore[reportDeprecated]
226228
self._invocation, Error(message=message, type=error_type)
227229
),
228230
"fail_llm",

instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
_InvocationManager,
2626
)
2727
from opentelemetry.util.genai.handler import TelemetryHandler
28+
from opentelemetry.util.genai.inference_invocation import (
29+
LLMInvocation, # pyright: ignore[reportDeprecated] # TODO: migrate to InferenceInvocation
30+
)
2831
from opentelemetry.util.genai.types import (
2932
Error,
3033
InputMessage,
31-
LLMInvocation,
3234
MessagePart,
3335
OutputMessage,
3436
Text,
@@ -140,7 +142,7 @@ def on_chat_model_start(
140142
)
141143
)
142144

143-
llm_invocation = LLMInvocation(
145+
llm_invocation = LLMInvocation( # pyright: ignore[reportDeprecated]
144146
request_model=request_model,
145147
input_messages=input_messages,
146148
provider=provider,
@@ -152,7 +154,7 @@ def on_chat_model_start(
152154
temperature=temperature,
153155
max_tokens=max_tokens,
154156
)
155-
llm_invocation = self._telemetry_handler.start_llm(
157+
llm_invocation = self._telemetry_handler.start_llm( # pyright: ignore[reportDeprecated]
156158
invocation=llm_invocation
157159
)
158160
self._invocation_manager.add_invocation_state(
@@ -171,7 +173,8 @@ def on_llm_end(
171173
) -> None:
172174
llm_invocation = self._invocation_manager.get_invocation(run_id=run_id)
173175
if llm_invocation is None or not isinstance(
174-
llm_invocation, LLMInvocation
176+
llm_invocation,
177+
LLMInvocation, # pyright: ignore[reportDeprecated]
175178
):
176179
# If the invocation does not exist, we cannot set attributes or end it
177180
return
@@ -246,7 +249,7 @@ def on_llm_end(
246249
if response_id is not None:
247250
llm_invocation.response_id = str(response_id)
248251

249-
llm_invocation = self._telemetry_handler.stop_llm(
252+
llm_invocation = self._telemetry_handler.stop_llm( # pyright: ignore[reportDeprecated]
250253
invocation=llm_invocation
251254
)
252255
if llm_invocation.span and not llm_invocation.span.is_recording():
@@ -262,13 +265,14 @@ def on_llm_error(
262265
) -> None:
263266
llm_invocation = self._invocation_manager.get_invocation(run_id=run_id)
264267
if llm_invocation is None or not isinstance(
265-
llm_invocation, LLMInvocation
268+
llm_invocation,
269+
LLMInvocation, # pyright: ignore[reportDeprecated]
266270
):
267271
# If the invocation does not exist, we cannot set attributes or end it
268272
return
269273

270274
error_otel = Error(message=str(error), type=type(error))
271-
llm_invocation = self._telemetry_handler.fail_llm(
275+
llm_invocation = self._telemetry_handler.fail_llm( # pyright: ignore[reportDeprecated]
272276
invocation=llm_invocation, error=error_otel
273277
)
274278
if llm_invocation.span and not llm_invocation.span.is_recording():

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ classifiers = [
2626
"Programming Language :: Python :: 3.14",
2727
]
2828
dependencies = [
29-
"opentelemetry-api >= 1.37",
29+
"opentelemetry-api >= 1.39",
3030
"opentelemetry-instrumentation >= 0.58b0",
31-
"opentelemetry-semantic-conventions >= 0.58b0",
32-
"opentelemetry-util-genai"
31+
"opentelemetry-semantic-conventions >= 0.60b0",
32+
"opentelemetry-util-genai >= 0.4b0.dev"
3333
]
3434

3535
[project.optional-dependencies]

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.latest.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ grpcio>=1.60.0; implementation_name != "pypy"
5050
grpcio<1.60.0; implementation_name == "pypy"
5151

5252
-e opentelemetry-instrumentation
53+
-e util/opentelemetry-util-genai
5354
-e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.oldest.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ pytest-asyncio==0.21.0
2626
wrapt==1.16.0
2727
opentelemetry-exporter-otlp-proto-grpc~=1.30
2828
opentelemetry-exporter-otlp-proto-http~=1.30
29-
opentelemetry-api==1.37 # when updating, also update in pyproject.toml
30-
opentelemetry-sdk==1.37 # when updating, also update in pyproject.toml
31-
opentelemetry-semantic-conventions==0.58b0 # when updating, also update in pyproject.toml
29+
opentelemetry-api==1.39 # when updating, also update in pyproject.toml
30+
opentelemetry-sdk==1.39 # when updating, also update in pyproject.toml
31+
opentelemetry-semantic-conventions==0.60b0 # when updating, also update in pyproject.toml
3232
grpcio>=1.60.0; implementation_name != "pypy"
3333
grpcio<1.60.0; implementation_name == "pypy"
3434

35+
-e util/opentelemetry-util-genai
3536
-e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2

0 commit comments

Comments
 (0)