Skip to content

fix(pipeline): expose RawRequest before llm middlewares#1687

Merged
looplj merged 1 commit into
looplj:unstablefrom
ttttmr:fix/raw-request-before-llm-middleware
May 21, 2026
Merged

fix(pipeline): expose RawRequest before llm middlewares#1687
looplj merged 1 commit into
looplj:unstablefrom
ttttmr:fix/raw-request-before-llm-middleware

Conversation

@ttttmr

@ttttmr ttttmr commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • set llm.Request.RawRequest before OnInboundLlmRequest middlewares run
  • add a pipeline test to lock the middleware visibility contract

Why

Request-header-based LLM middlewares can depend on the original inbound request headers. Today /responses-style inbounds can miss them because RawRequest is assigned after OnInboundLlmRequest middlewares run.

Validation

  • cd llm && go test ./pipeline -count=1

@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a sequencing bug in the LLM pipeline where llm.Request.RawRequest was assigned after OnInboundLlmRequest middlewares ran, making original inbound HTTP request headers invisible to those middlewares. The one-line reorder in pipeline.go moves the assignment to immediately after the inbound TransformRequest call, and a new test locks the visibility contract.

  • pipeline.go: llmRequest.RawRequest = request is moved 5 lines earlier, before applyBeforeRequestMiddlewares, so every OnInboundLlmRequest middleware sees the original *httpclient.Request.
  • middleware_test.go: Adds testOutboundWithHeaders (returns a non-nil http.Header to avoid a panic when MergeInboundRequest runs with a real RawRequest) and the new TestProcess_SetsRawRequestBeforeInboundLlmMiddlewares test that asserts both chained middlewares receive a non-nil RawRequest.

Confidence Score: 5/5

The change is a safe, targeted one-line reorder that correctly makes the raw inbound request available to middlewares earlier in the pipeline.

The fix is minimal and the intent is clear. Inbound transformers that already set RawRequest themselves (e.g. openai/inbound.go) assign the same pointer that the pipeline would, so the overwrite is harmless. The new test verifies the core scenario. The only gaps are a test-code style duplication and a missing edge-case test for middleware-replacing-the-request, neither of which affects runtime correctness.

No files require special attention; both changed files are straightforward.

Important Files Changed

Filename Overview
llm/pipeline/pipeline.go One-line reorder: RawRequest is now set before applyBeforeRequestMiddlewares, fixing header visibility for OnInboundLlmRequest middlewares. Change is minimal and correct.
llm/pipeline/middleware_test.go Adds testOutboundWithHeaders and a new test for the RawRequest visibility contract. The new outbound struct duplicates all interface methods rather than embedding testOutbound, which is minor boilerplate, but the test logic itself is correct.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Pipeline
    participant Inbound as Inbound Transformer
    participant MW1 as Middleware 1 (OnInboundLlmRequest)
    participant MW2 as Middleware 2 (OnInboundLlmRequest)
    participant Outbound as Outbound Transformer

    Client->>Pipeline: Process(ctx, rawRequest)
    Pipeline->>Inbound: TransformRequest(ctx, rawRequest)
    Inbound-->>Pipeline: llmRequest

    Note over Pipeline: ✅ llmRequest.RawRequest = rawRequest (BEFORE middlewares — this PR's fix)

    Pipeline->>MW1: OnInboundLlmRequest(ctx, llmRequest)
    Note over MW1: Can now read llmRequest.RawRequest.Headers
    MW1-->>Pipeline: llmRequest (modified)

    Pipeline->>MW2: OnInboundLlmRequest(ctx, llmRequest)
    Note over MW2: Can now read llmRequest.RawRequest.Headers
    MW2-->>Pipeline: llmRequest (modified)

    Pipeline->>Outbound: TransformRequest(ctx, llmRequest)
    Outbound-->>Pipeline: outboundHttpReq

    Note over Pipeline: MergeInboundRequest + RawRequest middlewares + execute
Loading

Reviews (1): Last reviewed commit: "fix(pipeline): expose RawRequest before ..." | Re-trigger Greptile

Comment on lines +50 to +60
// testOutboundWithHeaders is used when the pipeline needs a non-nil header map
// so inbound header merging can proceed without panicking.
type testOutboundWithHeaders struct{}

func (t *testOutboundWithHeaders) APIFormat() llm.APIFormat {
return "test/format"
}

func (t *testOutboundWithHeaders) TransformRequest(ctx context.Context, request *llm.Request) (*httpclient.Request, error) {
return &httpclient.Request{Headers: http.Header{}}, nil
}

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.

P2 testOutboundWithHeaders duplicates all five interface methods from testOutbound, differing only in the TransformRequest return value. Embedding testOutbound and overriding just that method removes the repetition entirely.

Suggested change
// testOutboundWithHeaders is used when the pipeline needs a non-nil header map
// so inbound header merging can proceed without panicking.
type testOutboundWithHeaders struct{}
func (t *testOutboundWithHeaders) APIFormat() llm.APIFormat {
return "test/format"
}
func (t *testOutboundWithHeaders) TransformRequest(ctx context.Context, request *llm.Request) (*httpclient.Request, error) {
return &httpclient.Request{Headers: http.Header{}}, nil
}
// testOutboundWithHeaders is used when the pipeline needs a non-nil header map
// so inbound header merging can proceed without panicking.
type testOutboundWithHeaders struct {
testOutbound
}
func (t *testOutboundWithHeaders) TransformRequest(ctx context.Context, request *llm.Request) (*httpclient.Request, error) {
return &httpclient.Request{Headers: http.Header{}}, nil
}

Comment on lines +792 to +813
func TestProcess_SetsRawRequestBeforeInboundLlmMiddlewares(t *testing.T) {
ctx := context.Background()
callOrder := []string{}

m1 := newTrackingMiddleware("M1", &callOrder)
m2 := newTrackingMiddleware("M2", &callOrder)

exec := &testExecutor{}
factory := NewFactory(exec)
p := factory.Pipeline(
&testInbound{},
&testOutboundWithHeaders{},
WithMiddlewares(m1, m2),
)

request := &httpclient.Request{Headers: http.Header{"X-Test": []string{"1"}}}
result, err := p.Process(ctx, request)
require.NoError(t, err)
require.NotNil(t, result)
require.True(t, m1.sawRawRequestOnInbound)
require.True(t, m2.sawRawRequestOnInbound)
}

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.

P2 RawRequest not tested when a middleware replaces the request object

The test uses trackingMiddleware.OnInboundLlmRequest, which passes the request through unchanged. It therefore doesn't cover the scenario where M1 returns a brand-new *llm.Request (without copying RawRequest), which would leave M2 seeing nil. Since the contract now relies on RawRequest surviving through the full middleware chain, a test case where a middleware wraps or replaces the request object would make the guarantee explicit and guard against regressions in future middleware implementations.

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

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.

Code Review

This pull request modifies the pipeline execution flow to ensure the original RawRequest is available to middlewares during the inbound request phase, supported by a new test case and test helper. The review feedback identifies a potential regression where RawRequest could be lost if a middleware returns a new request instance, suggesting a re-assignment after the middleware chain. Additionally, there is a recommendation to refactor the test helper testOutboundWithHeaders using struct embedding to reduce boilerplate code.

Comment thread llm/pipeline/pipeline.go
Comment on lines 257 to 260
llmRequest, err = p.applyBeforeRequestMiddlewares(ctx, llmRequest)
if err != nil {
return nil, err
}

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.

high

While moving the RawRequest assignment before the middlewares allows them to access the original request, it introduces a potential regression: if any middleware returns a new llm.Request instance (rather than modifying the pointer in-place), the RawRequest field will be nil for the rest of the pipeline. This would break httpclient.MergeInboundRequest later in processRequest.

To ensure robustness and maintain the previous behavior's guarantee that RawRequest is present during execution, you should re-assign it after the middleware chain finishes.

Suggested change
llmRequest, err = p.applyBeforeRequestMiddlewares(ctx, llmRequest)
if err != nil {
return nil, err
}
// Step 2: Apply before request middlewares
llmRequest, err = p.applyBeforeRequestMiddlewares(ctx, llmRequest)
if err != nil {
return nil, err
}
// Re-ensure RawRequest is set even if a middleware returned a new request object.
llmRequest.RawRequest = request

Comment on lines +52 to +60
type testOutboundWithHeaders struct{}

func (t *testOutboundWithHeaders) APIFormat() llm.APIFormat {
return "test/format"
}

func (t *testOutboundWithHeaders) TransformRequest(ctx context.Context, request *llm.Request) (*httpclient.Request, error) {
return &httpclient.Request{Headers: http.Header{}}, nil
}

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

testOutboundWithHeaders is currently duplicating all methods of testOutbound. You can significantly reduce boilerplate by embedding testOutbound and only overriding the TransformRequest method. The other redundant methods for testOutboundWithHeaders (like TransformResponse, TransformStream, etc.) can then be removed from this file.

type testOutboundWithHeaders struct {
	testOutbound
}

func (t *testOutboundWithHeaders) TransformRequest(ctx context.Context, request *llm.Request) (*httpclient.Request, error) {
	return &httpclient.Request{Headers: http.Header{}}, nil
}

@looplj looplj merged commit 88c399d into looplj:unstable May 21, 2026
4 checks passed
@ttttmr ttttmr deleted the fix/raw-request-before-llm-middleware branch May 27, 2026 03:57
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.

2 participants