feat(config): add WithHeader option for custom HTTP headers#65
feat(config): add WithHeader option for custom HTTP headers#65jrschumacher wants to merge 1 commit intomozilla-ai:mainfrom
Conversation
Add WithHeader(key, value) config option that injects custom HTTP headers on every request. This enables proxy/gateway authentication such as Cloudflare AI Gateway's cf-aig-authorization header. The implementation wraps the HTTP client's transport with a headerTransport that sets the configured headers before each request. This works transparently with all providers since they use cfg.HTTPClient() for their HTTP calls. Also updates the Anthropic provider to pass cfg.HTTPClient() to the SDK client, ensuring custom headers are applied to Anthropic requests as well (previously it used the SDK's default HTTP client). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e9c9344 to
a807465
Compare
Code reviewFound 3 issues:
Lines 184 to 196 in e9c9344
Lines 96 to 108 in e9c9344
Lines 172 to 199 in e9c9344 |
peteski22
left a comment
There was a problem hiding this comment.
Inline comments cover line-anchored issues. Non-anchored items:
Tests
- No end-to-end test. Spin up
httptest.NewServer, assert inbound headers match. Catches theRoundTripmutation bug and real injection regressions. - Missing test for duplicate-key semantics (
WithHeader("X","a"); WithHeader("X","b")). Documents overwrite vs append behavior — required ifHeadersswitches tohttp.Header.
Follow-ups (not blocking)
- Gateway provider on
mainhas its ownheaderTransport(providers/gateway/gateway.go:125-128,373-378). Migrate toconfig.WithHeaderin a follow-up to dedupe. - Platform provider builds its own
&http.Client{Timeout: 30s}and won't pick upWithHeader. Out of scope per gateway replacement.
Verified against e9c9344 + origin/main. Extends prior comment.
|
|
||
| func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| for key, value := range t.headers { | ||
| req.Header.Set(key, value) |
There was a problem hiding this comment.
RoundTrip mutates the caller's request. net/http contract: "RoundTrip should not modify the request, except for consuming and closing the Body." Gateway provider already does this correctly — clone first:
clone := req.Clone(req.Context())
for k, v := range t.headers {
clone.Header.Set(k, v)
}
return t.base.RoundTrip(clone)See providers/gateway/gateway.go:373-378 for the established pattern.
|
|
||
| clientOpts := []option.RequestOption{ | ||
| option.WithAPIKey(apiKey), | ||
| option.WithHTTPClient(cfg.HTTPClient()), |
There was a problem hiding this comment.
Correct fix, but silent behavior change. Before this PR the Anthropic SDK used its own default HTTP client (no timeout). Now cfg.Timeout applies — existing callers with Anthropic calls >120s (default) will start timing out. Please note in PR description + CHANGELOG.
| // Headers holds extra HTTP headers to include on every request. | ||
| // Useful for proxy authentication (e.g., Cloudflare AI Gateway's | ||
| // cf-aig-authorization header). | ||
| Headers map[string]string |
There was a problem hiding this comment.
map[string]string silently overwrites duplicate keys and can't represent multi-valued headers (RFC 7230 §3.2.2). Suggest http.Header to match stdlib, with WithHeader calling Add (append) or Set (overwrite) — pick one and document the semantic.
| } | ||
|
|
||
| // headerTransport is an http.RoundTripper that injects headers on every request. | ||
| type headerTransport struct { |
There was a problem hiding this comment.
Type belongs in the types section near Config/Option, not after HTTPClient(). Project CLAUDE.md File Organization: "Types (exported first, then unexported helpers)" precede constructor and methods.
| c.httpClient = &http.Client{Timeout: c.Timeout} | ||
| } | ||
|
|
||
| // Wrap the transport to inject custom headers on every request. |
There was a problem hiding this comment.
Narration — restates the next three lines. Drop, or replace with why (e.g. "applied transport-layer so header injection is provider-agnostic").
| } | ||
|
|
||
| require.NoError(t, err) | ||
| require.NotNil(t, cfg.Headers) |
There was a problem hiding this comment.
Only checks Headers != nil. Doesn't verify the trimmed key or value landed. Add:
require.Equal(t, expectedKey, /* resolved key */)
require.Equal(t, tc.value, cfg.Headers[expectedKey])| require.NotNil(t, client) | ||
|
|
||
| // Without headers, transport should NOT be a headerTransport. | ||
| _, ok := client.Transport.(*headerTransport) |
There was a problem hiding this comment.
Passes for the wrong reason. With no headers + no custom client, client.Transport is nil. Type assertion on nil returns ok=false, so this succeeds even if the wrapping logic were broken. Tighten:
require.Nil(t, client.Transport)|
@peteski22 Sounds good I'll take a look at these issues. |
Summary
WithHeader(key, value string)config option that injects custom HTTP headers on every provider requestheaderTransportthat sets configured headers before each requestcfg.HTTPClient()so custom headers apply to Anthropic requests tooWithHeaderfrom the rootanyllmpackageMotivation
Proxy/gateway services like Cloudflare AI Gateway require custom authentication headers (e.g.,
cf-aig-authorization) alongside the provider's own API key. Currently there's no way to inject custom headers without providing a fully custom HTTP client with a hand-rolled round-tripper.Usage
Works with all providers since they all use
cfg.HTTPClient()(Anthropic now does too, via this PR).Changes
config/config.goHeadersfield,WithHeaderoption,headerTransportround-tripper, updateHTTPClient()to wrap transport when headers are setconfig/config_test.goWithHeadervalidation, multiple headers, transport wrapping, custom client + headersproviders/anthropic/anthropic.gocfg.HTTPClient()to SDK client optionsanyllm.goWithHeaderTest plan
WithHeadervalidation, multiple headers, transport wrapping with default and custom clients, no-op when no headers setgo build ./...passes🤖 Generated with Claude Code