Skip to content

bugfix: hardening late July - #212

Merged
thushan merged 30 commits into
mainfrom
feature/campaign-fixes
Aug 8, 2026
Merged

bugfix: hardening late July#212
thushan merged 30 commits into
mainfrom
feature/campaign-fixes

Conversation

@thushan

@thushan thushan commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Various bug fixes and consistency updates from July from Forge testing.

Summary by CodeRabbit

  • Bug Fixes

    • Provider-specific routing prevents requests reaching incompatible endpoints.
    • Oversized request bodies return a clear 413 error.
    • Circuit-breaker recovery is more reliable during slow, stalled or concurrent health checks.
    • Least-connections balancing rotates fairly between equally available endpoints.
    • Dashboard access errors identify the settings needed to resolve them.
    • Status ETags remain stable when only scheduling timestamps change.
  • Improvements

    • Model detection recognises more variants and avoids partial-name matches.
    • Statistics use the resolved backend model, including aliases.
    • Header limits apply consistently with sensible defaults and minimums.
    • Request logging preserves the original route.
    • Proxy responses and streaming flushes are handled more reliably.

@thushan thushan self-assigned this Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 31979fc1-b4f5-4fcf-aa2e-6a306e1b9738

📥 Commits

Reviewing files that changed from the base of the PR and between 68aac4b and 3785dec.

📒 Files selected for processing (2)
  • internal/adapter/proxy/core/retry.go
  • internal/adapter/proxy/core/retry_safety_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/adapter/proxy/core/retry.go
  • internal/adapter/proxy/core/retry_safety_test.go

Walkthrough

The change updates model matching, endpoint balancing, circuit-breaker probes, proxy model metrics, request-size handling, provider routing, response handling, ETag generation, logging, and configuration defaults. Tests cover the revised behaviour across these areas.

Changes

Model matching and endpoint balancing

Layer / File(s) Summary
Boundary-aware model patterns
config/models.yaml, internal/adapter/unifier/*
Model-family patterns enforce boundaries, accept additional separators, support gpt-oss, and cover more model families.
Rotating least-connection ties
internal/adapter/balancer/*
Tied endpoints use an atomic rotating cursor. Strictly lower connection counts still take precedence.

Shared circuit-breaker resilience

Layer / File(s) Summary
Shared breaker state machine
internal/adapter/resilience/*
The shared breaker implements open, closed, half-open, stale-probe, and attempt-token handling.
Health-check breaker integration
internal/adapter/health/*
Health checks derive probe staleness from endpoint timeouts and correlate results with attempts.
Breaker lifecycle validation
internal/adapter/health/*_test.go, internal/adapter/proxy/olla/*_test.go
Tests cover concurrent probes, stale handover, ABA protection, closure, and reopening.

Resolved proxy model metrics

Layer / File(s) Summary
Resolved model statistics contract
internal/adapter/proxy/core/*
Statistics accept resolved model names and skip empty names.
Backend model attribution
internal/adapter/proxy/olla/service_retry.go, internal/adapter/proxy/sherpa/service_retry.go, internal/adapter/proxy/proxy_model_stats_test.go
Proxy engines record statistics under the dispatched backend model rather than the client alias.

Request and provider handling

Layer / File(s) Summary
Header limit normalisation
internal/adapter/security/request_size_limit*
Non-positive header limits use the standard default. Smaller positive limits use a 4096-byte floor.
Committed response and body-size errors
internal/adapter/proxy/core/retry*, internal/app/handlers/handler_proxy*
Response commitment is tracked. Wrapped http.MaxBytesError values return HTTP 413, while committed responses remain unchanged.
Provider fail-closed routing
internal/core/domain/routing.go, internal/app/handlers/handler_*provider*
Non-OpenAI provider profiles fail closed when no compatible endpoint exists.

Observability and configuration defaults

Layer / File(s) Summary
Access diagnostics and stable ETags
internal/app/handlers/dashboard/*, internal/app/handlers/handler_status*
Access responses identify configuration keys. Periodic health and synchronisation timestamps no longer change endpoint ETags.
Stable middleware request paths
internal/app/middleware/*
Logging and quiet-route decisions use the original request path.
Engine stream-buffer defaults
test/manual/*, test/validate/*
Fixed stream-buffer settings are commented out so engine defaults apply.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: bug, routing

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title indicates bug fixes but does not identify the primary changes, such as circuit-breaker, routing, response handling, or model-statistics updates. Use a specific title that names the main change, such as circuit-breaker hardening or response error-handling fixes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.15% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/campaign-fixes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
internal/adapter/proxy/sherpa/service_retry.go (1)

76-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve the translated model before request construction.

Line 78 can record a failure before Lines 93-96 copy ContextModelKey into stats.Model. If request construction fails after translation, the model statistic uses the client model instead of the resolved backend model.

Set stats.Model and derive resolvedModel before http.NewRequestWithContext. Keep the model-header assignment after request construction. Add a regression test for this failure path.

🤖 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/adapter/proxy/sherpa/service_retry.go` around lines 76 - 104, In the
retry flow around http.NewRequestWithContext, apply ContextModelKey to
stats.Model and derive resolvedModel before constructing proxyReq so
construction failures record the translated backend model. Keep setting the
translated model header on proxyReq after successful construction, reuse the
precomputed resolvedModel for all success/failure records, and add a regression
test covering request-construction failure after translation.
🧹 Nitpick comments (7)
internal/app/middleware/logging_gate_test.go (1)

246-258: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the test comment to its reason.

Lines 246-258 repeat the handler implementation and assertion mechanics. Keep the reason for the regression test. This makes the test easier to maintain.

Proposed change
-// TestCombinedLogging_PathMutatingHandlerStaysQuiet pins the fix for the
-// quiet-gate fail-open: handler_proxy.go's dispatchToEndpoints strips the
-// route prefix by mutating r.URL.Path in place (r.URL.Path = pr.targetPath)
-// before forwarding upstream, so the post-request console line and the
-// access log must not re-read r.URL.Path after next.ServeHTTP returns - they
-// would see the backend's target path instead of the original route and lose
-// the quiet-poll classification entirely. CombinedLoggingMiddleware now
-// captures path once, up front, and reuses it throughout.
-//
-// The stand-in handler mutates r.URL.Path to "/v1/chat/completions" - a
-// vLLM-style target path with neither an "/olla/" substring nor an "/api/"
-// prefix, so a regression here cannot hide behind Ollama's coincidentally
-// "/api/"-prefixed target paths.
+// TestCombinedLogging_PathMutatingHandlerStaysQuiet protects quiet-route logging
+// because proxy forwarding mutates r.URL.Path before upstream dispatch.

As per coding guidelines, comments in **/*.go must state why, not what, and be concise and direct.

