feat(headers): per-provider custom upstream and passthrough header overrides#513
feat(headers): per-provider custom upstream and passthrough header overrides#513weselben wants to merge 23 commits into
Conversation
Introduce a HeaderOverrides block on each provider that controls how ingress headers are forwarded upstream. Providers can now declare a blocklist for sensitive headers, explicit add/replace rules, and a list of headers to remove before the request leaves the gateway. This commit is schema-only: the runtime engine and middleware wiring are added in the following commits.
Implement the runtime header override engine and wire it into the proxy path. The engine applies provider-specific block, add, replace, and remove rules to ingress headers before forwarding them upstream. Provider implementations (OpenAI-compatible, OpenRouter, VLLM, and Ollama) now use the engine, and the server passthrough middleware exposes the resolved header set to downstream handlers.
Add unit tests for the header override engine, provider config/factory wiring, and passthrough middleware. Includes coverage for the OpenAI-compatible, OpenRouter, VLLM, Ollama, DeepSeek, Groq, Kimi Code, and xAI provider paths as well as server request snapshot handling.
Add tests for provider-level header override configuration parsing in config/providers_test.go and extend config/config_test.go to cover the new schema fields end-to-end.
Add a feature guide for per-provider custom upstream headers, including blocklist, add, replace, and remove examples. Update config.example.yaml, .env.template, and CLAUDE.md so users can discover and copy the new configuration.
The overlap warning is already fully exercised by TestApplyHeaderOverrides_PassthroughMode_LogsOverlappingStaticHeaders via a dedicated capturingSlogHandler. captureLogger was never called, so remove it and the now-unused bytes import to satisfy golangci-lint's unused check.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds per-provider header override support, filtered passthrough header capture, user-path header wiring, adapter updates, OpenRouter identity precedence, and related configuration and documentation changes. ChangesHeader Overrides Feature
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant EchoMiddleware
participant RequestSnapshotCapture
participant ProviderFactory
participant Provider
participant UpstreamAPI
Client->>EchoMiddleware: HTTP request
EchoMiddleware->>RequestSnapshotCapture: capture snapshot context
RequestSnapshotCapture->>RequestSnapshotCapture: filter passthrough headers
EchoMiddleware->>ProviderFactory: create provider with header settings
ProviderFactory->>Provider: pass HeaderOverrides and UserPathHeader
Client->>Provider: ChatCompletion / Passthrough
Provider->>UpstreamAPI: forward merged headers
UpstreamAPI-->>Client: response
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The example comments nested the header fields under a header_overrides block and used the shorthand skip_mode/skip_headers names, neither of which the parser accepts. Flatten the keys to the real schema (passthrough_user_headers, passthrough_user_headers_skip_mode, passthrough_user_headers_skip, custom_upstream_headers) in all three example providers (openai, groq, deepseek), and correct the stale header_overrides wording in .env.template.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/providers/init.go (1)
219-272: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMove the passthrough flag behind successful provider creation
anyPassthroughUserHeadersis set beforefactory.Create, so a config withpassthrough_user_headers: truestill turns on header capture even if that provider fails to initialize and never becomes active. Move the check afterCreatesucceeds.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/providers/init.go` around lines 219 - 272, The passthrough-user-headers flag is being recorded before provider creation succeeds, so failed providers can still enable header capture. In initializeProviders, move the HeaderOverrides.PassthroughUserHeaders check to after factory.Create returns successfully, and only then update anyPassthroughUserHeaders for the provider that actually initialized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/config.example.yaml`:
- Around line 269-279: The provider examples use a nested header_overrides block
with skip_mode and skip_headers, but RawProviderConfig expects flat top-level
fields instead. Update the example YAML in the openai, groq, and deepseek
sections to match the actual schema by moving the header fields to the top level
and using the real field names from config/providers.go
(custom_upstream_headers, passthrough_user_headers,
passthrough_user_headers_skip, passthrough_user_headers_skip_mode). Keep the
examples aligned with TestRawProviderConfig_YAMLHeaderFields so uncommenting
them produces the intended header behavior.
In `@internal/providers/config.go`:
- Around line 590-616: The comma-splitting in parseHeaderMapEnv cannot safely
represent header values that contain commas, so update the handling in
parseHeaderMapEnv/config parsing to use a safer format or separator (for example
JSON or another unambiguous encoding) and document the accepted format clearly
near the CUSTOM_UPSTREAM_HEADERS path. Make sure the new behavior is located
through parseHeaderMapEnv and any callers in config.go so operators don’t
silently get malformed header maps when values include commas.
- Around line 575-588: The parseBoolEnv helper currently fails open by treating
any unrecognized value as true, which can silently enable PassthroughUserHeaders
on bad env input. Update parseBoolEnv to use an explicit true-set and return
false or nil for anything else, so only clearly recognized enable values turn
the flag on. Keep the fix localized to parseBoolEnv in
internal/providers/config.go and preserve its existing trimming/lowercasing
behavior.
In `@internal/providers/headers.go`:
- Around line 169-204: Update the docstring for shouldForward to match the
implemented behavior: the current “Default-closed” wording is stale and
contradicts the default-open logic in the switch for mode "skip" and "" when
skipSet is empty. Rewrite the comment above shouldForward (and the nearby mode
handling note if needed) so it clearly states that empty skip sets forward all
headers except those blocked by IsHeaderBlocked or stripSet, and keep the
description consistent with shouldForward and
TestApplyHeaderOverrides_PassthroughMode_DefaultOpenSkipList.
- Around line 105-122: Update logStaticOverridden in
internal/providers/headers.go so it only warns for passthrough headers that
would actually be forwarded upstream after SkipMode filtering. Instead of
checking overlap against the raw PassthroughHeadersFromContext source alone, run
each candidate through the same shouldForward / allow-skip logic used when
building req.Header and only include names that would survive that filter. Keep
the warning and overlapping_names output, but base both on the effective
forwarded headers rather than the unfiltered source.
In `@internal/providers/kimicode/kimicode_headers_test.go`:
- Around line 55-83: TestNew_AppliesUserPathHeader only proves a request was
sent and does not verify the UserPathHeader behavior. Update the test to capture
request headers in the httptest handler, send a passthrough/user-path header
through the ChatCompletion call, and assert that the configured alias
X-Tenant-Path is not forwarded while the request still succeeds. Use the New
constructor, ProviderOptions.UserPathHeader, and provider.ChatCompletion to
locate the behavior.
In `@internal/providers/xai/xai_test.go`:
- Around line 489-499: The X-Custom-Header assertion in xai_test.go is
conditionally skipped when wantCustom is empty, so the default-case test does
not verify the header is absent. Update the header assertions in the relevant
test case to always check gotHeaders.Get("X-Custom-Header") against the expected
value, using the existing tc.wantCustom field and the surrounding gotHeaders
checks for X-Tenant-Path and X-User-Header as the reference points. Ensure the
no-options/default scenario explicitly expects an empty X-Custom-Header value so
stray headers are caught.
In `@internal/server/http.go`:
- Around line 289-293: The middleware wiring in the HTTP server is
double-processing passthrough headers because `RequestSnapshotCapture` is
already followed by `PassthroughHeaderCapture` when
`cfg.PassthroughUserHeadersEnabled` is true. Update the setup around
`e.Use(...)` in `http.go` so `RequestSnapshotCapture` is always called with
passthrough capture disabled, since `PassthroughHeaderCapture` already performs
the needed filtering and context overwrite for both ingress-managed and `/p/*`
routes. Keep the change localized to this middleware registration logic and
preserve the existing `RequestSnapshotCapture` and `PassthroughHeaderCapture`
behavior elsewhere.
---
Outside diff comments:
In `@internal/providers/init.go`:
- Around line 219-272: The passthrough-user-headers flag is being recorded
before provider creation succeeds, so failed providers can still enable header
capture. In initializeProviders, move the HeaderOverrides.PassthroughUserHeaders
check to after factory.Create returns successfully, and only then update
anyPassthroughUserHeaders for the provider that actually initialized.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f9b0ed2b-e465-467d-91a2-6b2f914eae92
📒 Files selected for processing (38)
.env.templateCLAUDE.mdconfig/config.example.yamlconfig/config_test.goconfig/providers.goconfig/providers_test.godocs/features/custom-headers.mdxinternal/app/app.gointernal/providers/config.gointernal/providers/config_test.gointernal/providers/deepseek/deepseek_headers_test.gointernal/providers/deepseek/deepseek_test.gointernal/providers/factory.gointernal/providers/factory_test.gointernal/providers/groq/groq_test.gointernal/providers/headers.gointernal/providers/headers_test.gointernal/providers/init.gointernal/providers/init_test.gointernal/providers/kimicode/kimicode_headers_test.gointernal/providers/ollama/ollama.gointernal/providers/ollama/ollama_test.gointernal/providers/openai/compatible_provider.gointernal/providers/openai/compatible_provider_test.gointernal/providers/openrouter/openrouter.gointernal/providers/openrouter/openrouter_test.gointernal/providers/vllm/vllm.gointernal/providers/vllm/vllm_test.gointernal/providers/xai/xai_test.gointernal/server/auth_test.gointernal/server/http.gointernal/server/model_validation_test.gointernal/server/passthrough_headers.gointernal/server/passthrough_headers_test.gointernal/server/passthrough_semantic_enrichment_test.gointernal/server/request_snapshot.gointernal/server/request_snapshot_test.gorun/lifecycle_test.go
…ling - parseBoolEnv now requires an explicit true-set; any unrecognized value defaults to false so typos cannot silently enable passthrough. - logStaticOverridden only warns for headers that survive the same skip/allow and strip-set filtering applied during forwarding. - anyPassthroughUserHeaders is only set after factory.Create succeeds, so failed providers cannot enable the middleware. - Corrected the stale "Default-closed" docstring on shouldForward to reflect the actual default-open skip-mode behavior.
…ough capture - TestNew_AppliesUserPathHeader now captures request headers and asserts the configured alias is blocked and user headers are not forwarded by default. - xai_test no longer conditionally skips the X-Custom-Header assertion, so the default case explicitly verifies absence. - RequestSnapshotCapture in server setup is always called with capturePassthroughHeaders=false; PassthroughHeaderCapture already handles the filtering and context overwrite, avoiding double work on the hot path.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/providers/config.go (1)
580-590: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winFail-closed fix looks correct; consider logging unrecognized values.
The explicit true-set plus
default → falseresolves the earlier fail-open concern. One residual gap: unrecognized values (e.g."tru","enalbed") are now silently coerced tofalse, so a fat-fingered value that was meant to enable passthrough disappears with no operator feedback. A warn log would surface the misconfiguration without changing the safe default.🪵 Optional: warn on unrecognized boolean env values
default: + slog.Warn("unrecognized boolean env value; defaulting to false", "value", value) b := false return &b }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/providers/config.go` around lines 580 - 590, The boolean parsing in the config helper now fails closed, but unrecognized inputs are silently coerced to false. Update the parsing logic around the boolean env/value helper in config.go to emit a warning when the normalized value does not match the known true/false sets, while still returning false by default. Keep the existing behavior in the parsing function that handles trimmed values, and use the existing logger/config context so operators can spot typos like “tru” or “enalbed” without changing the safe default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@internal/providers/config.go`:
- Around line 580-590: The boolean parsing in the config helper now fails
closed, but unrecognized inputs are silently coerced to false. Update the
parsing logic around the boolean env/value helper in config.go to emit a warning
when the normalized value does not match the known true/false sets, while still
returning false by default. Keep the existing behavior in the parsing function
that handles trimmed values, and use the existing logger/config context so
operators can spot typos like “tru” or “enalbed” without changing the safe
default.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 07bb5caf-ed5e-40b4-ba9f-e6253215baa2
📒 Files selected for processing (6)
internal/providers/config.gointernal/providers/headers.gointernal/providers/init.gointernal/providers/kimicode/kimicode_headers_test.gointernal/providers/xai/xai_test.gointernal/server/http.go
…eader overrides After resolving the earlier CodeRabbit review items, Greptile flagged two maintainability issues in the header override implementation that would leave misleading docs and dead parameters in the PR: - `internal/providers/headers.go`: `shouldForward` had a stale docstring that described the opposite forwarding behavior and a dead `case "only"` branch. `"only"` is rejected at config validation time, so it was unreachable. The function is now simplified to an if-chain and the docstring accurately reflects the default-open behavior. - `internal/server/request_snapshot.go`: `RequestSnapshotCapture` still carried a `capturePassthroughHeaders` bool and a conditional block that captured passthrough headers. On ingress-managed paths this value is always overwritten by the later `PassthroughHeaderCapture` middleware; on non-ingress-managed paths the block is never reached. Removed the parameter and block, updated every caller and test, and dropped the test that exercised the removed code path. All changes are within the existing PR's changed-file set and the package test suite (`go test ./internal/providers/... ./internal/server/... ./config/...`) passes.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…r overrides coverage - Extend config_test.go with coverage for parseBoolEnv, parseHeaderMapEnv, and collectProviderEnvValues header overrides - Extend headers_test.go with coverage for PassthroughHeadersFromContext, sourceHasHeader, logStaticOverridden, and applyPassthroughHeaders edge cases - Add unsuffixed, suffixed, and ambiguous-provider env overlay tests for header override fields - Integrate all coverage into existing test files instead of creating standalone test files
… skip mode - Add TestInit_PropagateInvalidHeaderOverridesSkipMode to verify buildProviderConfig error is returned through Init - Covers the error return path added by header overrides in both init.go and config.go
- Add TestServer_RegistersPassthroughHeaderCaptureWhenEnabled to ensure PassthroughUserHeadersEnabled causes the ingress capture middleware to run - Covers the server.New registration path added by the header overrides feature
- Add TestNew_AppliesHeaderOverrideWiring to verify SetUserPathHeader is called - Verify AnyPassthroughUserHeaders propagates from provider init to app state - Uses minimal config with SQLite temp file and disabled storage-backed features
|
@coderabbitai review |
✅ Action performedReview finished.
|
…r overrides - Add TestInitializeProviders_PassthroughUserHeadersNotSetOnFailedProvider to guard the CodeRabbit finding that the passthrough flag must only be set after provider creation succeeds. - Add TestProviderEnvValues_RawConfig_PassthroughUserHeaders and TestProviderEnvValues_Empty_HeaderFields to cover the missing line and partial branch Codecov reported in internal/providers/config.go.
The passthrough flag is already set after Commit: @coderabbitai resume |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
sed -n '190,280p' internal/providers/init.goLength of output: 3092 Confirmed — the ✅ Action performedReviews resumed. |
|
Converting to draft to fix a bug with the OpenRouter implementation. It currently overwrites headers even when 'User Headers Passthrough' is enabled. Note: Other providers' implementations are working perfectly based on my final manual E2E tests. |
…enRouter passthrough overwrite
…header hierarchy - Rewrite the custom-headers page to match the concise style of other docs/features/*.mdx files. - Add a four-layer header hierarchy table: auth → provider defaults → static custom headers → passthrough user headers. - Document provider default headers using OpenRouter's HTTP-Referer and X-OpenRouter-Title as the concrete example. - Explain that skip lists filter caller headers only and do not strip provider defaults, and how to override defaults with custom_upstream_headers or passthrough headers.
| // Tagging "do not pass" headers are treated as blocked (Santiago #2). | ||
| if stripSet != nil { | ||
| if _, blocked := stripSet[lower]; blocked { | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
Normalize strip keys
TaggingStripHeadersFromContext stores canonical header names such as X-Team, but this lookup uses the lower-cased request header (x-team). When a tagging rule marks X-Team as do_not_pass, shouldForward misses the strip entry and forwards that header upstream, defeating the configured strip policy.
Context Used: CLAUDE.md (source)
Artifacts
Repro: focused Go test source demonstrating canonical tagging strip bypass
- Contains supporting evidence from the run (text/x-go; charset=utf-8).
Repro: verbose go test output showing X-Team forwarded despite strip policy
- Keeps the command output available without making the summary code-heavy.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.env.template (1)
12-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win"Same header layer" claim contradicts the four-layer hierarchy.
The comment says identity headers from
OPENROUTER_SITE_URL/OPENROUTER_APP_NAMEare "in the same header layer ascustom_upstream_headers." However, per theCompatibleProviderConfig.DefaultHeadersfield comment and the four-layer precedence table indocs/features/custom-headers.mdx(lines 55-65),DefaultHeadersare layer 2 (provider defaults) whilecustom_upstream_headersare layer 3 (static overrides). They are in different layers, which is why static overrides take precedence. The precedence statement itself is correct, but "same header layer" is internally inconsistent with it and misleading.📝 Suggested wording fix
# OpenRouter-specific: OPENROUTER_SITE_URL and OPENROUTER_APP_NAME also set # identity headers (HTTP-Referer and X-OpenRouter-Title) upstream. These are -# in the same header layer as custom_upstream_headers, so static config values -# and caller passthrough headers take precedence over these env vars. +# in the provider-default header layer (below custom_upstream_headers), so +# static config values and caller passthrough headers take precedence over +# these env vars.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.template around lines 12 - 15, Update the OpenRouter comment in .env.template so it no longer says the identity headers are “in the same header layer as custom_upstream_headers.” Align the wording with the four-layer precedence model used by CompatibleProviderConfig.DefaultHeaders and custom_upstream_headers: make clear that OPENROUTER_SITE_URL and OPENROUTER_APP_NAME map to provider default headers, while custom_upstream_headers are a higher-precedence override layer, and keep the existing precedence explanation consistent with that distinction.config/providers.go (1)
26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider documenting the new header-override fields.
CustomUpstreamHeaders,PassthroughUserHeaders,PassthroughUserHeadersSkip, andPassthroughUserHeadersSkipModehave no doc comments, unlikeAPIKeysabove them. A short comment (e.g., validskip/allowvalues forSkipMode, default behavior) would help operators reading this struct directly, mirroring the existingAPIKeysconvention.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/providers.go` around lines 26 - 29, The new header-override fields in the provider config struct are undocumented, unlike the existing APIKeys field, so add concise doc comments for CustomUpstreamHeaders, PassthroughUserHeaders, PassthroughUserHeadersSkip, and PassthroughUserHeadersSkipMode. Describe the intended behavior and defaults, especially how passthrough is enabled/disabled, what the skip list applies to, and the valid SkipMode values such as allow and skip, so readers can understand the ProviderConfig fields directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CLAUDE.md`:
- Line 136: The provider header env var docs in CLAUDE.md omit the
always-blocked header behavior, so add a short callout near the existing
CUSTOM_UPSTREAM_HEADERS/PASSTHROUGH_* description that certain credentials,
hop-by-hop/transport headers, and the user-path alias are never forwarded even
when configured. Reference the existing Custom Headers doc and make it explicit
that values like Authorization may be silently dropped by the blocklist so
operators know where to look.
In `@docs/features/custom-headers.mdx`:
- Around line 109-119: Update the OpenRouter scenario table in
custom-headers.mdx to reflect the default values returned by identityHeaders()
when one env var is unset. The current `—` entries are incorrect; in the `Only
OPENROUTER_SITE_URL set` row, `X-OpenRouter-Title` should show the default app
name, and in the `Only OPENROUTER_APP_NAME set` row, `HTTP-Referer` should show
the default site URL. Keep the rest of the `custom_upstream_headers` and
`passthrough_user_headers_skip` examples unchanged.
In `@internal/providers/headers_test.go`:
- Around line 776-794: The test case in HeaderOverridesConfig is not proving
that defaults are preserved when static headers do not override them, because
X-Default is redundantly set in both DefaultHeaders and CustomUpstreamHeaders.
Update the "static overrides defaults" case in
internal/providers/headers_test.go so X-Default is supplied only by
DefaultHeaders and verify the expected result still includes it, using the same
HeaderOverridesConfig and want map assertions.
In `@internal/providers/openai/compatible_provider.go`:
- Around line 121-138: Add a unit test covering the branch in buildHeaderMutator
where cfg.HeaderOverrides.DefaultHeaders is already populated so the guard
len(overrides.DefaultHeaders) == 0 prevents cfg.DefaultHeaders from overwriting
operator-supplied values. Exercise this through NewCompatibleProvider or
buildHeaderMutator in internal/providers/openai/compatible_provider.go, and
assert the resulting request headers preserve the explicit
HeaderOverrides.DefaultHeaders precedence described by the doc comment.
- Around line 96-100: The comment about per-request key rotation is stale in the
provider setup and is attached to the wrong assignment block. Move that note to
the key-selection logic in buildHeaderMutator, where keys.Next() actually
happens, or remove/reword it from the cfg.HeaderOverrides and cfg.UserPathAlias
section in compatible_provider.go so the documentation matches the code path.
---
Outside diff comments:
In @.env.template:
- Around line 12-15: Update the OpenRouter comment in .env.template so it no
longer says the identity headers are “in the same header layer as
custom_upstream_headers.” Align the wording with the four-layer precedence model
used by CompatibleProviderConfig.DefaultHeaders and custom_upstream_headers:
make clear that OPENROUTER_SITE_URL and OPENROUTER_APP_NAME map to provider
default headers, while custom_upstream_headers are a higher-precedence override
layer, and keep the existing precedence explanation consistent with that
distinction.
In `@config/providers.go`:
- Around line 26-29: The new header-override fields in the provider config
struct are undocumented, unlike the existing APIKeys field, so add concise doc
comments for CustomUpstreamHeaders, PassthroughUserHeaders,
PassthroughUserHeadersSkip, and PassthroughUserHeadersSkipMode. Describe the
intended behavior and defaults, especially how passthrough is enabled/disabled,
what the skip list applies to, and the valid SkipMode values such as allow and
skip, so readers can understand the ProviderConfig fields directly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f8dfac1e-7239-424e-bd89-ed9aaa23d29f
📒 Files selected for processing (19)
.env.templateCLAUDE.mdconfig/config.example.yamlconfig/providers.godocs/features/custom-headers.mdxinternal/providers/config.gointernal/providers/factory.gointernal/providers/headers.gointernal/providers/headers_test.gointernal/providers/keyring_config_test.gointernal/providers/ollama/ollama.gointernal/providers/ollama/ollama_test.gointernal/providers/openai/compatible_provider.gointernal/providers/openrouter/openrouter.gointernal/providers/openrouter/openrouter_test.gointernal/providers/vllm/vllm.gointernal/providers/xai/xai_test.gointernal/server/http_test.gorun/lifecycle_test.go
- Add test coverage for buildHeaderMutator DefaultHeaders promotion - Add test asserting operator-supplied DefaultHeaders replace provider defaults - Fix headers_test.go static-overrides-defaults case to prove defaults survive - Move stale per-request key rotation comment into buildHeaderMutator - Add blocked-header callout to CLAUDE.md provider header env var docs - Correct OpenRouter scenario defaults in custom-headers.mdx - Clarify OpenRouter default header layer in .env.template - Add doc comments for header override fields in config/providers.go
…ultHeaders precedence shouldForward lower-cased the header name before looking it up in the tagging strip set, but TaggingStripHeadersFromContext stores canonical names (e.g. "X-Team"). This caused do-not-pass tagging headers to be forwarded upstream. Look up the canonical form instead, and update the existing test to use canonical strip keys. Also add a direct unit test for buildHeaderMutator ensuring that when HeaderOverrides.DefaultHeaders is already populated, provider DefaultHeaders do not overwrite operator-supplied values. Refs: ENTERPILOT#513 (comment) Refs: CodeRabbit review comment on internal/providers/openai/compatible_provider.go:121-138
Both should be fixed @coderabbitai |
GoModel now exposes four per-provider header fields (
custom_upstream_headers,passthrough_user_headers,passthrough_user_headers_skip,passthrough_user_headers_skip_mode) wired through the sharedCompatibleProvideradapter. Every OpenAI-compatible provider gets static injection, filtered caller-header passthrough, and a new provider-default header layer without per-provider code changes. The default passthrough mode is default-openskip: enablingpassthrough_user_headersforwards all non-blocked caller headers unless an explicitallowlist is configured.Files to review (41, +4,124 / -202):
internal/providers/headers.go(start here)HeaderOverridesConfig,ApplyHeaderOverrides,IsHeaderBlocked,shouldForward, and the four-layer composition engine.internal/providers/config.goparseBoolEnv, and skip-mode validation.internal/server/passthrough_headers.goPassthroughHeaderCapturemiddleware — filters incoming headers and stashes them in context.internal/server/request_snapshot.goPassthroughHeaderCaptureowns the hot path now.internal/providers/openai/compatible_provider.goHeaderOverrides/UserPathAliasand appliesDefaultHeadersbefore static/passthrough layers.internal/providers/openrouter/openrouter.goHTTP-Referer,X-OpenRouter-Title) now live asDefaultHeadersand flow through the shared engine.internal/providers/factory.goSetUserPathHeaderbuilder option and plumbing from config to provider options.internal/providers/init.goanyPassthroughUserHeadersis only set afterfactory.Createsucceeds.docs/features/custom-headers.mdxconfig/config.example.yaml/.env.templateWhy
OpenAI-compatible providers often require headers beyond
Authorizationto route, attribute, or throttle requests correctly. Operators need to inject static identity headers so internal tooling behind GoModel is identified upstream without every client sending them, and they need to forward selected caller headers such asX-TitleorX-Trace-Idwhen clients are trusted.The original request surfaced in issue #290. An earlier combined implementation in PR #487 was split: the Kimi Code provider landed in PR #508, and the header-override engine is re-implemented here on top of the PR #486 provider rework.
Design Decisions
internal/providers/headers.goand is applied throughCompatibleProvider's singlebuildHeaderMutatorclosure, so DeepSeek, Groq, Ollama, vLLM, xAI, OpenRouter, Kimi Code, and any future OpenAI-compatible provider inherit the feature automatically.Authorization, etc.) from the provider'sSetHeadershook.HTTP-Referer/X-OpenRouter-Title) supplied viaCompatibleProviderConfig.DefaultHeaders.custom_upstream_headersfromconfig.yamlor<PROVIDER>_CUSTOM_UPSTREAM_HEADERSenv var.passthrough_user_headersis enabled.HTTP-RefererandX-OpenRouter-Titlethrough a localRequestMutator. Those values are now resolved at construction and passed asDefaultHeaders, so they participate in the same precedence rules as static and passthrough headers. This fixes the bug where passthrough headers were overwritten by the local mutator, and it lets operators override attribution withcustom_upstream_headersor caller headers without special-casing OpenRouter.<PROVIDER>_prefix convention.passthrough_user_headersdefaults tofalse. When enabled withpassthrough_user_headers_skip_modeskipor empty (the default), an emptypassthrough_user_headers_skiplist forwards all non-blocked caller headers. Withallowmode and an empty list, no caller headers are forwarded.parseBoolEnvonly accepts an explicit true set (true,1,yes,on,y) or false set; any unrecognized value defaults tofalseso typos cannot silently enable passthrough.PassthroughHeaderCaptureis registered only when at least one successfully initialized provider haspassthrough_user_headers: true, and the flag is recorded afterfactory.Createsucceeds so a failed provider cannot enable the middleware.IsHeaderBlockedpermanently removes credential headers,X-GoModel-User-Path, the configureduser_pathalias, and hop-by-hop/transport headers. The passthrough path additionally consultsTaggingStripHeadersFromContextso headers markeddo_not_passin tagging rules are never forwarded.slog.Warnfires when static and passthrough are both configured and an overlapping header would actually be forwarded; passthrough wins on conflict.How
RawProviderConfiggains four flat header fields; these are packed intoHeaderOverridesConfigduring resolution. Env-var overlays apply on top of YAML.ProviderFactory.SetUserPathHeaderstores the alias thatIsHeaderBlockedchecks beforeproviders.Initruns.PassthroughHeaderCapturemiddleware captures the incoming request, strips hard-blocked headers, and stashes the filtered copy in context.CompatibleProviderreceivesHeaderOverridesandUserPathAliasin its config, merges providerDefaultHeadersinto the override struct, and callsApplyHeaderOverridesafter the provider-specificSetHeadershook.ApplyHeaderOverridesapplies default headers, then static headers, then passthrough headers.shouldForwardchecks the blocklist and strip-set before the skip/allow list.Reviewer notes
internal/providers/openrouter/openrouter.gosuppliesidentityHeaders()asDefaultHeadersand that the precedence table indocs/features/custom-headers.mdxmatches the behavior.custom_upstream_headersor rely on passthrough.RequestSnapshotCaptureno longer captures passthrough headers;PassthroughHeaderCaptureowns filtering and context storage.internal/providers/headers_test.go(TestApplyHeaderOverrides_DefaultStaticPassthroughPrecedence) is intended to be the single source of truth for header ordering.Tests
headers.gopaths: static apply, default-header apply, passthrough apply, skip/allow modes, blocklist, strip-set, default-open skip, allow-empty-forward-nothing, overlap warning, and four-layer precedence.SetUserPathHeaderwiring,HeaderOverridesplumbing, and failed-provider passthrough flag guarding.PassthroughHeaderCapturefilters blocked headers, context storage, and registration when enabled.App.Newpropagates header overrides andAnyPassthroughUserHeadersstate.Run:
go test ./internal/providers/... ./internal/server/... ./config/... ./internal/app/... ./run/...Follow-up
ApplyHeaderOverridesthere.openai.CompatibleProvideralready propagates overrides; the other compatible providers inherit without additional code.Links
Generated with creating-pull-requests Skill
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
bailianfrom the default passthrough provider list.Documentation
Tests