Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/instructions/util-genai.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ land the semconv change first.

## 4. Invocation shape

- `start_*()` factories must accept all sampling-relevant semconv attributes as parameters.
- Factory methods must accept all sampling-relevant semconv attributes as parameters.
Attributes also marked required by semconv must be required parameters (no default value).
- `start_*()` factories must map 1:1 to distinct semconv operation types (inference, embeddings,
- Factory methods must map 1:1 to distinct semconv operation types (inference, embeddings,
tool execution, agent invocation, workflow invocation). Names must match the operation
unambiguously — e.g., `create_agent` vs `invoke_agent` are distinct ops; `start_agent()` alone
unambiguously — e.g., `create_agent` vs `invoke_agent` are distinct ops; `agent()` alone
is ambiguous.
- Each operation exposes both a factory (`start_inference(...)`) and a context-manager
(`inference(...)`) form.
- Each operation exposes a factory such as `inference(...)`; returned invocations may also be
used as context managers.
- Never construct invocation types directly (`InferenceInvocation(...)`) — skips span creation,
silent no-ops. Always use `handler.start_*()` or the context manager.
silent no-ops. Always use the corresponding `handler` factory.

## 5. Exception handling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def test_stream_wrapper_finalization_records_thinking_tokens(
"opentelemetry.instrumentation.genai.anthropic"
),
)
invocation = handler.start_inference(
invocation = handler.inference(
provider="anthropic",
request_model="claude-sonnet-4-20250514",
)
Expand Down
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/.changelog/823.removed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove the deprecated GenAI utility callables ``should_emit_event()``, ``should_capture_content_on_spans()``, ``TelemetryHandler.start_inference()``, ``TelemetryHandler.start_llm()``, ``TelemetryHandler.start_embedding()``, ``TelemetryHandler.start_tool()``, ``TelemetryHandler.start_workflow()``, ``TelemetryHandler.stop_llm()``, ``TelemetryHandler.fail_llm()``, ``TelemetryHandler.start_invoke_local_agent()``, ``TelemetryHandler.start_invoke_remote_agent()``, and ``ToolInvocation.should_capture_content_on_span
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from __future__ import annotations

import logging
import os
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Final
Expand All @@ -13,15 +15,19 @@
gen_ai_attributes as GenAI,
)
from opentelemetry.semconv.attributes import server_attributes
from opentelemetry.trace import INVALID_SPAN, Span, SpanKind, Tracer
from opentelemetry.trace import SpanKind, Tracer
from opentelemetry.util.genai._instruments import _Instruments
from opentelemetry.util.genai._invocation import (
Error,
GenAIInvocation,
get_content_attributes,
)
from opentelemetry.util.genai.completion_hook import CompletionHook
from opentelemetry.util.genai.environment_variables import (
OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT,
)
from opentelemetry.util.genai.types import (
ContentCapturingMode,
Comment thread
rads-1996 marked this conversation as resolved.
ErrorTypeResolver,
InputMessage,
MessagePart,
Expand All @@ -31,12 +37,10 @@
SystemInstructionPart,
ToolDefinition,
)
from opentelemetry.util.genai.utils import (
ContentCapturingMode,
_should_emit_event,
)
from opentelemetry.util.types import AttributeValue

_logger = logging.getLogger(__name__)

_GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS: Final = (
"gen_ai.usage.cache_write.input_tokens"
)
Expand Down Expand Up @@ -78,6 +82,30 @@
_GEN_AI_PROMPT_VERSION: Final = "gen_ai.prompt.version"


def _should_emit_event(
content_capturing_mode: ContentCapturingMode,
) -> bool:
"""Check if event emission is enabled."""
if (
envvar := os.environ.get(OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, "")
.lower()
.strip()
):
if envvar == "true":
return True
if envvar == "false":
return False
_logger.warning(
"%s is not a valid option for `%s` environment variable. Must be one of true or false (case-insensitive). Defaulting based on content capturing mode.",
envvar,
OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT,
)
return content_capturing_mode in (
ContentCapturingMode.EVENT_ONLY,
ContentCapturingMode.SPAN_AND_EVENT,
)


