Skip to content

options.WithHTTPTransportWrapper: allow callers to inject HTTP middleware around all provider clients #3089

Description

@simonferquel-clanker

Problem

When embedding docker-agent programmatically (via anthropic.NewClient, teamloader.Load, etc.), there is currently no way to inject custom HTTP middleware into the transport chain used by provider clients — neither in direct mode nor in gateway/proxy mode.

Both the Anthropic and OpenAI provider clients build their HTTP client internally:

// direct path (anthropic/client.go)
option.WithHTTPClient(httpclient.NewHTTPClient(ctx))

// gateway path (anthropic/client.go)
option.WithHTTPClient(httpclient.NewHTTPClient(ctx, httpOptions...))

The options.ModelOptions struct and config.RuntimeConfig carry no transport field, so there is no place to hook in.

Use cases

  • Bearer-token injection for a private LLM proxy: a private reverse proxy requires Authorization: Bearer <token> on every request. The token is known at client-construction time but must be injected regardless of whether the provider is in direct or gateway mode.
  • Custom observability / tracing: adding spans or metrics around every LLM HTTP call, beyond what OTel wrapping already provides.
  • Request/response logging and debugging: inspect raw HTTP traffic during development.
  • Retry/circuit-breaker policies: wrap the transport with custom retry logic specific to the deployment.

All of these share the same shape: wrap whatever transport docker-agent would build, apply some middleware, and delegate to the original.

Proposed API

Add a new Opt to the options package:

// WithHTTPTransportWrapper registers a function that wraps the HTTP transport
// used by all provider clients. The function receives the transport that
// docker-agent built (including OTel instrumentation, SSE decompression fix,
// and Desktop proxy support) and must return a new RoundTripper that delegates
// to it. The wrapper is applied in both direct mode and gateway/proxy mode.
//
// Example — inject a bearer token on every outbound LLM request:
//
//	options.WithHTTPTransportWrapper(func(base http.RoundTripper) http.RoundTripper {
//	    return &bearerTransport{token: myToken, base: base}
//	})
func WithHTTPTransportWrapper(fn func(base http.RoundTripper) http.RoundTripper) Opt {
	return func(cfg *ModelOptions) {
		cfg.transportWrapper = fn
	}
}

And surface it via ModelOptions:

func (o *ModelOptions) TransportWrapper() func(http.RoundTripper) http.RoundTripper {
	return o.transportWrapper
}

Implementation sketch (Anthropic provider, direct path)

httpClient := httpclient.NewHTTPClient(ctx)
if wrapper := globalOptions.TransportWrapper(); wrapper != nil {
	httpClient.Transport = wrapper(httpClient.Transport)
}
requestOptions := []option.RequestOption{
	option.WithHTTPClient(httpClient),
	...
}

Same two-liner applied to the gateway path. The same pattern applies symmetrically to the OpenAI provider (and any future providers that use httpclient.NewHTTPClient).

Notes

  • The wrapper function receives the already-built transport (post-OTel, post-SSE-filter, post-Desktop-proxy), so callers compose on top of docker-agent's chain rather than replacing it.
  • A single WithHTTPTransportWrapper call is the common case; stacking multiple wrappers is possible by chaining the functions before passing them in.
  • This is intentionally lower-level than WithGateway — it targets use cases where callers manage their own transport-level concerns that docker-agent has no opinion on.

Metadata

Metadata

Labels

area/apiFor features/issues/fixes related to the usage of the cagent APIarea/providersFor features/issues/fixes related to LLM providers (Bedrock, LiteLLM, Qwen, custom, etc.)area/providers/anthropicFor features/issues/fixes related to the usage of Anthropic modelsarea/providers/openaiFor features/issues/fixes related to the usage of OpenAI models

Fields

No fields configured for Enhancement.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions