fix(responses): forward verified reasoning accounting header - #2035
fix(responses): forward verified reasoning accounting header#2035asuan-dev wants to merge 2 commits into
Conversation
Greptile SummaryThis PR fixes a header forwarding gap where
Confidence Score: 5/5Safe to merge — the change is narrowly scoped to forwarding a single allowlisted header and does not touch auth, data persistence, or any other security boundary. The header-forwarding logic is strictly allowlisted; every code path (non-streaming, streaming, Codex aggregation, auto-aggregate) captures the header before any transformation wrappers and re-attaches it at the return site, preventing accidental loss. Headers are always cloned on attach and on read, ruling out cross-request mutation. The retry path correctly discards headers from failed attempts and uses only the successful attempt's headers. Tests cover all four paths plus the allowlist edge cases (false values, missing header, conflicting duplicates, Set-Cookie exclusion). No unrelated headers reach the downstream client. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Upstream as Upstream LLM API
participant HC as httpclient.DoStream
participant Pipeline as pipeline.stream()
participant Codex as codexExecutor.Do()
participant API as api.writeForwardResponseHeaders
participant Client as Downstream Client
Upstream->>HC: HTTP response + X-Reasoning-Included: true
HC->>HC: MergeForwardResponseHeaders(nil, rawResp.Header)
HC->>HC: WithResponseHeaders(stream, filteredHeaders)
HC-->>Pipeline: outboundStream (carries headers)
Pipeline->>Pipeline: MergeForwardResponseHeaders(nil, GetResponseHeaders(outboundStream))
note over Pipeline: responseHeaders captured before transformations
Pipeline->>Pipeline: applyRawStreamMiddlewares → TransformStream → Inbound.TransformStream
Pipeline->>Pipeline: WithResponseHeaders(inboundStream, responseHeaders)
Pipeline-->>API: inboundStream (headers re-attached)
alt Non-streaming (Codex aggregation)
Pipeline->>Codex: executor.Do → inner.DoStream
Codex->>Codex: consume stream, AggregateStreamChunks
Codex->>Codex: MergeForwardResponseHeaders(baseHeaders, GetResponseHeaders(stream))
Codex-->>Pipeline: "Response{Headers: filtered}"
Pipeline->>Pipeline: MergeForwardResponseHeaders(nil, httpResp.Headers)
Pipeline-->>API: "Response{Headers: filtered}"
API->>API: MergeForwardResponseHeaders(c.Writer.Header(), response.Headers)
else Streaming
API->>API: MergeForwardResponseHeaders(c.Writer.Header(), GetResponseHeaders(stream))
end
API-->>Client: HTTP response with X-Reasoning-Included: true
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Upstream as Upstream LLM API
participant HC as httpclient.DoStream
participant Pipeline as pipeline.stream()
participant Codex as codexExecutor.Do()
participant API as api.writeForwardResponseHeaders
participant Client as Downstream Client
Upstream->>HC: HTTP response + X-Reasoning-Included: true
HC->>HC: MergeForwardResponseHeaders(nil, rawResp.Header)
HC->>HC: WithResponseHeaders(stream, filteredHeaders)
HC-->>Pipeline: outboundStream (carries headers)
Pipeline->>Pipeline: MergeForwardResponseHeaders(nil, GetResponseHeaders(outboundStream))
note over Pipeline: responseHeaders captured before transformations
Pipeline->>Pipeline: applyRawStreamMiddlewares → TransformStream → Inbound.TransformStream
Pipeline->>Pipeline: WithResponseHeaders(inboundStream, responseHeaders)
Pipeline-->>API: inboundStream (headers re-attached)
alt Non-streaming (Codex aggregation)
Pipeline->>Codex: executor.Do → inner.DoStream
Codex->>Codex: consume stream, AggregateStreamChunks
Codex->>Codex: MergeForwardResponseHeaders(baseHeaders, GetResponseHeaders(stream))
Codex-->>Pipeline: "Response{Headers: filtered}"
Pipeline->>Pipeline: MergeForwardResponseHeaders(nil, httpResp.Headers)
Pipeline-->>API: "Response{Headers: filtered}"
API->>API: MergeForwardResponseHeaders(c.Writer.Header(), response.Headers)
else Streaming
API->>API: MergeForwardResponseHeaders(c.Writer.Header(), GetResponseHeaders(stream))
end
API-->>Client: HTTP response with X-Reasoning-Included: true
Reviews (2): Last reviewed commit: "fix(api): tidy forwarded response header..." | Re-trigger Greptile |
|
需要帮忙确认下,本地是否验证过 |
Summary
Why
AxonHub terminates the upstream HTTP response and rebuilds the downstream response. The old path discarded the handshake header before it reached Codex, so Codex could re-estimate reasoning tokens that the server had already accounted for.
Scope
This change covers the HTTP/SSE response path used by the current Codex channel. Successful WebSocket handshake response headers are not changed here; the WebSocket path has a separate metadata/event contract.
Design
The allowlist and validation live in llm/httpclient. Non-streaming responses reuse the existing response header field. Streaming responses carry a cloned, filtered header alongside the stream and are re-wrapped after protocol transformations. The API boundary applies the same allowlist again as defense in depth.
Validation