Skip to content

feat(genai): add pre-authorized OSS uploader for multimodal content - #264

Merged
sipercai merged 5 commits into
mainfrom
feat/genai-multimodal-presign-upload
Sep 7, 2026
Merged

feat(genai): add pre-authorized OSS uploader for multimodal content#264
sipercai merged 5 commits into
mainfrom
feat/genai-multimodal-presign-upload

Conversation

@123liuziming

Copy link
Copy Markdown
Collaborator

What

Adds a presign multimodal uploader to opentelemetry-util-genai. Instead of holding OSS credentials, the SDK asks the server for a short-lived presigned URL (authenticated with a license key) and then writes the object straight to the server-owned bucket.

Objects are addressed as sls://{project}/{logstore}[/{prefix}]/{objectName}. That address is fully resolvable locally, so span attributes are rewritten before the upload is even issued — the span never has to wait on the network.

Why

The existing uploader requires the application to hold OSS credentials. This mode removes that requirement, which matters for the SDK-only (manual instrumentation) case where there is no agent in the process to supply anything.

Configuration

Variable Purpose
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_UPLOADER=presign selects this uploader
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_ENDPOINT where presigned URLs are requested
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_LICENSE_KEY presign auth; falls back to ARMS_LICENSE_KEY
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_WORKSPACE falls back to ARMS_WORKSPACE
APSARA_APM_COLLECTOR_MULTIMODAL_SLS_PROJECT / ..._SLS_LOGSTORE object address
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_OSS_PATH_PREFIX shared object path prefix (new)

The dedicated PRESIGN_* variables take precedence, with a fallback to the ARMS_* ones so a co-located agent keeps working with no extra configuration. ..._OSS_BUCKET is ignored here, since the server decides which bucket backs the objects.

Failure behavior

If project, endpoint or license key cannot be resolved, both the uploader and the pre-uploader return None and the whole multimodal path degrades to disabled. The original content stays in the span rather than being replaced by a URI pointing at an object that was never written.

Manual instrumentation

Also included: examples/multimodal_presign_manual.py plus docs/manual-instrumentation.md covering the SDK-only flow.

Two configuration requirements there are easy to miss because neither fails loudly:

  • Content capturing needs both OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental and OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. The latter takes a ContentCapturingMode enum name (SPAN_ONLY / SPAN_AND_EVENT) — passing true is an invalid value and silently falls back to NO_CONTENT, producing a span with no message content and no upload at all.
  • The logstore must already exist; the default value will not.

The example validates its configuration up front so both surface as explicit errors instead of an empty span.

Testing

  • 222 passed, 1 skipped in tests/_multimodal_upload/
  • Verified end to end against a real presign endpoint: the presign request and the OSS PUT both returned 200, and the URI written to the span matched the uploaded object key segment for segment. Exercised through both with handler.llm(...) and the explicit start_llm / stop_llm APIs.
  • Negative paths checked: missing project, missing opt-in, and an invalid CAPTURE_MESSAGE_CONTENT value are each rejected by the example's config check rather than degrading silently.

Add the `presign` multimodal uploader: instead of holding OSS credentials,
the SDK asks the server for a short-lived presigned URL (authenticated with
a license key) and then writes the object straight to the server-owned
bucket. Objects are addressed as
`sls://{project}/{logstore}[/{prefix}]/{objectName}`, which is fully
resolvable locally, so span attributes can be rewritten before the upload
is even issued.

The identity used for the presign request can now come from dedicated
environment variables, so an application using the SDK standalone no
longer needs to run under an agent:

  OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_LICENSE_KEY
  OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_WORKSPACE

Both fall back to ARMS_LICENSE_KEY / ARMS_WORKSPACE so a co-located agent
keeps working without extra configuration.

Also add `OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_OSS_PATH_PREFIX` to give an
application a shared object path prefix. `..._OSS_BUCKET` is ignored in
this mode because the server decides which bucket backs the objects.

When the project, endpoint or license key cannot be resolved, the uploader
and pre-uploader both return None and the whole multimodal path degrades to
disabled, leaving the original content in place rather than pointing at an
object that was never written.

Ship a manual-instrumentation example and document the SDK-only flow. Two
configuration requirements are easy to miss there, since neither fails
loudly: content capturing needs both OTEL_SEMCONV_STABILITY_OPT_IN and
CAPTURE_MESSAGE_CONTENT (the latter takes a ContentCapturingMode enum name,
not a boolean), and the logstore must already exist. The example validates
its configuration up front so these surface as errors instead of an empty
span.

Verified end to end against a real presign endpoint: presign request and
OSS PUT both returned 200, and the URI written to the span matched the
uploaded object key segment for segment, using both the context-manager
and the explicit start/stop APIs.

Change-Id: I0308e9e0ea5887c9d73fbd3c7b49c0d3c1f49429
Co-developed-by: Qoder <noreply@qoder.com>
Copilot AI lite review requested due to automatic review settings September 4, 2026 06:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Adds a new “presign” multimodal uploader that requests short-lived presigned URLs from an ARMS endpoint (authenticated via license key) and uploads directly to a server-owned OSS bucket, avoiding local OSS credentials.

