feat(config): add chat.headers hook with outbound header injection#101
Open
waiyong wants to merge 1 commit into
Open
feat(config): add chat.headers hook with outbound header injection#101waiyong wants to merge 1 commit into
waiyong wants to merge 1 commit into
Conversation
Adds a chat.headers hook that injects user-configured custom headers (OPENCODE_OUTBOUND_HEADERS) into outgoing LLM requests, scoped to specific endpoints (OPENCODE_OUTBOUND_ENDPOINTS) so headers are not sent to third-party providers. The resolved URL mirrors opencode's own resolution: a non-empty provider.options.baseURL override wins, otherwise model.api.url. Matching is by hostname and fails closed on unparseable URLs. When OPENCODE_OUTBOUND_ENDPOINTS is unset, headers are injected into every request, preserving the obvious default. Both settings are also available as plugin-tuple options (outboundHeaders, outboundEndpoints), following the established option > env > default precedence. outboundHeaders is stored raw in PluginConfig and parsed at the use site, mirroring spanAttributes. The handler lives in src/handlers/chat-headers.ts for direct unit testing, consistent with the other handlers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a
chat.headershook that injects user-configured custom headers into opencode's outgoing LLM requests, with optional endpoint scoping so headers are not sent to unintended providers.The mechanism is provider-agnostic — it copies arbitrary
key=valuepairs onto the request and matches against arbitrary hostnames. It has no knowledge of any specific gateway or backend. LLM gateways (LiteLLM, vLLM, or any reverse proxy) commonly require custom request headers: routing hints, tenant IDs, feature flags, or tracing toggles. Arize Phoenix'sx-enable-phoenix-tracingis one example of such a header; nothing in the code is specific to it.Two settings, both also available as plugin-tuple options following the
option > env > defaultprecedence from #65:OPENCODE_OUTBOUND_HEADERSoutboundHeaderskey=valueheaders to inject, parsed by the existingparseAttributePairs().OPENCODE_OUTBOUND_ENDPOINTSoutboundEndpointssplitList()/normalizeList(). When set, headers are injected only for requests whose resolved URL matches by hostname. When unset, headers go to every request (backward compatible).Why endpoint scoping matters:
chat.headersfires for every outgoing LLM request. WithoutOPENCODE_OUTBOUND_ENDPOINTS, a configured header is sent to all providers, including third parties (Anthropic, OpenAI). If a user ever placed a credential-like value in the env var, that would leak. Scoping restricts injection to the hostnames you name.Implementation notes:
PluginConfig.outboundHeadersstays a raw string and is parsed at the use site, mirroringspanAttributes(feat(config): support OPENCODE_SPAN_ATTRIBUTES #67).HandlerContext, alongsidedisabledTraces.provider.options.baseURLif overridden, otherwisemodel.api.url— mirroring opencode's ownresolveSDK.src/handlers/chat-headers.tsfor direct unit testing, consistent with the other handlers.This PR is scoped to custom headers only. W3C
traceparentpropagation is a natural follow-up but is a separate, opt-in concern (it would leak internal trace IDs to third parties by default), so it will be a second PR.Type of change
Checklist
bun run lintpasses with no errorsbun run check:jsdoc-coveragepasses with no errorsbun run typecheckpasses with no errorsbun testpasses with no errorsRelated issues
Closes #100
Additional context
Tests (
bun test: 322 pass, 0 fail — 20 new):tests/chat-headers.test.ts—getResolvedURL(baseURL override, empty-string override falls through, defaultmodel.api.url, both unset);matchesEndpoint(empty allowlist matches all, hostname match ignoring path, non-match, unparseable URL fails closed, unparseable allowlist entries ignored);handleChatHeaders(injects unscoped, injects on matching endpoint, skips on non-matching endpoint, multiple headers, no-op when unconfigured).tests/config.test.ts—loadConfigforOPENCODE_OUTBOUND_HEADERS/OPENCODE_OUTBOUND_ENDPOINTS, and the plugin-option overrides.OtelPluginwith telemetry enabled and confirmed it registers thechat.headershook, injects unscoped headers, skips a non-matching endpoint, and injects for a matchingbaseURLoverride.Open question for review: endpoint matching is by hostname, which tolerates port and path differences but means
http://gateway.internalmatches an allowlist entry ofhttps://gateway.internal. Happy to switch toorigin(scheme + host + port) if you prefer strict matching.