Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Record cache-write and modality token usage from OpenAI Chat Completions responses and streams.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
)

from .chat_buffers import ChoiceBuffer
from .utils import get_property_value, map_finish_reason
from .utils import (
get_property_value,
map_finish_reason,
)

_logger = logging.getLogger(__name__)

Expand All @@ -41,6 +44,12 @@ class _ChatStreamMixin:
_self_prompt_tokens: int | None
_self_completion_tokens: int | None
_self_cached_prompt_tokens: int | None
_self_cache_write_prompt_tokens: int | None
_self_text_prompt_tokens: int | None
_self_image_prompt_tokens: int | None
_self_audio_prompt_tokens: int | None
_self_text_completion_tokens: int | None
_self_audio_completion_tokens: int | None
_self_reasoning_tokens: int | None

def _set_response_model(self, chunk: ChatCompletionChunk) -> None:
Expand Down Expand Up @@ -106,9 +115,27 @@ def _set_usage(self, chunk: ChatCompletionChunk) -> None:
self._self_cached_prompt_tokens = get_property_value(
prompt_tokens_details, "cached_tokens"
)
self._self_cache_write_prompt_tokens = get_property_value(
prompt_tokens_details, "cache_write_tokens"
)
self._self_text_prompt_tokens = get_property_value(
prompt_tokens_details, "text_tokens"
)
self._self_image_prompt_tokens = get_property_value(
prompt_tokens_details, "image_tokens"
)
self._self_audio_prompt_tokens = get_property_value(
prompt_tokens_details, "audio_tokens"
)
completion_tokens_details = getattr(
usage, "completion_tokens_details", None
)
self._self_text_completion_tokens = get_property_value(
completion_tokens_details, "text_tokens"
)
self._self_audio_completion_tokens = get_property_value(
completion_tokens_details, "audio_tokens"
)
if completion_tokens_details is not None:
self._self_reasoning_tokens = get_property_value(
completion_tokens_details, "reasoning_tokens"
Expand Down Expand Up @@ -177,6 +204,22 @@ def _cleanup(self, error: BaseException | None = None) -> None:
self._self_invocation.cache_read_input_tokens = (
self._self_cached_prompt_tokens
)
self._self_invocation.cache_write_input_tokens = (
self._self_cache_write_prompt_tokens
)
self._self_invocation.text_input_tokens = self._self_text_prompt_tokens
self._self_invocation.image_input_tokens = (
self._self_image_prompt_tokens
)
self._self_invocation.audio_input_tokens = (
self._self_audio_prompt_tokens
)
self._self_invocation.text_output_tokens = (
self._self_text_completion_tokens
)
self._self_invocation.audio_output_tokens = (
self._self_audio_completion_tokens
)
self._self_invocation.thinking_tokens = self._self_reasoning_tokens
finish_reasons = [
choice.finish_reason
Expand Down Expand Up @@ -219,6 +262,12 @@ def __init__(
self._self_prompt_tokens = None
self._self_completion_tokens = None
self._self_cached_prompt_tokens = None
self._self_cache_write_prompt_tokens = None
self._self_text_prompt_tokens = None
self._self_image_prompt_tokens = None
self._self_audio_prompt_tokens = None
self._self_text_completion_tokens = None
self._self_audio_completion_tokens = None
self._self_reasoning_tokens = None


Expand All @@ -241,6 +290,12 @@ def __init__(
self._self_prompt_tokens = None
self._self_completion_tokens = None
self._self_cached_prompt_tokens = None
self._self_cache_write_prompt_tokens = None
self._self_text_prompt_tokens = None
self._self_image_prompt_tokens = None
self._self_audio_prompt_tokens = None
self._self_text_completion_tokens = None
self._self_audio_completion_tokens = None
self._self_reasoning_tokens = None


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
get_server_address_and_port,
get_value,
is_streaming,
set_chat_usage_details,
)

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -208,6 +209,7 @@ def _set_response_properties(
chat_invocation.cache_read_input_tokens = get_property_value(
prompt_tokens_details, "cached_tokens"
)
set_chat_usage_details(invocation=chat_invocation, usage=result.usage)
completion_tokens_details = getattr(
result.usage, "completion_tokens_details", None
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import openai
from openai import NotGiven
from openai.types import CompletionUsage

from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
Expand Down Expand Up @@ -65,6 +66,33 @@ def get_property_value(obj, property_name):
return getattr(obj, property_name, None)


def set_chat_usage_details(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token-detail extraction is duplicated between this helper and the stream wrapper. Please share extraction across both paths, buffering the values until stream cleanup.

Please also wait for #674 to land, then reuse its set_input_tokens() and set_output_tokens() helpers for modality breakdowns. Keep the aggregate cache-write count separate.

invocation: InferenceInvocation, usage: CompletionUsage
) -> None:
prompt_details: object = get_property_value(usage, "prompt_tokens_details")
completion_details: object = get_property_value(
usage, "completion_tokens_details"
)
invocation.cache_write_input_tokens = get_property_value(
prompt_details, "cache_write_tokens"
)
invocation.text_input_tokens = get_property_value(
prompt_details, "text_tokens"
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAI Chat Completions does not expose text_tokens or image_tokens in prompt_tokens_details or completion_tokens_details. Only cache_write_tokens and audio_tokens exist on PromptTokensDetails, and audio_tokens on CompletionTokensDetails. Please remove the non-existent modality mappings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invocation.image_input_tokens = get_property_value(
prompt_details, "image_tokens"
)
invocation.audio_input_tokens = get_property_value(
prompt_details, "audio_tokens"
)
invocation.text_output_tokens = get_property_value(
completion_details, "text_tokens"
)
invocation.audio_output_tokens = get_property_value(
completion_details, "audio_tokens"
)


def get_server_address_and_port(
client_instance,
) -> tuple[str | None, int | None]:
Expand Down
Loading
Loading