🤖 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/app/middleware/logging_gate_test.go` around lines 246 - 258, Shorten
the comment above TestCombinedLogging_PathMutatingHandlerStaysQuiet to state
only why the regression test exists: the handler mutates r.URL.Path, so
middleware must preserve the original route for quiet-poll classification.
Remove implementation details, target-path examples, and assertion mechanics.

Source: Coding guidelines

internal/adapter/balancer/least_connection_test.go (1)

443-460: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert consecutive tie selections differ.

The 5/5 aggregate assertion accepts clustered selections that do not alternate. Record the previous selection and fail when the next tied selection has the same endpoint.

Proposed test update
-	counts := make(map[string]int)
+	counts := make(map[string]int)
+	var previous string
 	for range 10 {
 		selected, err := selector.Select(ctx, endpoints)
 		if err != nil {
 			t.Fatalf("Select failed: %v", err)
 		}
+		if selected.Name == previous {
+			t.Fatalf("expected consecutive tied selections to alternate, got %q twice", selected.Name)
+		}
+		previous = selected.Name
 		counts[selected.Name]++
 	}
🤖 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/adapter/balancer/least_connection_test.go` around lines 443 - 460,
Update the sequential tie-selection test around selector.Select to track the
previously selected endpoint and fail whenever consecutive selections have the
same Name. Retain the existing 10-selection loop and aggregate 5/5 assertions,
adding the consecutive-selection check after each successful selection.
internal/adapter/balancer/least_connections.go (1)

15-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce implementation detail in Go comments.

Keep each comment concise and explain why the code exists. Do not describe the cursor mechanics or test steps.

  • internal/adapter/balancer/least_connections.go#L15-L21: State that tie rotation prevents recovered endpoints from starvation.
  • internal/adapter/balancer/least_connections.go#L54-L57: State that all minimum-count endpoints are retained to prevent registration-order bias.
  • internal/adapter/balancer/least_connection_test.go#L427-L433: State that the test prevents starvation during persistent ties.
  • internal/adapter/balancer/least_connection_test.go#L463-L466: State that the test preserves strict minimum-count selection.
🤖 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/adapter/balancer/least_connections.go` around lines 15 - 21, Shorten
the comments at internal/adapter/balancer/least_connections.go:15-21 to state
that tie rotation prevents recovered endpoints from starvation; at
internal/adapter/balancer/least_connections.go:54-57 to state that retaining all
minimum-count endpoints prevents registration-order bias; at
internal/adapter/balancer/least_connection_test.go:427-433 to state that the
test prevents starvation during persistent ties; and at
internal/adapter/balancer/least_connection_test.go:463-466 to state that the
test preserves strict minimum-count selection. Keep each comment focused on why
the behavior exists and remove cursor mechanics and test-step details.

Source: Coding guidelines

internal/adapter/health/circuit_breaker_test.go (1)

293-298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the atomic load in the t.Fatalf arguments.

Both conditions load the field atomically, then the format argument reads state.isOpen and state.lastAttempt directly. These two tests run no concurrent goroutines, so -race will not report anything today. The direct read is still inconsistent with every other access to circuitState, and it is a copy-paste hazard for the concurrent tests in this file.

♻️ Proposed change
-	if atomic.LoadInt32(&state.isOpen) != 0 {
-		t.Fatalf("isOpen = %d after RecordSuccess, want closed (0)", state.isOpen)
-	}
-	if atomic.LoadInt64(&state.lastAttempt) != 0 {
-		t.Fatalf("lastAttempt = %d after RecordSuccess, want reset to 0", state.lastAttempt)
-	}
+	if isOpen := atomic.LoadInt32(&state.isOpen); isOpen != 0 {
+		t.Fatalf("isOpen = %d after RecordSuccess, want closed (0)", isOpen)
+	}
+	if lastAttempt := atomic.LoadInt64(&state.lastAttempt); lastAttempt != 0 {
+		t.Fatalf("lastAttempt = %d after RecordSuccess, want reset to 0", lastAttempt)
+	}

Line 321 has the same pattern.

🤖 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/adapter/health/circuit_breaker_test.go` around lines 293 - 298,
Update the failure arguments in the RecordSuccess assertions to reuse
atomic.LoadInt32(&state.isOpen) and atomic.LoadInt64(&state.lastAttempt) instead
of reading the fields directly. Apply the same correction to the matching
pattern around line 321, keeping the existing conditions and messages unchanged.
internal/adapter/health/client.go (1)

56-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the retry loop variable to remove the shadowing of attempt.

Line 59 binds attempt to the circuit-breaker probe token (int64). Line 76 declares a second attempt (int) as the retry index. The two names carry different meanings in the same function.

The current code is correct. The retry variable's scope ends at Line 101, so Lines 111-119 read the probe token. The type difference also means a wrong binding would fail to compile.

The risk is future edits. If anyone moves a recording call into the loop body, it silently binds the retry index instead of the probe token, and the correlation guard in RecordSuccess drops the result. Rename the loop variable to make the distinction explicit.

♻️ Proposed rename
-	for attempt := 0; attempt <= maxRetries; attempt++ {
-		if attempt > 0 {
+	for retry := 0; retry <= maxRetries; retry++ {
+		if retry > 0 {
 			// Calculate exponential backoff delay
-			delay := calculateBackoffDelay(attempt)
+			delay := calculateBackoffDelay(retry)
🤖 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/adapter/health/client.go` around lines 56 - 59, Rename the
retry-loop variable in the health-check function to distinguish it from the
int64 circuit-breaker probe token assigned by IsOpen. Update all references
within the retry loop and preserve the existing probe-token uses in the
subsequent RecordSuccess/RecordFailure calls.
internal/adapter/health/circuit_breaker.go (1)

143-163: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Attempt tokens derived from time.Now().UnixNano() can collide.

IsOpen uses the wall-clock nanosecond value as the attempt token. On a platform with coarse clock resolution, a stale-handover replacement can receive the same token value as the probe it supersedes. The correlation guard then compares equal, and the superseded probe's late result still applies. The test at Lines 124-128 of internal/adapter/health/circuit_breaker_test.go already works around this exact clock behaviour.

A monotonic counter removes the collision entirely and keeps the same CAS structure. lastAttempt would then hold the token, and staleness would need a separate stamp field.

Given the extra field, this is a follow-up rather than a merge blocker.

♻️ Sketch: separate the token from the timestamp
 type circuitState struct {
 	failures    int64
 	lastFailure int64
 	lastAttempt int64
+	attemptID   int64 // monotonic probe token; never reused
 	isOpen      int32
 }

Assign attemptID with atomic.AddInt64(&state.attemptID, 1) when a probe is admitted, and correlate on that value instead of the timestamp.

🤖 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/adapter/health/circuit_breaker.go` around lines 143 - 163, Replace
wall-clock-derived attempt tokens in IsOpen with a per-endpoint monotonic
counter, adding an attemptID field to the endpoint state and assigning it via
atomic.AddInt64 when a probe is admitted. Store and compare lastAttempt using
this unique counter token, while keeping any timestamp needed for staleness in a
separate field; update RecordSuccess and RecordFailure correlation to use the
new token.
internal/app/handlers/handler_proxy_provider_scope_test.go (1)

14-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce repetitive test comments.

Keep the regression reason in each comment. Let the test name, setup, and assertions describe the tested behaviour.

  • internal/app/handlers/handler_proxy_provider_scope_test.go#L14-L21: Reduce this to the fail-closed regression reason.
  • internal/app/handlers/handler_proxy_provider_scope_test.go#L46-L50: Reduce this to the inclusive-routing regression reason.
  • internal/app/handlers/handler_proxy_provider_scope_test.go#L75-L79: Remove repeated request and response details.
  • internal/app/handlers/handler_proxy_provider_scope_test.go#L146-L151: Retain only why the inspector is necessary.
  • internal/app/handlers/handler_proxy_provider_scope_test.go#L164-L171: Retain only why the registry mock differs from a simple endpoint filter.
  • internal/app/handlers/handler_proxy_provider_scope_test.go#L189-L202: Reduce the diagnostic-masking explanation to the failure cause.

As per coding guidelines, “Comment on why, not what. Concise and direct.”

🤖 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/app/handlers/handler_proxy_provider_scope_test.go` around lines 14 -
21, In internal/app/handlers/handler_proxy_provider_scope_test.go:14-21, shorten
the comment to only the fail-closed regression reason for
TestFilterEndpointsByProfile_FailClosedOnNoMatch. At 46-50, retain only the
inclusive-routing regression reason; at 75-79, remove repeated request and
response details. At 146-151, explain only why the inspector is necessary; at
164-171, explain only why the registry mock differs from a simple endpoint
filter; and at 189-202, reduce the diagnostic-masking comment to the failure
cause.

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.

Inline comments:
In `@internal/app/handlers/dashboard/access.go`:
- Around line 176-183: Update the Warn logging in reject to include the
applicable dashboard.access_policy.allowed_hosts or allowed_cidrs configuration
guidance, matching the operator-facing 403 body; preserve the existing reason,
client_ip, and host fields.

In `@internal/app/handlers/handler_proxy_chunked_oversize_test.go`:
- Around line 77-138: Update TestSizeValidator_ChunkedProxyBody_EndToEnd to use
a configured proxy engine and send requests through the actual proxy
route/request flow instead of the inline io.ReadAll handler. Preserve the
separate direct handleProxyError coverage as a unit test, while asserting the
configured engine produces 413 for oversized chunked bodies and 200 for bodies
under the cap.

In `@internal/app/handlers/handler_proxy.go`:
- Around line 564-565: Replace the Content-Type check in
writeNoRoutableEndpoints with a genuine response-commit check, using the
existing response-writer abstraction or a wrapper that tracks whether
WriteHeader or Write has occurred. Ensure handleProxyError still runs when
headers were only prepared, while preserving the early return after an actual
response commit.

In `@internal/app/handlers/handler_status_etag_test.go`:
- Around line 210-216: Update the inverse-test comment near
TestEndpointStatusResponse_ETagStableAcrossRelativeTimeChurn to reference
TestEndpointStatusResponse_ETagChangesOnStatusFlip instead of
TestStatusResponse_ETagChangesOnRealDataChange, without changing the tests.

---

Outside diff comments:
In `@internal/adapter/proxy/sherpa/service_retry.go`:
- Around line 76-104: In the retry flow around http.NewRequestWithContext, apply
ContextModelKey to stats.Model and derive resolvedModel before constructing
proxyReq so construction failures record the translated backend model. Keep
setting the translated model header on proxyReq after successful construction,
reuse the precomputed resolvedModel for all success/failure records, and add a
regression test covering request-construction failure after translation.

---

Nitpick comments:
In `@internal/adapter/balancer/least_connection_test.go`:
- Around line 443-460: Update the sequential tie-selection test around
selector.Select to track the previously selected endpoint and fail whenever
consecutive selections have the same Name. Retain the existing 10-selection loop
and aggregate 5/5 assertions, adding the consecutive-selection check after each
successful selection.

In `@internal/adapter/balancer/least_connections.go`:
- Around line 15-21: Shorten the comments at
internal/adapter/balancer/least_connections.go:15-21 to state that tie rotation
prevents recovered endpoints from starvation; at
internal/adapter/balancer/least_connections.go:54-57 to state that retaining all
minimum-count endpoints prevents registration-order bias; at
internal/adapter/balancer/least_connection_test.go:427-433 to state that the
test prevents starvation during persistent ties; and at
internal/adapter/balancer/least_connection_test.go:463-466 to state that the
test preserves strict minimum-count selection. Keep each comment focused on why
the behavior exists and remove cursor mechanics and test-step details.

In `@internal/adapter/health/circuit_breaker_test.go`:
- Around line 293-298: Update the failure arguments in the RecordSuccess
assertions to reuse atomic.LoadInt32(&state.isOpen) and
atomic.LoadInt64(&state.lastAttempt) instead of reading the fields directly.
Apply the same correction to the matching pattern around line 321, keeping the
existing conditions and messages unchanged.

In `@internal/adapter/health/circuit_breaker.go`:
- Around line 143-163: Replace wall-clock-derived attempt tokens in IsOpen with
a per-endpoint monotonic counter, adding an attemptID field to the endpoint
state and assigning it via atomic.AddInt64 when a probe is admitted. Store and
compare lastAttempt using this unique counter token, while keeping any timestamp
needed for staleness in a separate field; update RecordSuccess and RecordFailure
correlation to use the new token.

In `@internal/adapter/health/client.go`:
- Around line 56-59: Rename the retry-loop variable in the health-check function
to distinguish it from the int64 circuit-breaker probe token assigned by IsOpen.
Update all references within the retry loop and preserve the existing
probe-token uses in the subsequent RecordSuccess/RecordFailure calls.

In `@internal/app/handlers/handler_proxy_provider_scope_test.go`:
- Around line 14-21: In
internal/app/handlers/handler_proxy_provider_scope_test.go:14-21, shorten the
comment to only the fail-closed regression reason for
TestFilterEndpointsByProfile_FailClosedOnNoMatch. At 46-50, retain only the
inclusive-routing regression reason; at 75-79, remove repeated request and
response details. At 146-151, explain only why the inspector is necessary; at
164-171, explain only why the registry mock differs from a simple endpoint
filter; and at 189-202, reduce the diagnostic-masking comment to the failure
cause.

In `@internal/app/middleware/logging_gate_test.go`:
- Around line 246-258: Shorten the comment above
TestCombinedLogging_PathMutatingHandlerStaysQuiet to state only why the
regression test exists: the handler mutates r.URL.Path, so middleware must
preserve the original route for quiet-poll classification. Remove implementation
details, target-path examples, and assertion mechanics.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 229856ca-0697-43b7-a23f-67f2a3fa2e16

📥 Commits

Reviewing files that changed from the base of the PR and between 1708f3d and 5ebb4f8.

📒 Files selected for processing (39)
  • config/models.yaml
  • internal/adapter/balancer/least_connection_test.go
  • internal/adapter/balancer/least_connections.go
  • internal/adapter/health/checker_test.go
  • internal/adapter/health/circuit_breaker.go
  • internal/adapter/health/circuit_breaker_test.go
  • internal/adapter/health/client.go
  • internal/adapter/health/client_classify_test.go
  • internal/adapter/health/client_ratelimit_test.go
  • internal/adapter/health/types.go
  • internal/adapter/proxy/core/base.go
  • internal/adapter/proxy/core/base_test.go
  • internal/adapter/proxy/core/model_rewrite.go
  • internal/adapter/proxy/core/retry.go
  • internal/adapter/proxy/olla/service.go
  • internal/adapter/proxy/olla/service_retry.go
  • internal/adapter/proxy/proxy_benchmark_test.go
  • internal/adapter/proxy/proxy_model_stats_test.go
  • internal/adapter/proxy/proxy_olla_test.go
  • internal/adapter/proxy/sherpa/service_retry.go
  • internal/adapter/security/request_size_limit.go
  • internal/adapter/security/request_size_limit_test.go
  • internal/adapter/unifier/metadata_extractor_test.go
  • internal/adapter/unifier/model_config.go
  • internal/app/handlers/dashboard/access.go
  • internal/app/handlers/dashboard/access_test.go
  • internal/app/handlers/handler_provider_common.go
  • internal/app/handlers/handler_proxy.go
  • internal/app/handlers/handler_proxy_chunked_oversize_test.go
  • internal/app/handlers/handler_proxy_provider_scope_test.go
  • internal/app/handlers/handler_status.go
  • internal/app/handlers/handler_status_endpoints.go
  • internal/app/handlers/handler_status_etag_test.go
  • internal/app/middleware/logging.go
  • internal/app/middleware/logging_gate_test.go
  • internal/core/domain/routing.go
  • test/manual/config.sticky.yaml
  • test/validate/config.validate.limits.yaml
  • test/validate/config.validate.yaml

Comment thread internal/app/handlers/dashboard/access.go
Comment thread internal/app/handlers/handler_proxy_chunked_oversize_test.go
Comment thread internal/app/handlers/handler_proxy.go Outdated
Comment thread internal/app/handlers/handler_status_etag_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@internal/adapter/proxy/core/retry.go`:
- Around line 73-76: Update ResponseStartedWriter to mark started when Flush is
invoked through http.NewResponseController, while preserving the underlying
flusher behavior exposed by Unwrap. Add a regression test that flushes the
wrapper via http.NewResponseController before invoking handleProxyError, and
verify no error response is appended after the response is committed.

In `@internal/app/handlers/handler_proxy_chunked_oversize_test.go`:
- Around line 265-267: Strengthen the under-cap request assertion in the
relevant chunked-body test so it verifies the request reached the upstream, not
merely that Sherpa returned http.StatusOK. Assert the expected upstream response
body or use the existing upstream request-recording mechanism, while preserving
the current status check and test setup.

In `@internal/app/handlers/handler_proxy.go`:
- Around line 89-100: Update core.ResponseStartedWriter to add FlushError and
Flush methods that mark the response started before delegating to the underlying
writer, including support for http.NewResponseController behavior. Preserve
existing WriteHeader, Write, and Unwrap semantics, and add a regression test
covering a flush followed by handleProxyError to ensure no error response is
appended after commitment.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a1ef3555-4588-4866-b29a-4cfed4051b41

📥 Commits

Reviewing files that changed from the base of the PR and between 5ebb4f8 and d15871d.

📒 Files selected for processing (6)
  • internal/adapter/proxy/core/retry.go
  • internal/adapter/proxy/core/retry_safety_test.go
  • internal/app/handlers/dashboard/access.go
  • internal/app/handlers/handler_proxy.go
  • internal/app/handlers/handler_proxy_chunked_oversize_test.go
  • internal/app/handlers/handler_status_etag_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/app/handlers/dashboard/access.go
  • internal/app/handlers/handler_status_etag_test.go

Comment thread internal/adapter/proxy/core/retry.go
Comment thread internal/app/handlers/handler_proxy_chunked_oversize_test.go
Comment thread internal/app/handlers/handler_proxy.go
thushan added 5 commits August 8, 2026 10:59
http.NewResponseController(rw).Flush() could walk past the wrapper via
Unwrap() straight to the underlying writer, leaving started=false even
though bytes had already reached the client. Flush/FlushError now mark
started before delegating, so handleProxyError can't append an error
onto a response that was committed by a flush with no prior write.
The 200 check alone would also pass if something short-circuited
before the real upstream, since both look identical on status code.
Checking the body proves the response genuinely came through.
…silience

Olla's proxy and health packages had each hand-rolled a near-identical
closed/open/half-open breaker with single-flight probe gating and
ABA-safe attempt tokens, and the fixes for that kept landing in one
copy and missing the other. Pulls the state machine into one type,
with its own test suite covering the full machine independently of
either adapter.
thushan added 3 commits August 8, 2026 15:25
Keeps the endpoint map and per-endpoint CheckTimeout-derived staleness
here; the state machine itself now delegates to resilience.Breaker.
circuitBreaker is now a local alias for resilience.Breaker; this
package still owns the endpoint map, the cleanup sweep and its own
halfOpenStaleness constant (now exported as HalfOpenStaleness since
IsOpen takes it as a call parameter). Exposes the same behaviour as
before, including the persisted half-open state that lets a
sub-threshold half-open failure retry immediately rather than forcing
a fresh full recovery wait.
Review caught that the doc comment presented the sub-threshold
half-open retry as observable behaviour - it isn't reachable via the
public API (failures only resets on success, so every non-closed
state already has failures >= threshold). States plainly that this
mirrors olla's original mechanics as future-proofing, not a
load-bearing path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
internal/adapter/resilience/circuit_breaker.go (2)

214-224: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that SetOpen(true) does not stamp lastFailure.

SetOpen(true) stores stateOpen but leaves lastFailure untouched. If lastFailure is still 0, the next IsOpen call evaluates the recovery timeout against the Unix epoch, finds it elapsed, and transitions straight to half-open. A caller that expects SetOpen(true) to block traffic for OpenDuration gets the opposite result. NewTripped avoids this because it sets both fields.

Either stamp lastFailure inside SetOpen(true), or state the coupling in the doc comment so callers pair it with SetLastFailureNanos.

♻️ Proposed doc clarification
 // SetOpen forces the breaker to the open state (v true) or the closed state
 // (v false). Exported for test construction of specific scenarios;
 // production callers should go through RecordSuccess/RecordFailure, which
-// own the state's lifecycle.
+// own the state's lifecycle.
+//
+// SetOpen(true) does not stamp lastFailure. A breaker forced open with
+// lastFailure still 0 measures its recovery timeout from the Unix epoch, so
+// the next IsOpen call transitions it straight to half-open. Pair this with
+// SetLastFailureNanos, or use NewTripped, when the open state must persist.
 func (b *Breaker) SetOpen(v bool) {
🤖 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/adapter/resilience/circuit_breaker.go` around lines 214 - 224,
Update SetOpen so enabling the open state also stamps lastFailure, matching
NewTripped and ensuring the breaker remains open for OpenDuration; leave the
closed-state behavior unchanged.

132-143: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider a monotonic counter for the attempt token.

The attempt token is time.Now().UnixNano(). On platforms with a coarse wall clock, two successive admissions can produce the same value. If a superseded probe and its replacement share a token, the correlation guard in RecordSuccess and RecordFailure accepts the stale result. The test files already work around this by backdating lastAttempt instead of chaining two real IsOpen calls, which is direct evidence that collisions are possible.

An atomic.Int64 sequence counter removes the collision class entirely and keeps the 0 sentinel. The staleness comparison would then need a separate timestamp field, so this is a deliberate trade-off rather than a drop-in change.

🤖 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/adapter/resilience/circuit_breaker.go` around lines 132 - 143,
Replace the time-based attempt token in the circuit-breaker admission logic with
a strictly increasing atomic.Int64 sequence that preserves 0 as the unset
sentinel, ensuring each probe receives a unique token. Add a separate timestamp
field for probe-staleness checks, and update IsOpen plus
RecordSuccess/RecordFailure to use the sequence token for correlation and the
timestamp for expiration.
internal/adapter/proxy/olla/circuit_breaker_test.go (1)

216-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider trimming the duplicated state-machine tests now that circuitBreaker is an alias.

circuitBreaker is now a type alias for resilience.Breaker, so this package adds no wrapper code between the test and the state machine. TestCircuitBreaker_ABARace_SupersededProbeResultDropped is therefore a near-copy of TestBreaker_ABARace_SupersededProbeResultDropped in internal/adapter/resilience/circuit_breaker_test.go and exercises the same implementation.

The health copy is different: health.CircuitBreaker is a real wrapper that adds endpoint keying and derives probeStaleness from CheckTimeout, so its tests cover behaviour the shared tests cannot.

Keeping the olla copy means a future state-machine change needs three test updates instead of one, which works against the consolidation this PR performs. Consider reducing the olla tests to what is olla-specific, such as GetCircuitBreaker construction, the HalfOpenStaleness value, and the cleanup sweep.

🤖 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/adapter/proxy/olla/circuit_breaker_test.go` around lines 216 - 296,
Remove the duplicated state-machine test
TestCircuitBreaker_ABARace_SupersededProbeResultDropped from the olla package,
since circuitBreaker aliases resilience.Breaker and the shared resilience test
already covers this behavior. Retain or add only olla-specific coverage around
GetCircuitBreaker construction, HalfOpenStaleness, and cleanup-sweep behavior.
internal/adapter/proxy/proxy_olla_test.go (1)

87-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the sleep; it does not affect the assertion.

The open duration is 30 seconds. A 10 millisecond sleep cannot change the result of the following IsOpen call, so the assertion is identical with or without it. The sleep only adds wall-clock time to the suite.

If the intent is to prove the breaker stays open until the recovery timeout elapses, backdate lastFailure with SetLastFailureNanos instead. That tests the boundary and stays deterministic.

♻️ Proposed change
-	// Should remain open for timeout period
-	time.Sleep(10 * time.Millisecond)
+	// Should remain open until the recovery timeout elapses
 	if open, _ := cb.IsOpen(olla.HalfOpenStaleness); !open {
 		t.Error("Circuit breaker should remain open during timeout")
 	}
🤖 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/adapter/proxy/proxy_olla_test.go` around lines 87 - 91, Remove the
10-millisecond time.Sleep from the circuit-breaker assertion in the relevant
test. Keep the existing IsOpen check, or if testing timeout behavior is
intended, use SetLastFailureNanos to backdate lastFailure and make the boundary
deterministic.
internal/adapter/proxy/olla/service.go (1)

235-243: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a proxy-local constant for the open duration.

GetCircuitBreaker takes OpenDuration from health.DefaultCircuitBreakerTimeout. That names a health-check default, but it now configures the recovery window for proxied inference requests. The two workloads have different budgets, so a later change to the health default silently changes proxy behaviour.

The value carries over from the previous implementation, so this is not a regression. Since this PR consolidates breaker configuration, a proxy-local constant alongside HalfOpenStaleness would make the intent explicit and decouple the two subsystems.

🤖 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/adapter/proxy/olla/service.go` around lines 235 - 243, Define a
proxy-local constant for the circuit breaker open duration alongside
HalfOpenStaleness, using the current timeout value, and update
Service.GetCircuitBreaker to use it instead of
health.DefaultCircuitBreakerTimeout. Keep the existing resilience configuration
and behavior unchanged.
internal/adapter/resilience/circuit_breaker_test.go (1)

301-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a floor assertion so the race test cannot degrade to a no-op.

The test only asserts _ = b.Tripped(). If a future change makes IsOpen always return open, every goroutine hits continue, RecordSuccess and RecordFailure never run, and the test still passes. The race coverage would then be silently lost.

Count the admitted calls and assert the count is greater than zero.

♻️ Proposed floor assertion
 	b := New(testConfig(5))
 
+	var admitted int64
 	var wg sync.WaitGroup
 	wg.Add(goroutines)
 	for i := range goroutines {
 		go func(id int) {
 			defer wg.Done()
 			for j := range iterations {
 				open, attempt := b.IsOpen(testProbeStaleness)
 				if open {
 					continue
 				}
+				atomic.AddInt64(&admitted, 1)
 				if (id+j)%3 == 0 {
 					b.RecordFailure(attempt)
 				} else {
 					b.RecordSuccess(attempt)
 				}
 			}
 		}(i)
 	}
 	wg.Wait()
 
+	// Guards against the loop degrading to a no-op that races nothing.
+	if atomic.LoadInt64(&admitted) == 0 {
+		t.Fatal("no call was admitted, so the concurrent record paths were never exercised")
+	}
+
 	// No panic and the breaker is still in a legal state (open or closed).
 	_ = b.Tripped()
🤖 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/adapter/resilience/circuit_breaker_test.go` around lines 301 - 328,
Add an atomic or mutex-protected counter in
TestBreaker_Concurrent_MixedTrafficNoRace, incremented whenever IsOpen returns
closed and the goroutine proceeds to RecordFailure or RecordSuccess. After
waiting for all goroutines, assert that this admitted-call count is greater than
zero before the existing Tripped check.
🤖 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.

Inline comments:
In `@internal/adapter/proxy/core/retry.go`:
- Around line 94-101: The FlushError method should restore rw.started to false
when ResponseController.Flush returns an error matching http.ErrNotSupported, so
handleProxyError can still write a proxy error response; preserve the committed
state for other flush outcomes. Update the related coverage in
internal/adapter/proxy/core/retry_safety_test.go at lines 350-360 to verify
unsupported flushes remain uncommitted.

---

Nitpick comments:
In `@internal/adapter/proxy/olla/circuit_breaker_test.go`:
- Around line 216-296: Remove the duplicated state-machine test
TestCircuitBreaker_ABARace_SupersededProbeResultDropped from the olla package,
since circuitBreaker aliases resilience.Breaker and the shared resilience test
already covers this behavior. Retain or add only olla-specific coverage around
GetCircuitBreaker construction, HalfOpenStaleness, and cleanup-sweep behavior.

In `@internal/adapter/proxy/olla/service.go`:
- Around line 235-243: Define a proxy-local constant for the circuit breaker
open duration alongside HalfOpenStaleness, using the current timeout value, and
update Service.GetCircuitBreaker to use it instead of
health.DefaultCircuitBreakerTimeout. Keep the existing resilience configuration
and behavior unchanged.

In `@internal/adapter/proxy/proxy_olla_test.go`:
- Around line 87-91: Remove the 10-millisecond time.Sleep from the
circuit-breaker assertion in the relevant test. Keep the existing IsOpen check,
or if testing timeout behavior is intended, use SetLastFailureNanos to backdate
lastFailure and make the boundary deterministic.

In `@internal/adapter/resilience/circuit_breaker_test.go`:
- Around line 301-328: Add an atomic or mutex-protected counter in
TestBreaker_Concurrent_MixedTrafficNoRace, incremented whenever IsOpen returns
closed and the goroutine proceeds to RecordFailure or RecordSuccess. After
waiting for all goroutines, assert that this admitted-call count is greater than
zero before the existing Tripped check.

In `@internal/adapter/resilience/circuit_breaker.go`:
- Around line 214-224: Update SetOpen so enabling the open state also stamps
lastFailure, matching NewTripped and ensuring the breaker remains open for
OpenDuration; leave the closed-state behavior unchanged.
- Around line 132-143: Replace the time-based attempt token in the
circuit-breaker admission logic with a strictly increasing atomic.Int64 sequence
that preserves 0 as the unset sentinel, ensuring each probe receives a unique
token. Add a separate timestamp field for probe-staleness checks, and update
IsOpen plus RecordSuccess/RecordFailure to use the sequence token for
correlation and the timestamp for expiration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56176105-2eb7-4c27-ab33-f61ead9bca4c

📥 Commits

Reviewing files that changed from the base of the PR and between d15871d and 68aac4b.

📒 Files selected for processing (17)
  • config/models.yaml
  • internal/adapter/health/circuit_breaker.go
  • internal/adapter/health/circuit_breaker_test.go
  • internal/adapter/proxy/benchmark_refactor_test.go
  • internal/adapter/proxy/core/retry.go
  • internal/adapter/proxy/core/retry_safety_test.go
  • internal/adapter/proxy/olla/circuit_breaker_test.go
  • internal/adapter/proxy/olla/service.go
  • internal/adapter/proxy/olla/service_leak_test.go
  • internal/adapter/proxy/olla/service_retry.go
  • internal/adapter/proxy/proxy_integration_test.go
  • internal/adapter/proxy/proxy_olla_test.go
  • internal/adapter/resilience/circuit_breaker.go
  • internal/adapter/resilience/circuit_breaker_test.go
  • internal/adapter/unifier/metadata_extractor_test.go
  • internal/adapter/unifier/model_config.go
  • internal/app/handlers/handler_proxy_chunked_oversize_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/adapter/unifier/model_config.go
  • internal/adapter/proxy/olla/service_retry.go
  • config/models.yaml
  • internal/adapter/health/circuit_breaker_test.go
  • internal/adapter/unifier/metadata_extractor_test.go

Comment thread internal/adapter/proxy/core/retry.go
@thushan
thushan merged commit 200c24f into main Aug 8, 2026
6 checks passed
@thushan
thushan deleted the feature/campaign-fixes branch August 8, 2026 08:40
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.

1 participant