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
6 changes: 6 additions & 0 deletions util/opentelemetry-util-genai/CHANGELOG-loongsuite.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Add multimodal runtime config snapshot and generation-aware uploader hot-reload
(`MultimodalRuntimeConfig`, `update_multimodal_runtime_config`,
`get_or_rebuild_uploader_pair`).

## Version 0.7.0 (2026-07-03)

### Added
Expand Down
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add multimodal runtime config snapshot and generation-aware uploader hot-reload.
- Avoid import-time warnings when optional audio dependencies for PCM16-to-WAV conversion are not installed.

## Version 0.3b0 (2026-02-20)
Expand Down
25 changes: 25 additions & 0 deletions util/opentelemetry-util-genai/README-loongsuite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,31 @@ Framework 探针若已声明对本包的依赖,会随探针一并安装;单

**自定义上传实现**:通过 ``pyproject.toml`` 中的 entry point ``opentelemetry_genai_multimodal_uploader``、``opentelemetry_genai_multimodal_pre_uploader`` 注册实现;本仓库默认提供 ``fs`` hook(见包内 ``pyproject.toml``)。

**运行时动态配置**

进程内维护一份多模态配置 snapshot(``MultimodalRuntimeConfig`` / ``MultimodalConfigSnapshot``)。启动时仍由上述环境变量完成 bootstrap;之后可通过公开 API 热更新,无需重启进程:

::

from opentelemetry.util.genai._multimodal_upload import (
get_multimodal_config_snapshot,
update_multimodal_runtime_config,
)

update_multimodal_runtime_config(
upload_mode="both",
storage_base_path="file:///var/log/genai/multimodal",
uploader_hook_name="fs",
pre_uploader_hook_name="fs",
)
snapshot = get_multimodal_config_snapshot()

策略字段(如 ``upload_mode``、``download_enabled``、``local_file_enabled``、
``allowed_root_paths``)变更会递增 ``strategy_version``,立即作用于后续请求;
构造字段(如 ``storage_base_path``、uploader/pre-uploader hook 名)变更会递增
``uploader_generation``,并在下次上传前通过 ``get_or_rebuild_uploader_pair()``
热重建 uploader/pre-uploader。

------------------------------------------------------------------------
5. 补充说明
------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ def __init__(self, ...):
from opentelemetry.util.genai.utils import ( # pylint: disable=no-name-in-module
gen_ai_json_dumps,
get_content_capturing_mode,
get_multimodal_upload_mode,
is_experimental_mode,
)

Expand Down Expand Up @@ -140,30 +139,7 @@ class MultimodalProcessingMixin:

def _init_multimodal(self) -> None:
"""Initialize multimodal-related instance attributes, called in subclass __init__"""
self._multimodal_enabled = False

if get_multimodal_upload_mode() == "none":
return

try:
capture_enabled = (
is_experimental_mode()
and get_content_capturing_mode()
in (
ContentCapturingMode.SPAN_ONLY,
ContentCapturingMode.SPAN_AND_EVENT,
)
)
except ValueError:
# get_content_capturing_mode raises ValueError when GEN_AI stability mode is DEFAULT
capture_enabled = False

if not capture_enabled:
return

uploader, pre_uploader = self._get_uploader_and_pre_uploader()
if uploader is not None and pre_uploader is not None:
self._multimodal_enabled = True
self._multimodal_enabled = True

# ==================== Public Methods ====================

Expand Down Expand Up @@ -370,19 +346,42 @@ def _at_fork_reinit(cls) -> None:

# ==================== Internal Methods ====================

def _should_async_process(self, invocation: _MultimodalInvocation) -> bool:
"""Determine whether async processing is needed
@staticmethod
def _content_capture_supports_multimodal() -> bool:
try:
return is_experimental_mode() and get_content_capturing_mode() in (
ContentCapturingMode.SPAN_ONLY,
ContentCapturingMode.SPAN_AND_EVENT,
)
except Exception: # pylint: disable=broad-except
return False

Condition: Has multimodal data and multimodal upload switch is not 'none'
"""
def _should_async_process(self, invocation: _MultimodalInvocation) -> bool:
"""Determine whether async processing is needed."""
if not self._multimodal_enabled:
return False

return MultimodalProcessingMixin._quick_has_multimodal(invocation)
from opentelemetry.util.genai._multimodal_upload.config import ( # pylint: disable=import-outside-toplevel,no-name-in-module # noqa: PLC0415
get_multimodal_config_snapshot,
)

snapshot = get_multimodal_config_snapshot()
if snapshot.upload_mode == "none":
return False
if not self._content_capture_supports_multimodal():
return False
if not self._quick_has_multimodal_for_mode(invocation, snapshot):
return False
return True