Changes:

  • Introduces MultimodalPresignClient + PresignUploader and wires them as entry points (presign) for uploader and pre-uploader.
  • Extends runtime/env configuration to support presign endpoint, license/workspace fallback behavior, and a shared OSS path prefix.
  • Adds comprehensive tests plus SDK-only manual instrumentation docs and an executable example.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_client.py New presign API client: endpoint resolution, payload construction, response parsing, error classification
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py New uploader: bounded async queue, presign+PUT retry loop, optional download of source URIs
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py Adds presign hook support, env snapshot fields, SLS base-path formatting helper, prefix/bucket normalization
util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_environment_variables.py Documents new env vars for presign endpoint/auth/timeout and shared object path prefix
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/init.py Exposes presign hook constants/types in package exports
util/opentelemetry-util-genai/pyproject.toml Registers presign as entry points for uploader + pre-uploader
util/opentelemetry-util-genai/tests/_multimodal_upload/test_presign_uploader.py Adds presign-specific unit tests across client, uploader, hooks, and config normalization
util/opentelemetry-util-genai/examples/multimodal_presign_manual.py Adds SDK-only runnable example that validates env/config before running
util/opentelemetry-util-genai/docs/manual-instrumentation.md Adds manual instrumentation guide including presign mode wiring and pitfalls
util/opentelemetry-util-genai/README-loongsuite.rst Documents presign mode behavior, env vars, and operational expectations

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The example only put an image on the output side, which left the
impression that uploads apply to model output alone. Both sides are
uploaded, so give the input message an image part too — now the request
carries text plus an image (a visual question) and the two sides use
different images, making it visible that these are two independent
uploads.

Fix a wrong hint while here: it described `span_only` as a valid
upload_mode. The valid values are none / input / output / both, and
anything else is silently normalized to none, so following that hint
would have disabled uploading altogether. `span_only` belongs to
CAPTURE_MESSAGE_CONTENT, not here.

Since upload_mode also decides *which* side is processed, the example now
reads the library's own config snapshot and rejects a mode that does not
cover both sides, rather than letting one side quietly stay as base64.

Verified against a real presign endpoint: two presign requests and two OSS
PUTs all returned 200, `gen_ai.input.messages` and
`gen_ai.output.messages` each carried a different `sls://` URI matching
its uploaded object key, and the text part was left untouched. Modes
`input`, `output` and `span_only` are each rejected up front.

Change-Id: I0bad5743eb5294cf9c63171cd1df19dbf63f5774
Co-developed-by: Qoder <noreply@qoder.com>
Copilot AI review requested due to automatic review settings September 4, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Change-Id: If110a24f6f5433db5d8e249c4774c23d4279cba3
Copilot AI review requested due to automatic review settings September 7, 2026 09:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Change-Id: I8303f0abe27d7495d46919d5e8aec70ce29cdf55
Copilot AI review requested due to automatic review settings September 7, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new presign uploader path parsing/validation has a few concrete edge cases (invalid project and URI variants) that should be tightened to avoid accepting malformed sls:// addresses and endpoints.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py:373

  • presign_endpoint is read from the environment without trimming whitespace. This is inconsistent with _first_env (used for license key/workspace) and can make a seemingly-valid endpoint fail readiness checks or generate invalid request URLs.

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py:245

  • When UploadItem.url uses an sls://... URI, _object_key_from_url currently ignores URL params/query/fragment and doesn't verify the parsed scheme; this can allow multiple textual variants of the same object key (or malformed URIs) to be treated as valid.
        if target.startswith("sls://"):
            parsed = urlparse(target)
            logstore, _, remainder = parsed.path.lstrip("/").partition("/")
            if parsed.netloc != self._project or logstore != self._logstore:
                raise PresignUploadConfigError(
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

Change-Id: I9b0faf8110473d91e78962b1d8ed7ee8bcb9010e
Copilot AI review requested due to automatic review settings September 7, 2026 12:58
@sipercai
sipercai merged commit 24096a5 into main Sep 7, 2026
214 of 233 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new presign client/uploader have confirmed httpx response lifecycle issues (responses not closed on error/success paths) that can exhaust connection pools in production.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py:364

  • presign_endpoint is read from the environment without trimming whitespace. A value like ' https://example.com ' will be treated as non-empty but later produce invalid URLs; it should be normalized (at least .strip()) at snapshot creation time to match how other string fields are handled.
    util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_client.py:352
  • HTTPX responses should be closed to return the connection to the pool. In the current implementation, non-success presign responses raise without reading/closing the body, which can leak connections under load.
    util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py:522
  • HTTPX responses should be closed to release the connection back to the pool. _put_presigned currently returns/raises without closing the response on both success and error paths, which can exhaust the connection pool under retries or high throughput.
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +735 to +739
ssl_verify = os.environ.get(
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_DOWNLOAD_SSL_VERIFY,
"true",
).lower() not in ("false", "0", "no")
# Require readiness before enabling URI rewriting. Keep resolving on each
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants