Skip to content

fix: passthrough X-Initiator header in Copilot channel for billing control#1222

Merged
looplj merged 1 commit intolooplj:release/v0.9.xfrom
JasonWenTheFox:fix/copilot-x-initiator-passthrough
Mar 30, 2026
Merged

fix: passthrough X-Initiator header in Copilot channel for billing control#1222
looplj merged 1 commit intolooplj:release/v0.9.xfrom
JasonWenTheFox:fix/copilot-x-initiator-passthrough

Conversation

@JasonWenTheFox
Copy link
Copy Markdown
Contributor

Summary

  • Block X-Initiator from the global header auto-merge pipeline (blockedHeaders) to prevent it from leaking to non-Copilot upstream providers
  • Explicitly forward X-Initiator in the Copilot outbound transformer for both Chat Completions and Responses API (Codex) request paths

Problem

When using opencode (or oh-my-opencode) with AxonHub's Copilot channel, every tool call is billed as a separate premium request, causing quota to deplete dozens of times faster than expected.

opencode already sends X-Initiator: agent for agent-initiated requests (tool calls, compaction, subagent flows) and X-Initiator: user for user-initiated turns. GitHub Copilot's server uses this header to skip billing for agent requests. However, AxonHub's MergeInboundRequest pipeline silently forwards ALL non-blocked inbound headers to ALL upstream providers — so X-Initiator either gets forwarded everywhere (undesirable) or needs to be explicitly controlled per channel.

Solution

  1. Add X-Initiator to blockedHeaders in llm/httpclient/utils.go — this prevents the header from being auto-forwarded to any upstream provider via the merge pipeline
  2. In llm/transformer/openai/copilot/outbound.go, explicitly read X-Initiator from the inbound request headers and set it on the outgoing Copilot request — following the same pattern used by the Codex transformer for Originator, Session_id, and User-Agent

This ensures only the Copilot channel forwards X-Initiator to GitHub's API, while all other channels remain unaffected.

Changes

File Change
llm/httpclient/utils.go Add X-Initiator to blockedHeaders
llm/transformer/openai/copilot/outbound.go Add InitiatorHeader constant; forward header in TransformRequest() and transformResponsesRequest()

Testing

The header forwarding follows the exact same pattern as codex/outbound.go (lines 94-98, 157-174) which reads Session_id, Originator, User-Agent from llmReq.RawRequest.Headers and sets them on the outgoing request. The pattern is battle-tested across the codebase.

Closes #820

…ntrol

When using opencode with AxonHub's Copilot channel, every tool call is
billed as a separate premium request because the X-Initiator header
(which tells GitHub Copilot whether a request is user- or agent-initiated)
is dropped by AxonHub's header merge pipeline.

- Block X-Initiator from auto-merge to prevent leaking to non-Copilot providers
- Explicitly forward X-Initiator in Copilot outbound transformer for both
  Chat Completions and Responses API (Codex) paths

Closes looplj#820
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the X-Initiator header for Copilot billing control, blocking it from automatic forwarding while implementing manual forwarding in the outbound transformer. Feedback was provided to address code duplication by extracting the header forwarding logic into a reusable helper function.

Comment on lines +163 to +167
if llmReq.RawRequest != nil && llmReq.RawRequest.Headers != nil {
if initiator := llmReq.RawRequest.Headers.Get(InitiatorHeader); initiator != "" {
headers.Set(InitiatorHeader, initiator)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for forwarding a header is duplicated in transformResponsesRequest at lines 588-592. To improve maintainability and avoid code duplication, consider extracting this into a helper function. This would also be useful if other headers need to be forwarded in the future.

For example, you could create a function like this:

// forwardHeaderIfExists forwards a header from the inbound raw request to the destination headers if it exists.
func forwardHeaderIfExists(dest http.Header, llmReq *llm.Request, headerName string) {
    if llmReq != nil && llmReq.RawRequest != nil && llmReq.RawRequest.Headers != nil {
        if value := llmReq.RawRequest.Headers.Get(headerName); value != "" {
            dest.Set(headerName, value)
        }
    }
}

Then you could call it here as forwardHeaderIfExists(headers, llmReq, InitiatorHeader) and similarly in transformResponsesRequest.

@looplj looplj merged commit bd5fd21 into looplj:release/v0.9.x Mar 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature/功能]: 是否会考虑添加(优化?)Github Copilot OAuth作为模型提供商?

2 participants