class InferenceInvocation(GenAIInvocation):
"""Represents a single LLM chat/completion call.

Expand Down Expand Up @@ -457,10 +485,9 @@ def _maybe_create_event(self) -> LogRecord | None:

@dataclass
class LLMInvocation:
"""Deprecated. Use InferenceInvocation instead.
"""Deprecated compatibility data container for an LLM invocation.

Data container for an LLM invocation. Pass to handler.llm() to start
the span, then update fields and call handler.stop_llm() or handler.fail_llm().
Use ``handler.inference()`` to create an ``InferenceInvocation`` instead.
"""

request_model: str | None = None
Expand Down Expand Up @@ -489,82 +516,3 @@ class LLMInvocation:
seed: int | None = None
server_address: str | None = None
server_port: int | None = None

_inference_invocation: InferenceInvocation | None = field(
default=None, init=False, repr=False
)

def _start_with_handler(
self,
tracer: Tracer,
instruments: _Instruments,
logger: Logger,
completion_hook: CompletionHook,
*,
content_capturing_mode: ContentCapturingMode | None = None,
) -> None:
"""Create and start an InferenceInvocation from this data container. Called by handler.start_llm()."""
inv = InferenceInvocation(
tracer,
instruments,
logger,
completion_hook,
self.provider or "",
request_model=self.request_model,
server_address=self.server_address,
server_port=self.server_port,
content_capturing_mode=content_capturing_mode,
)
inv.input_messages = self.input_messages
inv.output_messages = self.output_messages
inv.system_instruction = self.system_instruction
inv.response_model_name = self.response_model_name
inv.response_id = self.response_id
inv.finish_reasons = self.finish_reasons
inv.input_tokens = self.input_tokens
inv.output_tokens = self.output_tokens

inv.temperature = self.temperature
inv.top_p = self.top_p
inv.frequency_penalty = self.frequency_penalty
inv.presence_penalty = self.presence_penalty
inv.max_tokens = self.max_tokens
inv.stop_sequences = self.stop_sequences
inv.seed = self.seed
inv.attributes.update(self.attributes)
inv.metric_attributes.update(self.metric_attributes)
self._inference_invocation = inv

def _sync_to_invocation(self) -> None:
inv = self._inference_invocation
if inv is None:
return
# Start attributes (provider, request_model, server_address, server_port)
# are fixed at construction in _start_with_handler and cannot be reassigned.
inv.input_messages = self.input_messages
inv.output_messages = self.output_messages
inv.system_instruction = self.system_instruction
inv.response_model_name = self.response_model_name
inv.response_id = self.response_id
inv.finish_reasons = self.finish_reasons
inv.input_tokens = self.input_tokens
inv.output_tokens = self.output_tokens

inv.temperature = self.temperature
inv.top_p = self.top_p
inv.frequency_penalty = self.frequency_penalty
inv.presence_penalty = self.presence_penalty
inv.max_tokens = self.max_tokens
inv.stop_sequences = self.stop_sequences
inv.seed = self.seed
inv.attributes = self.attributes
inv.metric_attributes = self.metric_attributes

@property
def span(self) -> Span:
"""The underlying span, for back-compat with code that checks span.is_recording()."""
return (
self._inference_invocation.span
if self._inference_invocation is not None
else INVALID_SPAN
)
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,6 @@ def __init__(
self._tool_type: str | None = tool_type
self._agent_name: str | None = agent_name

@property
def should_capture_content_on_span(self) -> bool:
"""Returns whether content capture is enabled on spans.

.. deprecated:: 1.2b0
Use :attr:`should_capture_content` instead.
"""
return self._should_capture_content_on_span

def _get_metric_attributes(self) -> dict[str, AttributeValue]:
attrs: dict[str, AttributeValue] = {
GenAI.GEN_AI_TOOL_NAME: self._name,
Expand Down
Loading
Loading