@staticmethod
def _quick_has_multimodal(invocation: _MultimodalInvocation) -> bool:
"""Quick detection of multimodal data (O(n), no network)"""
def _quick_has_multimodal_for_mode(
invocation: _MultimodalInvocation,
snapshot: Any,
) -> bool:
"""Quick detection of multimodal data respecting upload_mode."""
check_input = snapshot.process_input
check_output = snapshot.process_output

def _check_messages(
messages: Optional[List[InputMessage] | List[OutputMessage]],
Expand All @@ -397,8 +396,22 @@ def _check_messages(
return True
return False

return _check_messages(invocation.input_messages) or _check_messages(
invocation.output_messages
if check_input and _check_messages(invocation.input_messages):
return True
if check_output and _check_messages(invocation.output_messages):
return True
return False

@staticmethod
def _quick_has_multimodal(invocation: _MultimodalInvocation) -> bool:
"""Quick detection of multimodal data (O(n), no network)"""
from opentelemetry.util.genai._multimodal_upload.config import ( # pylint: disable=import-outside-toplevel,no-name-in-module # noqa: PLC0415
get_multimodal_config_snapshot,
)

return MultimodalProcessingMixin._quick_has_multimodal_for_mode(
invocation,
get_multimodal_config_snapshot(),
)

@classmethod
Expand Down Expand Up @@ -751,10 +764,14 @@ def _get_uploader_and_pre_uploader( # pylint: disable=no-self-use
"""
try:
from opentelemetry.util.genai._multimodal_upload import ( # pylint: disable=import-outside-toplevel,no-name-in-module # noqa: PLC0415
get_or_load_uploader_pair,
get_or_rebuild_uploader_pair,
)
from opentelemetry.util.genai._multimodal_upload.config import ( # pylint: disable=import-outside-toplevel,no-name-in-module # noqa: PLC0415
get_multimodal_config_snapshot,
)

return get_or_load_uploader_pair()
snapshot = get_multimodal_config_snapshot()
return get_or_rebuild_uploader_pair(snapshot)
except ImportError:
return None, None

Expand All @@ -766,7 +783,14 @@ def _upload_and_set_metadata(
pre_uploader: "PreUploader",
) -> None:
"""Upload multimodal data and set metadata attributes on span"""
self._separate_and_upload(span, invocation, uploader, pre_uploader)
from opentelemetry.util.genai._multimodal_upload.config import ( # pylint: disable=import-outside-toplevel,no-name-in-module # noqa: PLC0415
get_multimodal_config_snapshot,
)

snapshot = get_multimodal_config_snapshot()
self._separate_and_upload(
span, invocation, uploader, pre_uploader, snapshot
)

input_metadata, output_metadata = (
MultimodalProcessingMixin._extract_multimodal_metadata(
Expand All @@ -790,6 +814,7 @@ def _separate_and_upload( # pylint: disable=no-self-use
invocation: _MultimodalInvocation,
uploader: "Uploader",
pre_uploader: "PreUploader",
config_snapshot: Any,
) -> None:
"""Separate multimodal data and submit for upload"""
try:
Expand All @@ -803,6 +828,7 @@ def _separate_and_upload( # pylint: disable=no-self-use
start_time_ns,
invocation.input_messages,
invocation.output_messages,
config_snapshot=config_snapshot,
)

for item in upload_items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
Uploader,
UploadItem,
)
from opentelemetry.util.genai._multimodal_upload.config import ( # pylint: disable=no-name-in-module
get_multimodal_config_snapshot,
update_multimodal_runtime_config,
)
from opentelemetry.util.genai._multimodal_upload.multimodal_upload_hook import ( # pylint: disable=no-name-in-module
get_or_load_pre_uploader,
get_or_load_uploader,
get_or_load_uploader_pair,
get_or_rebuild_uploader_pair,
get_pre_uploader,
get_uploader,
get_uploader_pair,
Expand Down Expand Up @@ -58,6 +63,9 @@
"get_or_load_uploader",
"get_or_load_pre_uploader",
"get_or_load_uploader_pair",
"get_or_rebuild_uploader_pair",
"get_multimodal_config_snapshot",
"update_multimodal_runtime_config",
"get_uploader",
"get_pre_uploader",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def pre_upload(
start_time_utc_nano: int,
input_messages: Optional[List[Any]],
output_messages: Optional[List[Any]],
config_snapshot: Optional[Any] = None,
) -> List[PreUploadItem]:
"""
Preprocess multimodal data in messages:
Expand All @@ -117,6 +118,8 @@ def pre_upload(
start_time_utc_nano: Start time in nanoseconds
input_messages: List of input messages (with .parts attribute), will be modified in-place
output_messages: List of output messages (with .parts attribute), will be modified in-place
config_snapshot: Optional runtime config snapshot; when omitted, implementation
may read process-wide multimodal config itself

Returns:
List of PreUploadItem to be uploaded, returns empty list on failure
Expand Down
Loading