Skip to content

[FEATURE]: propagate W3C traceparent on outbound LLM HTTP requests #49038

Open

Description

@joshreini1

Feature hasn't been suggested before.

Describe the enhancement

OpenCode's outbound LLM HTTP requests do not carry a W3C traceparent header. When OpenCode is used behind a tracing-capable gateway or proxy, the gateway creates server-side spans for each LLM request, but these spans start a new trace because the inbound HTTP request has no trace context. Client-side and server-side spans end up as disjoint traces that can only be correlated by content and timing.

Suggested implementation

OpenCode already has full OTel infrastructure in packages/core/src/observability/otlp.ts: @opentelemetry/api, AsyncLocalStorageContextManager, and the OTLP HTTP trace exporter. The missing piece is injecting the active span context into outbound LLM requests.

Injection point: packages/llm/src/route/executor.ts, in the layer definition (~line 370). Before http.execute(request), inject the traceparent header from the active OTel context:

import { context, propagation } from "@opentelemetry/api"

// In executeOnce, before http.execute:
const carrier: Record<string, string> = {}
propagation.inject(context.active(), carrier)
const traced = Object.entries(carrier).reduce(
  (req, [k, v]) => HttpClientRequest.setHeader(req, k, v),
  request,
)
return yield* http.execute(traced)...

This requires adding @opentelemetry/api as a dependency to the llm package (it's already in the monorepo via packages/core).

Behavior when tracing is not configured: propagation.inject() is a no-op when no propagator is registered (no TracerProvider set up), so no headers are added. Zero behavioral change for users without tracing.

Verified end-to-end

Tested the equivalent fix (W3C traceparent injection via SDK middleware) against a live OTLP-capable gateway. With propagation, the gateway's server span becomes a child of the client's span in a single connected trace:

rag-pipeline (root)
|- retrieve (parent: rag-pipeline)
|- generate (parent: rag-pipeline)
|   \-- chat openai-gpt-5.4 [SERVER] (parent: generate)  <-- gateway span

Without it, the gateway span is in a separate trace.

Activity

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    This issue doesn't fully meet our contributing guidelines.

    What needs to be fixed:

    • This appears to be a feature request but does not use the required Feature Request template. The title should start with [FEATURE]: and the issue body should include the verification checkbox confirming the feature hasn't already been requested.

    Please edit this issue to address the above within 2 hours, or it will be automatically closed.


    This issue might be a duplicate of existing issues. Please check:

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. changed the title [-]feat: propagate W3C traceparent on outbound LLM HTTP requests[/-] [+][FEATURE]: propagate W3C traceparent on outbound LLM HTTP requests[/+] on Sep 14, 2026
  3. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    Thanks for updating the issue and following the contributing guidelines! Everything looks good now.

  4. joshreini1 commented on Sep 14, 2026

    @joshreini1
    Author

    End-to-end proof via AGENT_TRACE_TABLE

    Ran two requests against a live AI gateway (openai-gpt-5.4 via DEFAULT_GATEWAY), then queried TABLE(AGENT_TRACE_TABLE('DEFAULT_GATEWAY')):

    Test 1 (no traceparent): gateway server span gets its own trace_id, disconnected from any client trace.

    Test 2 (with traceparent 00-e2bc2acd...-620bb0e5...-01): gateway server span adopts the client trace context:

    trace=e2bc2acd791b48ff  span=620bb0e5fee04c14  parent=(root)           SPAN_KIND_CLIENT  opencode.chat.completions
    trace=e2bc2acd791b48ff  span=e036e58baf3757f2  parent=620bb0e5fee04c14  SPAN_KIND_SERVER  chat openai-gpt-5.4
    

    The server span's parent_span_id (620bb0e5fee04c14) matches the client span_id from the traceparent header. Both spans share the same trace_id. This is a single connected distributed trace.

    Without the traceparent header, the gateway's server spans start independent traces with no link back to the client. The PR fix (propagation.inject() in executor.ts) adds this header when OTel tracing is configured.

  5. joshreini1 commented on Sep 14, 2026

    @joshreini1
    Author

    Correction on the root cause

    My earlier comments pointed at packages/llm/src/route/executor.ts as the injection point. That was wrong, and testing a build with the change in place showed traces were still disconnected. Two things are actually going on:

    1. No propagator is registered. tracingLayer() in packages/core/src/observability/otlp.ts sets a global context manager but never sets a propagator. propagation.inject() falls back to a no-op propagator when none is registered, so it writes nothing to the carrier. Confirmed by logging the carrier during a live run: carrier={} on every request. This is the primary reason no trace headers are sent, and it would defeat an injection call placed anywhere.

    2. executor.ts is not on the request path. AI SDK providers issue their HTTP requests through the provider fetch wrapper in packages/opencode/src/provider/provider.ts (~line 1822), not the Effect HTTP client in packages/llm/src/route/executor.ts. Injection has to go in the fetch wrapper to cover providers.

    With a propagator registered and the injection in the fetch wrapper, the same live session produces:

    carrier={"traceparent":"00-eb775c51c6fdf39a427732baa9da8178-28eae3b144d52dad-01"}
    

    and the gateway server span lands in the client trace:

    ai.streamText.doStream       [INTERNAL]  opencode   span=28eae3b144d52dad
      └─ chat openai-gpt-5.4     [SERVER]    gateway    parent=28eae3b144d52dad
    

    PR #49046 has been updated with this approach: 2 files, 30 lines.

  6. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up! You're asking for OpenCode to add a W3C traceparent header to its outbound HTTP requests to LLM providers when tracing is configured. Then spans from a tracing gateway or proxy would join OpenCode's client trace instead of starting a new one.

    The file paths in the issue are out of date on the current dev branch. The request executor is now packages/ai/src/route/executor.ts, and the OTLP setup is packages/util/src/observability/otlp.ts. Effect's HttpClient, which that executor uses, already adds traceparent from the current span by default. So the gap is probably in providers that go through the AI SDK's own fetch path. If you can say which provider/model you tested with and which OpenCode version (opencode --version), that would help locate where the header is missing.

    Related issues:

    The maintainers will consider this as a feature request.

  7. siglp commented on Oct 30, 2026

    @siglp

    I found a remaining propagation issue on current dev. When request middleware supplies an explicit traceparent, Effect’s HTTP client overwrites it during send and also adds a B3 header from a different trace.

    I have a small fix that disables automatic tracer propagation only for requests already containing traceparent. Requests without an explicit parent retain native propagation. A wire-level regression test covers direct headers, middleware replacement, middleware injection, native propagation, and disabled propagation.

    Verification: 42 executor tests pass and the AI package typecheck passes.

      Effect.gen(function* () {
        const client = yield* HttpClient.HttpClient
        // Request middleware can supply an external trace parent. Effect otherwise
        // overwrites it (and adds B3 from a different trace) just before sending.
        const http = HttpClient.transform(client, (effect, request) =>
          Object.hasOwn(request.headers, "traceparent")
            ? effect.pipe(Effect.provideService(HttpClient.TracerPropagationEnabled, false))
            : effect,
        )
        const executeOnce = (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) =>

    Tested E2E on agentgateway 1.6.0 a and langfuse 4.71.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions