Skip to content

refactor(responsecache): transport-free cache core, drop httptest from production#474

Merged
SantiagoDePolonia merged 2 commits into
mainfrom
refactor/responsecache-transport-free
Jul 4, 2026
Merged

refactor(responsecache): transport-free cache core, drop httptest from production#474
SantiagoDePolonia merged 2 commits into
mainfrom
refactor/responsecache-transport-free

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Third PR from the architecture review (doc in #472; follows #473). The response cache's decision logic was modeled as HTTP middleware, so the internal guardrail LLM path had to fabricate a fake HTTP request/recorder with net/http/httptest in production code to pass through it.

This introduces an exchange seam — request info, hit replay, miss capture, hit side effects — with two implementations:

  • echoExchange: the HTTP adapter; now the only place cache logic touches the web framework (replay/capture keep their existing echo implementations, including SSE streaming replay).
  • internalExchange: a buffered transport-free call for the guardrail executor; request headers still come from the originating request's snapshot allowlist.

HandleRequest (HTTP) and HandleInternalRequest (internal) are thin adapters over one shared handle() core, so the two paths can no longer drift.

What's gone

  • httptest and the synthetic echo.Echo instance in production code.
  • Dead exported IoReadAllBody (zero callers).
  • The m.echo == nil failure mode: a zero-value middleware is now a working no-op cache (pinned by a test).

Behavior

Unchanged: same cache keys, same skip-header semantics, same capture rules (200-only, JSON/SSE validation, failover responses never stored), same usage/audit hit recording. Audit enrichment stays HTTP-only — on the old synthetic internal context it was already a silent no-op since the echo store was empty.

New coverage: internal exact miss→hit round trip (incl. failover-responses-never-stored) and the zero-value no-op case.

Follow-up (intentionally not here)

Deleting the legacy ResponseCacheMiddleware.Middleware() path (possible-refactoring #4) — it now shims through the same exchange seam, but its removal requires migrating ~15 middleware-driven tests per that doc's guidance, so it stays a separate focused change.

Testing

Full go build ./... and go test ./... pass; go test -race on responsecache + server; golangci-lint 0 issues; pre-commit make test-race on the commit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Internal requests now use the same response-caching behavior as regular HTTP requests.
    • Streaming responses are handled more consistently, including server-sent events.
    • Cache results now include clearer response details such as status, content type, and failover usage.
  • Bug Fixes

    • Improved cache hit/miss behavior for internal calls, including proper replay of stored responses.
    • Prevents failover responses from being stored as cacheable results.

…m production

The cache decision logic (skip checks, hit lookup, replay, miss capture) was
written against *echo.Context, so the internal guardrail LLM path had to
synthesize a fake HTTP request and recorder via net/http/httptest to pass
through it. Introduce an exchange seam with two implementations: echoExchange
(the HTTP adapter, sole owner of framework types) and internalExchange (a
buffered transport-free call). HandleRequest and HandleInternalRequest are now
thin adapters over one shared handle() core.

- httptest and the synthetic echo.Echo instance are gone from production code;
  HandleInternalRequest's callback returns a buffered InternalResponse instead
  of writing through a fake echo context.
- Audit/usage hit side effects move behind the seam: audit enrichment stays on
  the HTTP path (it was already a no-op on the synthetic internal context) and
  the usage hit recorder is transport-free.
- Cache behavior is unchanged: same keys, same skip headers, same capture
  rules (200-only, JSON/SSE validation, failover responses never stored).
- New coverage: internal exact miss->hit round trip incl. failover-not-stored,
  and zero-value middleware acting as a no-op cache.
- Remove dead exported IoReadAllBody (no callers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@SantiagoDePolonia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 789d2a2a-a137-47a5-8c91-1e8981cb090a

📥 Commits

Reviewing files that changed from the base of the PR and between 01868a3 and a7f005a.

📒 Files selected for processing (1)
  • internal/responsecache/semantic.go
📝 Walkthrough

Walkthrough

Introduces a transport-agnostic exchange abstraction (echoExchange, internalExchange) to unify HTTP and internal cache handling in internal/responsecache. Refactors HandleRequest, HandleInternalRequest, simple/semantic cache middleware, and the usage-hit recorder to use exchange instead of *echo.Context. Updates the internal chat completion executor's cache callback and associated tests accordingly.

Changes

Exchange-based response cache refactor

Layer / File(s) Summary
Exchange interface and implementations
internal/responsecache/exchange.go
Adds exchange interface, echoExchange, InternalResponse struct, and internalExchange with accessors, ReplayHit, runNext, Capture, MarkHit, and result.
HandleRequest/HandleInternalRequest wiring
internal/responsecache/responsecache.go
Refactors HandleRequest and rewrites HandleInternalRequest to build and use an internalExchange via a shared handle(ex, body, next) function, removing the Echo field and synthetic httptest context.
Semantic cache middleware exchange migration
internal/responsecache/semantic.go
Changes Handle, hitRecorder, and header helpers to operate on exchange/header func(string) string instead of *echo.Context/*http.Request; removes IoReadAllBody.
Simple cache middleware exchange migration
internal/responsecache/simple.go
Updates hitRecorder, cache-hit path, StoreAfter, shouldSkipCache, and cachedBody validation to use exchange and a new cacheableResponseBody helper.
Usage hit recorder exchange migration
internal/responsecache/usage_hit.go
Changes newUsageHitRecorder handler signature to accept exchange and derive endpoint/request-id via exchange methods.
Internal chat completion executor callback migration
internal/server/internal_chat_completion_executor.go
Removes Echo dependency; cache callback now returns *responsecache.InternalResponse from a context.Context-based flow, including FailoverUsed.
Tests for internal request handling and semantic middleware
internal/responsecache/handle_request_test.go, internal/responsecache/semantic_test.go
Updates call sites to new signatures and adds tests for cacheless pass-through, exact miss/hit, and failover-not-stored behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Executor as internal_chat_completion_executor
  participant Middleware as ResponseCacheMiddleware
  participant Exchange as internalExchange
  participant Cache as simple/semanticCacheMiddleware

  Executor->>Middleware: HandleInternalRequest(ctx, method, path, body, next)
  Middleware->>Exchange: newInternalExchange(ctx, method, path, next)
  Middleware->>Cache: handle(exchange, body, next)
  Cache->>Exchange: RequestHeader / Path / Method
  alt cache hit
    Cache->>Exchange: ReplayHit(cachedBytes)
  else cache miss
    Exchange->>Executor: runNext(ctx)
    Executor-->>Exchange: InternalResponse
    Cache->>Exchange: Capture(response)
  end
  Middleware-->>Executor: InternalHandleResult
Loading

Possibly related PRs

  • ENTERPILOT/GoModel#196: Refactors usage-hit recording and middleware wiring introduced there to work with the new exchange abstraction instead of *echo.Context.
  • ENTERPILOT/GoModel#202: Both modify the same semantic.go/simple.go cache hit/capture/replay code paths that this PR restructures via the exchange abstraction.
  • ENTERPILOT/GoModel#241: Both touch cached streaming response handling and audit enrichment paths in the response-cache middleware.

Poem

A cache once bound to Echo's frame,
Now hops through exchange, free of that name.
Internal calls and HTTP too,
Replay, capture, mark the hit anew.
This bunny thumps in pure delight —
🐇 one clean contract, day or night!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor: making the response cache transport-free and removing httptest from production code.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/responsecache-transport-free

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 83.21168% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/responsecache/exchange.go 80.30% 8 Missing and 5 partials ⚠️
internal/responsecache/semantic.go 85.00% 1 Missing and 2 partials ⚠️
internal/responsecache/simple.go 86.36% 1 Missing and 2 partials ⚠️
...ternal/server/internal_chat_completion_executor.go 75.00% 2 Missing and 1 partial ⚠️
internal/responsecache/usage_hit.go 83.33% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/responsecache/semantic.go (1)

644-659: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delegate Cache-Control parsing to shouldSkipCacheControl to remove duplication.

shouldSkipAllCacheHeaders reimplements the exact Cache-Control directive loop already provided by shouldSkipCacheControl (simple.go, lines 237-249). Beyond the DRY smell, the two copies have drifted: this one uses strings.Split while shouldSkipCacheControl uses strings.SplitSeq. Collapsing them keeps the skip semantics in one place.

As per coding guidelines: "Keep files small and follow KISS" and "make the smallest change... avoid... duplication."

♻️ Proposed refactor
 func shouldSkipAllCacheHeaders(header func(string) string) bool {
 	if strings.EqualFold(header("X-Cache-Control"), "no-store") {
 		return true
 	}
-	cc := header("Cache-Control")
-	if cc == "" {
-		return false
-	}
-	directives := strings.Split(strings.ToLower(cc), ",")
-	for _, d := range directives {
-		d = strings.TrimSpace(d)
-		if d == "no-cache" || d == "no-store" {
-			return true
-		}
-	}
-	return false
+	return shouldSkipCacheControl(header("Cache-Control"))
 }
🤖 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/responsecache/semantic.go` around lines 644 - 659,
`shouldSkipAllCacheHeaders` is duplicating the `Cache-Control` directive parsing
already handled by `shouldSkipCacheControl`; update this helper to delegate the
`Cache-Control` check to `shouldSkipCacheControl` instead of reimplementing the
loop. Keep the existing `X-Cache-Control` handling in
`shouldSkipAllCacheHeaders`, and use the shared helper in
`semantic.go`/`simple.go` so the skip logic stays in one place and remains
consistent.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@internal/responsecache/semantic.go`:
- Around line 644-659: `shouldSkipAllCacheHeaders` is duplicating the
`Cache-Control` directive parsing already handled by `shouldSkipCacheControl`;
update this helper to delegate the `Cache-Control` check to
`shouldSkipCacheControl` instead of reimplementing the loop. Keep the existing
`X-Cache-Control` handling in `shouldSkipAllCacheHeaders`, and use the shared
helper in `semantic.go`/`simple.go` so the skip logic stays in one place and
remains consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f172ec03-d712-451d-8d2f-bede8c7c3b4f

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed7417 and 01868a3.

📒 Files selected for processing (8)
  • internal/responsecache/exchange.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go
  • internal/responsecache/semantic.go
  • internal/responsecache/semantic_test.go
  • internal/responsecache/simple.go
  • internal/responsecache/usage_hit.go
  • internal/server/internal_chat_completion_executor.go

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge with low risk.

The refactor is contained to response cache transport adaptation. The shared cache decision points preserve the existing exact and semantic cache behavior. Focused tests cover the new internal adapter and zero-value no-op path. No accepted issues were identified.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Ran a focused set of unit tests for the responsecache package, covering Test.*Cache, Test.*Internal, and Test.*Semantic, which completed with EXIT_CODE: 0.
  • Ran a race-detection test across the responsecache and server packages, which completed with both packages reported ok and EXIT_CODE: 0.
  • Built the Gomodel command binary by running go build on ./cmd/gomodel, which completed with EXIT_CODE: 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant HTTP as Echo HTTP request
participant Internal as Internal guardrail call
participant Core as ResponseCacheMiddleware.handle
participant Exact as simpleCacheMiddleware
participant Semantic as semanticCacheMiddleware
participant LLM as LLM/orchestrator

HTTP->>Core: HandleRequest(echoExchange, body, next)
Internal->>Core: HandleInternalRequest(internalExchange, body, next)
Core->>Core: evaluate skip headers and X-Cache-Type
Core->>Exact: TryHit(exchange, body)
alt exact cache hit
    Exact->>HTTP: ReplayHit via writeCachedResponse
    Exact->>Internal: ReplayHit into buffered InternalHandleResult
else miss
    Core->>Semantic: Handle(exchange, body, innerNext)
    Semantic->>Semantic: embed/search when enabled
    alt semantic cache hit
        Semantic->>HTTP: ReplayHit via writeCachedResponse
        Semantic->>Internal: ReplayHit into buffered InternalHandleResult
    else full miss
        Semantic->>Exact: innerNext optionally wraps StoreAfter
        Exact->>LLM: Capture(next)
        LLM-->>Exact: response bytes/status/failover flag
        Exact-->>Exact: enqueue exact write when cacheable
        Semantic-->>Semantic: enqueue semantic write when cacheable
    end
end
Loading
%%{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 HTTP as Echo HTTP request
participant Internal as Internal guardrail call
participant Core as ResponseCacheMiddleware.handle
participant Exact as simpleCacheMiddleware
participant Semantic as semanticCacheMiddleware
participant LLM as LLM/orchestrator

HTTP->>Core: HandleRequest(echoExchange, body, next)
Internal->>Core: HandleInternalRequest(internalExchange, body, next)
Core->>Core: evaluate skip headers and X-Cache-Type
Core->>Exact: TryHit(exchange, body)
alt exact cache hit
    Exact->>HTTP: ReplayHit via writeCachedResponse
    Exact->>Internal: ReplayHit into buffered InternalHandleResult
else miss
    Core->>Semantic: Handle(exchange, body, innerNext)
    Semantic->>Semantic: embed/search when enabled
    alt semantic cache hit
        Semantic->>HTTP: ReplayHit via writeCachedResponse
        Semantic->>Internal: ReplayHit into buffered InternalHandleResult
    else full miss
        Semantic->>Exact: innerNext optionally wraps StoreAfter
        Exact->>LLM: Capture(next)
        LLM-->>Exact: response bytes/status/failover flag
        Exact-->>Exact: enqueue exact write when cacheable
        Semantic-->>Semantic: enqueue semantic write when cacheable
    end
end
Loading

Reviews (1): Last reviewed commit: "refactor(responsecache): transport-free ..." | Re-trigger Greptile

… helper

shouldSkipAllCacheHeaders reimplemented the no-cache/no-store directive loop
that shouldSkipCacheControl already owns; delegate to it so the skip logic
lives in one place. X-Cache-Control handling is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SantiagoDePolonia SantiagoDePolonia merged commit 2417ed1 into main Jul 4, 2026
19 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.

2 participants