Skip to content

feat(guardrails): route rewrite calls through workflows + remove synthetic internal guardrail transport#210

Merged
SantiagoDePolonia merged 15 commits intomainfrom
refactor/internal-translated-executor
Apr 5, 2026
Merged

feat(guardrails): route rewrite calls through workflows + remove synthetic internal guardrail transport#210
SantiagoDePolonia merged 15 commits intomainfrom
refactor/internal-translated-executor

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Apr 5, 2026

Summary

  • replace the synthetic internal HTTP/Echo guardrail executor with a transport-free translated executor
  • share translated execution-plan construction between HTTP and internal request paths
  • remove the context-masking workaround introduced only to support the synthetic nested transport path

Testing

  • go test ./...
  • node --test internal/admin/dashboard/static/js/modules/guardrails.test.js internal/admin/dashboard/static/js/modules/timezone-layout.test.js

Summary by CodeRabbit

  • New Features

    • Optional LLM-based "llm_based_altering" guardrail for configurable text rewrites (model, roles, prompt, max_tokens, user_path).
  • Admin UI

    • Enhanced guardrail type selector, checkbox-array role controls, per-field editor/legend/help text, and alias/fieldset CSS.
  • Documentation

    • README, advanced guardrails guide, and example config updated with new guardrail type and examples.
  • Bug Fixes

    • Guardrail names containing “/” are rejected.
  • Caching

    • X-Cache header labels clarified for exact vs semantic hits.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Adds a new llm_based_altering guardrail (LLM-backed pre-provider rewrites), wires an internal transport-free chat executor into guardrails with audit/cache/plan integration, propagates request origin/user_path, extends responses↔messages adapters and structured-content rewriting, and updates admin UI, config, and tests.

Changes

Cohort / File(s) Summary
Docs & Examples
README.md, config/config.example.yaml, docs/advanced/guardrails.mdx
Document new llm_based_altering guardrail and optional user_path; update roadmap/status text and add commented YAML example for auxiliary LLM rewrites.
Guardrail Core & Runtime
internal/guardrails/llm_based_altering.go, internal/guardrails/definitions.go, internal/guardrails/service.go, internal/guardrails/factory.go, internal/guardrails/system_prompt.go
Introduce LLMBasedAltering guardrail type, config/role normalization and validation, type normalization, executor-aware buildDefinition/unavailable fallback, snapshot rebuilds, and small system_prompt refactor.
Provider / Responses Adapter
internal/guardrails/provider.go, internal/guardrails/executor.go, internal/guardrails/provider_test.go
Extend responses↔messages adapters and coercion layer to accept varied input shapes, return errors from conversion, and support structured-content rewriting and input patching.
Internal Executor & App Wiring
internal/server/internal_chat_completion_executor.go, internal/app/app.go, internal/guardrails/service.go
Add transport-free InternalChatCompletionExecutor with audit/cache/plan wiring; app constructs and sets guardrail executor at startup; guardrails service accepts/validates optional executor variadic and supports SetExecutor.
Execution Policy & Context
internal/core/context.go, internal/server/execution_policy.go, internal/server/execution_plan_helpers.go, internal/server/model_validation.go, internal/server/native_batch_service.go
Add RequestOrigin type/helpers, thread contexts into applyExecutionPolicy, disable guardrails for guardrail-origin calls, and refactor execution-plan construction and caller threading.
Audit & Response Cache
internal/auditlog/entry_capture.go, internal/auditlog/middleware.go, internal/responsecache/responsecache.go, internal/server/translated_inference_service.go
Add PopulateResponseData and CaptureInternalJSONExchange, generic body capture helpers, exported enrichment APIs, InternalHandleResult and HandleInternalRequest, and cache-request-context helper wiring.
Admin UI & Tests
internal/admin/dashboard/static/js/modules/guardrails.js, internal/admin/dashboard/static/js/...guardrails.test.js, internal/admin/dashboard/templates/index.html, internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/static/js/...timezone-layout.test.js
JS helpers for guardrail type resolution and checkbox-array normalization, select sync logic, template changes for checkbox arrays and user_path hint, CSS for alias fieldset/legend, and matching tests.
Guardrails Tests & Concurrency
internal/guardrails/llm_based_altering_test.go, internal/guardrails/service_test.go, internal/guardrails/provider_test.go, internal/server/internal_chat_completion_executor_test.go, internal/admin/handler_guardrails_test.go
Extensive tests for config normalization, rewrite behavior, concurrency bounds/cancellation, fail-open semantics, executor/audit/cache integration, and name validation.
Response Cache & Headers
internal/responsecache/semantic.go, internal/responsecache/stream_cache.go, internal/responsecache/handle_request_test.go
Add cache header constants and mapping helper; compute X-Cache header variants; implement HandleInternalRequest and validation tests for nil context/uninitialized middleware.
Auditlog Tests
internal/auditlog/entry_capture_test.go
Tests for CaptureInternalJSONExchange header/body capture, trace propagation, marshal-failure handling, and size-bound truncation semantics.
Server & Translation Adjustments
internal/server/translated_inference_service.go, internal/server/execution_policy_test.go, internal/server/execution_plan_helpers.go, internal/server/model_validation.go, internal/server/native_batch_service.go
Refactor cache request-context helper, thread context into policy application and related tests/call sites.
Misc / Tooling
go.mod, assorted middleware/tests`
Promote golang.org/x/sync to direct require and small import/refactor adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Guardrails as GuardrailService
    participant Executor as InternalChatExecutor
    participant LLM
    participant Provider

    Client->>Guardrails: Submit request (instructions + input/messages)
    Guardrails->>Guardrails: Select messages by role, skip by prefix
    Guardrails->>Executor: ChatCompletion requests for texts to rewrite
    Note right of Executor: context set to rgba(255,165,0,0.5) "guardrail" and effective user_path
    Executor->>LLM: POST /v1/chat/completions (system + <TEXT_TO_ALTER>)
    LLM-->>Executor: Rewritten content
    Executor-->>Guardrails: Rewritten texts
    Guardrails->>Guardrails: Apply rewrites to instructions/input/messages
    Guardrails-->>Provider: Forward rewritten envelope
    Provider-->>Client: Final provider response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

release:internal

Poem

🐇 I nibble prompts and stitch new seams,
I hop through logs and gentle beams,
I wrap each text in tiny marks,
Then hop it back with brighter sparks—
Hooray, the guardrails hum in streams.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and specifically describes the main changes: introducing an LLM-based altering guardrail type and routing internal rewrite calls through workflows instead of synthetic transport.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 refactor/internal-translated-executor

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 and usage tips.

@SantiagoDePolonia SantiagoDePolonia changed the base branch from feat/guardrail-data-anonymization to main April 5, 2026 15:09
@SantiagoDePolonia SantiagoDePolonia changed the title refactor(server): remove synthetic internal guardrail transport feat(guardrails): route rewrite calls through workflows + remove synthetic internal guardrail transport Apr 5, 2026
@SantiagoDePolonia SantiagoDePolonia marked this pull request as ready for review April 5, 2026 15:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors guardrail execution to route internal guardrail-driven rewrites through the existing translated workflow/planning path (without a synthetic internal HTTP transport), and introduces a new llm_based_altering guardrail type that performs auxiliary LLM-based message rewriting.

Changes:

  • Added a transport-free internal translated chat executor (InternalChatCompletionExecutor) that reuses execution planning, audit, usage, and response cache behavior.
  • Implemented llm_based_altering guardrails (config, registry/definitions, UI/admin plumbing) and expanded Responses request rewriting to preserve input-item shape.
  • Added request-origin context + execution-policy overrides to prevent guardrail recursion (disable guardrails + clear guardrails hash for internal guardrail calls).

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates guardrails capability summary wording/types.
internal/server/translated_inference_service.go Centralizes cache-related context wiring (plan + guardrails hash).
internal/server/native_batch_service.go Passes request context into execution-policy application.
internal/server/model_validation.go Uses shared translated-plan builder and passes ctx into policy application.
internal/server/internal_chat_completion_executor.go Adds transport-free internal translated chat execution path (with cache/audit/usage).
internal/server/internal_chat_completion_executor_test.go Tests internal executor planning, audit capture, and cache routing.
internal/server/execution_policy.go Applies request-origin-based policy overrides (disable guardrails for guardrail-origin calls).
internal/server/execution_policy_test.go Updates tests for new policy application signature.
internal/server/execution_plan_helpers.go Extracts reusable translatedExecutionPlan(...) helper.
internal/responsecache/responsecache.go Adds HandleInternalRequest to reuse cache middleware for internal execution.
internal/guardrails/system_prompt.go Minor slice construction simplification.
internal/guardrails/service.go Adds executor injection + snapshot rebuild for LLM-based guardrails.
internal/guardrails/service_test.go Adds coverage for LLM-based altering pipelines and executor swapping.
internal/guardrails/provider.go Extends Responses input rewriting/preservation for guardrails (esp. structured inputs).
internal/guardrails/provider_test.go Adds coverage for LLM-based altering behavior and Responses input rewriting.
internal/guardrails/llm_based_altering.go Implements the LLM-based altering guardrail.
internal/guardrails/llm_based_altering_test.go Adds unit tests for config normalization, rewrite behavior, and concurrency limits.
internal/guardrails/factory.go Threads auxiliary executor into guardrails factory/service construction.
internal/guardrails/executor.go Updates Responses path to handle new message extraction/apply error returns.
internal/guardrails/definitions.go Adds LLM-based guardrail type config/normalization, descriptors, and “unavailable” guardrail behavior.
internal/core/context.go Introduces request-origin context propagation.
internal/auditlog/middleware.go Refactors body capture helpers and adds entry-level enrich helpers.
internal/auditlog/entry_capture.go Adds internal JSON exchange capture for transport-free execution.
internal/app/app.go Wires internal guardrail executor + extends guardrail config type handling and user_path plumbing.
internal/admin/handler_guardrails_test.go Expands admin tests for new guardrail type defaults/normalization/validation.
internal/admin/dashboard/templates/index.html Updates guardrail form UI to support checkbox fields + type select sync.
internal/admin/dashboard/static/js/modules/timezone-layout.test.js Updates template assertions for new guardrail UI wiring.
internal/admin/dashboard/static/js/modules/guardrails.test.js Adds tests for checkbox fields + type select syncing + defaults behavior.
internal/admin/dashboard/static/js/modules/guardrails.js Adds array/checkbox handling and robust guardrail-type resolution/sync.
go.mod Promotes golang.org/x/sync to a direct dependency.
docs/advanced/guardrails.mdx Documents llm_based_altering, user_path, and internal execution behavior.
config/config.go Adds YAML config schema for llm_based_altering + user_path.
config/config.example.yaml Adds example YAML for llm_based_altering guardrails.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +205 to +209
if err := decoder.Decode(&cfg); err != nil {
return llmBasedAlteringDefinitionConfig{}, newValidationError("invalid llm_based_altering config: "+err.Error(), err)
}
if decoder.More() {
return llmBasedAlteringDefinitionConfig{}, newValidationError("invalid llm_based_altering config: trailing data", nil)
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

decodeLLMBasedAlteringDefinitionConfig attempts to detect trailing JSON by calling decoder.More(), but More() only reports whether the current array/object has more elements; it won’t catch extra tokens after the top-level object (e.g. {...} garbage). Use a second decoder.Decode(&struct{}{}) and require io.EOF (or equivalent) to reliably reject trailing data.

Copilot uses AI. Check for mistakes.
ruleType := strings.ToLower(strings.TrimSpace(rule.Type))
switch ruleType {
case "llm-based-altering":
ruleType = "llm_based_altering"
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

configGuardrailDefinitions normalizes the hyphenated alias for llm_based_altering, but not for system_prompt (even though guardrails.normalizeDefinitionType supports system-prompt). If a user sets type: system-prompt in YAML, it will currently fail as “unsupported type”. Consider normalizing system-prompt to system_prompt here as well for consistent config parsing.

Suggested change
ruleType = "llm_based_altering"
ruleType = "llm_based_altering"
case "system-prompt":
ruleType = "system_prompt"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (1)
internal/guardrails/factory.go (1)

60-90: 🛠️ Refactor suggestion | 🟠 Major

Reject extra executors instead of silently dropping them.

The new variadic constructors read like multi-executor support, but internal/guardrails/service.go only preserves executors[0]. Passing two executors now succeeds and quietly ignores the second one. Either fail fast on len(executors) > 1 or make the API singular so the contract matches the implementation.

♻️ Proposed fix
 func newResult(ctx context.Context, storeConn storage.Storage, refreshInterval time.Duration, executors ...ChatCompletionExecutor) (*Result, error) {
+	if len(executors) > 1 {
+		return nil, fmt.Errorf("at most one ChatCompletionExecutor may be provided")
+	}
 	store, err := createStore(ctx, storeConn)
 	if err != nil {
 		return nil, err
 	}
 	service, err := NewService(store, executors...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/guardrails/factory.go` around lines 60 - 90, The constructors accept
variadic ChatCompletionExecutor but the implementation (NewService) only uses
executors[0], so callers can silently pass extra executors; add a fast-fail
check to reject multiple executors. In newResult (and optionally in New and
NewWithSharedStorage) check if len(executors) > 1 and return a clear error like
"only one ChatCompletionExecutor is supported" before any work is done; this
enforces the single-executor contract and avoids silently dropping extra
executors (refer to the functions New, NewWithSharedStorage, newResult and the
ChatCompletionExecutor parameter and NewService usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 384-388: The User Path input (x-model="guardrailForm.user_path")
only affects auxiliary rewrite guardrails, so either conditionally render it
only when guardrailForm.type === 'llm_based_altering' or update the hint to
state it only applies to rewrite-type guardrails; locate the input block around
the label with class "alias-form-field" and either wrap it in a conditional
(e.g., x-show/x-if using guardrailForm.type === 'llm_based_altering') or change
the small.alias-form-hint text to explicitly say "Only used for auxiliary
rewrite (llm_based_altering) guardrails; ignored for other guardrail types."
Ensure the visible behavior and aria-label remain unchanged.
- Around line 411-423: The checkbox collection currently rendered when
field.input === 'checkboxes' lacks a semantic group; replace the outer div with
a fieldset and render field.label inside a legend (keeping the same visual
styling) so screen readers announce a shared group label, and keep the template
x-for loop using the same input handling functions guardrailArrayFieldSelected
and toggleGuardrailArrayFieldValue; ensure the hint (field.help) remains
associated (e.g., aria-describedby on inputs or the fieldset) and verify the
rendered editor with accessibility tooling after the change.

In `@internal/admin/handler_guardrails_test.go`:
- Around line 296-316: Update the TestUpsertGuardrailRejectsSlashInName test to
assert the full OpenAI-compatible error envelope instead of only the HTTP 400
code: after calling h.UpsertGuardrail(c) and checking rec.Code, parse/inspect
rec.Body and reuse the same envelope helper used in the delete tests (the helper
used there to validate the `error` object shape) to assert the response contains
an `error` object with `type`, `message`, `param`, and `code` fields and
appropriate values for the invalid name case; this ensures the handler method
UpsertGuardrail returns the required contract.

In `@internal/auditlog/entry_capture.go`:
- Around line 203-218: In boundedAuditBody, the nested nil check inside the
len(body) <= MaxBodyCapture branch is redundant and unclear; explicitly handle a
nil body first (e.g., if body == nil { return []byte{}, false }) then proceed to
the length-based logic so callers get a consistent empty slice vs nil result,
and remove the unreachable inner nil check in boundedAuditBody to simplify the
control flow.

In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 198-217: Add a second test that cancels the context during
processing to exercise mid-flight cancellation: create a cancellable ctx, pass a
mockChatCompletionExecutor into NewLLMBasedAlteringGuardrail whose chatFn blocks
on a channel or sync point (e.g., make chatFn wait on a started channel), then
start Process in a goroutine and trigger cancel() after the chatFn has begun
(unblock/notify started channel or sleep briefly then call cancel), and assert
Process returns a context cancellation error; reference
TestLLMBasedAltering_Process_PropagatesContextCancellation,
NewLLMBasedAlteringGuardrail, g.Process, mockChatCompletionExecutor, and chatFn
to locate where to implement the blocking/unblocking and cancellation logic.

In `@internal/guardrails/llm_based_altering.go`:
- Around line 22-228: The large hard-coded prompt DefaultLLMBasedAlteringPrompt
should be externalized; create an external template file (e.g.,
llm_based_altering_prompt.txt) and replace the long const with a var (e.g.,
DefaultLLMBasedAlteringPrompt string) that is populated at init by reading the
file (use go:embed or a config loader) with a fallback to the original string if
the file is missing; update any code that references
DefaultLLMBasedAlteringPrompt to use the var so deployments can override the
prompt without recompiling.
- Around line 438-440: The loop over texts currently spawns detached goroutines
via group.Go() and uses i := i; text := text to avoid captures; instead, process
each text synchronously within the current context (respecting ctx) and remove
the inner reassignments; also change the function's return type from error to
core.GatewayError and convert any returned errors into that typed value before
returning; locate usages of group.Go, the loop over texts, and the function
signature in llm_based_altering.go to implement these changes.

In `@internal/guardrails/provider.go`:
- Around line 1423-1432: The cloneStringAnyMap function currently performs a
shallow copy and will share nested references (maps, slices, etc.) between src
and the clone; update the code by adding a clear comment above cloneStringAnyMap
describing this shallow-copy semantics and that nested structures are not
deep-copied (and mark intended usage assumptions: values are primitives or fully
replaced), and if any caller (e.g., response input patching code) might mutate
nested structures instead of replacing them, replace cloneStringAnyMap with a
deep-cloning implementation or add a helper deepCloneStringAnyMap to avoid
accidental shared mutations.

In `@internal/responsecache/responsecache.go`:
- Line 177: The code creates a new Echo instance for every internal request (c
:= echo.New().NewContext(req, rec)), causing allocations; modify the middleware
to accept or cache a shared Echo instance in the middleware struct (e.g., set a
field in NewResponseCacheMiddleware) and use that instance to create contexts in
HandleInternalRequest (replace echo.New() with the cached/shared echo instance),
or add an optional constructor parameter to pass an existing *echo.Echo so the
context creation reuses the same router/middleware chain.
- Around line 168-170: The code currently silently replaces a nil ctx with
context.Background(), which masks caller bugs; instead remove the fallback and
fail fast: in the function(s) where you see the snippet that sets "if ctx == nil
{ ctx = context.Background() }" (any method in the responsecache package that
accepts a ctx parameter), change it to validate ctx != nil and return an error
(or propagate an error type) immediately when ctx is nil; update the function
signature to return an error if it doesn't already (or document panics if you
prefer), and update all callers to pass a non-nil context or handle the returned
error so request-scoped values are preserved.

In `@internal/server/translated_inference_service.go`:
- Around line 183-199: The current withCacheRequestContext uses s.guardrailsHash
as a fallback and only writes a guardrails hash into the context when
guardrailsHash != "", which lets an incoming context keep a stale caller hash
when plan.Policy is present but plan.GuardrailsHash() returns "". Update
withCacheRequestContext so that when a plan exists and plan.Policy != nil you
always call core.WithGuardrailsHash(ctx, plan.GuardrailsHash()) to explicitly
overwrite/clear any inherited hash (even if empty); otherwise, if no plan
policy, fall back to applying s.guardrailsHash only when non-empty (using
core.WithGuardrailsHash). This ensures guardrail-origin requests can clear the
caller’s cache namespace.
- Around line 164-167: Remove the redundant conditional around setting the
request context: always call c.SetRequest(c.Request().WithContext(ctx)) after
obtaining ctx from withCacheRequestContext(c.Request().Context(), plan). The
withCacheRequestContext function always returns a wrapped context (it uses
core.WithExecutionPlan and may add core.WithGuardrailsHash), so drop the if ctx
!= c.Request().Context() check and unconditionally update the request context
via c.SetRequest; this matches the pattern used elsewhere in the file.

---

Outside diff comments:
In `@internal/guardrails/factory.go`:
- Around line 60-90: The constructors accept variadic ChatCompletionExecutor but
the implementation (NewService) only uses executors[0], so callers can silently
pass extra executors; add a fast-fail check to reject multiple executors. In
newResult (and optionally in New and NewWithSharedStorage) check if
len(executors) > 1 and return a clear error like "only one
ChatCompletionExecutor is supported" before any work is done; this enforces the
single-executor contract and avoids silently dropping extra executors (refer to
the functions New, NewWithSharedStorage, newResult and the
ChatCompletionExecutor parameter and NewService usage).
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3408508-3b14-4fd9-9408-61ade1a71bce

📥 Commits

Reviewing files that changed from the base of the PR and between 3f81d47 and 80c1274.

📒 Files selected for processing (33)
  • README.md
  • config/config.example.yaml
  • config/config.go
  • docs/advanced/guardrails.mdx
  • go.mod
  • internal/admin/dashboard/static/js/modules/guardrails.js
  • internal/admin/dashboard/static/js/modules/guardrails.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler_guardrails_test.go
  • internal/app/app.go
  • internal/auditlog/entry_capture.go
  • internal/auditlog/middleware.go
  • internal/core/context.go
  • internal/guardrails/definitions.go
  • internal/guardrails/executor.go
  • internal/guardrails/factory.go
  • internal/guardrails/llm_based_altering.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/guardrails/provider.go
  • internal/guardrails/provider_test.go
  • internal/guardrails/service.go
  • internal/guardrails/service_test.go
  • internal/guardrails/system_prompt.go
  • internal/responsecache/responsecache.go
  • internal/server/execution_plan_helpers.go
  • internal/server/execution_policy.go
  • internal/server/execution_policy_test.go
  • internal/server/internal_chat_completion_executor.go
  • internal/server/internal_chat_completion_executor_test.go
  • internal/server/model_validation.go
  • internal/server/native_batch_service.go
  • internal/server/translated_inference_service.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 96-101: CaptureInternalJSONExchange currently calls
internalJSONAuditRequest and internalJSONAuditResponse unconditionally which
causes headers-only auditing to still attempt body marshaling and lose synthetic
header metadata when bodies are nil/unserializable; update
internalJSONAuditRequest and internalJSONAuditResponse to always construct and
return header/request metadata (including synthetic trace/user-path/X-Request-ID
via requestIDForEntry) and add a logBodies boolean parameter (pass cfg.LogBodies
from CaptureInternalJSONExchange) so the helpers only perform JSON marshaling of
bodies when logBodies is true, then call PopulateRequestData and
PopulateResponseData with the metadata and marshaled body only when available;
apply the same change to the other call sites referenced (the other
CaptureInternalJSONExchange usages).

In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 154-174: The test TestLLMBasedAltering_Process_SkipsPrefix can
return a false positive because Process is fail-open on executor errors; update
the mockChatCompletionExecutor chatFn used in NewLLMBasedAlteringGuardrail to
explicitly fail the test if invoked (e.g., call t.Fatalf or set a called flag
and assert it is false after Process), so you verify the executor is never
called when SkipContentPrefix matches; reference
mockChatCompletionExecutor.chatFn, NewLLMBasedAlteringGuardrail and Process to
locate where to inject the fail-fast assertion.

In `@internal/guardrails/provider.go`:
- Around line 923-932: The function_call_output branch currently flattens
item.Output to text via stringifyResponsesValue and then writes that text back
into the message, losing original types; change the assignment so we preserve
typed values: call stringifyResponsesValue(item.Output) as before, then try to
json.Unmarshal the resulting content string into a temporary interface{}; if
unmarshalling succeeds use that decoded value for preserved.Output (or
Message.Content/Output field as appropriate), otherwise fall back to the
original item.Output (so arrays/objects/numbers/null remain typed) — update the
logic around stringifyResponsesValue, item.Output, preserved.Output (and the
case "function_call_output" handling) to implement this.

In `@internal/responsecache/responsecache.go`:
- Around line 242-252: The mapping in internalCacheType is fragile because it
checks hardcoded strings ("HIT (exact)" / "HIT (semantic)") that are composed
elsewhere in writeCachedResponse; define canonical header-value constants (e.g.,
CacheHeaderExact, CacheHeaderSemantic) next to CacheTypeExact and
CacheTypeSemantic in semantic.go and update both writeCachedResponse to use
those constants when writing the header and internalCacheType to compare against
them (instead of the current literal strings) so there is a single source of
truth for the header formats.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dce8d1a8-9ed0-4722-95bd-2df02a60d7a3

📥 Commits

Reviewing files that changed from the base of the PR and between 80c1274 and 25ebca3.

📒 Files selected for processing (14)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler_guardrails_test.go
  • internal/auditlog/entry_capture.go
  • internal/guardrails/factory.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/guardrails/provider.go
  • internal/guardrails/service.go
  • internal/guardrails/service_test.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go
  • internal/server/translated_inference_service.go
  • internal/server/translated_inference_service_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 117-145: internalJSONAuditRequest currently only attaches a
RequestSnapshot to the context when logBodies is true and json.Marshal succeeds,
which can leave an ingress snapshot in place and leak outer request data; change
internalJSONAuditRequest to always create and attach a synthetic
core.RequestSnapshot whenever logBodies is true (use core.NewRequestSnapshot and
core.WithRequestSnapshot), setting capturedBody to the marshaled bytes on
success and to an empty or nil captured body with bodyTooBig/body-not-captured
flags set appropriately on nil body or marshal failure; update the function to
compute capturedBody/bodyTooBig deterministically and attach the snapshot to
reqCtx regardless of marshal outcome, and add a regression test that pre-seeds
an ingress snapshot and exercises an unmarshalable internal body to assert the
internal snapshot is used.

In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 300-347: Replace raw int32 counters with sync/atomic helper types
inside TestLLMBasedAltering_Process_LimitsConcurrentRewrites: change inFlight,
maxInFlight, and requestsSeen to atomic.Int32, update increments/decrements to
inFlight.Add(1)/inFlight.Add(-1) and requestsSeen.Add(1), replace
atomic.LoadInt32(&maxInFlight)/atomic.CompareAndSwapInt32(&maxInFlight,
previous, current) in the mockChatCompletionExecutor.chatFn CAS loop with
maxInFlight.Load() and maxInFlight.CompareAndSwap(previous, current) (or use
maxInFlight.Swap/Store as appropriate), and update the final checks to use
requestsSeen.Load() and maxInFlight.Load(); keep all logic identical but use the
atomic.Int32 methods for cleaner code.

In `@internal/guardrails/provider.go`:
- Around line 873-900: In coerceResponsesInputElements, remove the intermediate
[]any allocation in the case []map[string]any branch and instead directly
convert each map to a core.ResponsesInputElement: allocate elements :=
make([]core.ResponsesInputElement, len(typed)) and for each map in the input
marshal the map to JSON and unmarshal into elements[i], returning elements, nil;
update the case []map[string]any in function coerceResponsesInputElements
accordingly and preserve error handling that returns core.NewInvalidRequestError
on marshal/unmarshal failures.

In `@internal/responsecache/responsecache.go`:
- Around line 179-185: The current fallback that silently calls echo.New() when
m.echo is nil should be removed because it masks constructor bugs; instead,
detect when m != nil && m.echo == nil and log a warning or error (using the
package logger e.g., response cache's logger or the standard log) indicating the
unexpected nil m.echo (include context like the ResponseCache instance ID or
constructor used), and then fail-fast by returning an error or nil to the caller
rather than creating a new Echo instance; update the calling code or the
constructor (e.g., NewResponseCache / any New* functions that initialize m.echo)
to ensure m.echo is always initialized so this branch never executes.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21eacc1e-59ef-4ee7-92e4-a9f6980498e6

📥 Commits

Reviewing files that changed from the base of the PR and between 25ebca3 and 072a120.

📒 Files selected for processing (8)
  • internal/auditlog/entry_capture.go
  • internal/auditlog/entry_capture_test.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/guardrails/provider.go
  • internal/guardrails/provider_test.go
  • internal/responsecache/responsecache.go
  • internal/responsecache/semantic.go
  • internal/responsecache/stream_cache.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@internal/auditlog/entry_capture_test.go`:
- Around line 57-84: The test only covers the marshal-failure/no-body path for
CaptureInternalJSONExchange; add two more subtests in
TestCaptureInternalJSONExchange_PreservesHeadersWhenBodyMarshalFails (or new
tests) that exercise the responseErr branch and the oversized/truncation branch:
1) call CaptureInternalJSONExchange with a non-nil responseErr value and assert
that entry.Data.ResponseError (or the field your helper sets for synthesized
errors) is populated and headers are still preserved; 2) simulate an oversized
payload/truncation by using the helper path that marks payloads as truncated (or
set the config/inputs that trigger the oversized marker) and assert that
RequestBody/ResponseBody are replaced with the truncation marker while headers
remain intact. Reference CaptureInternalJSONExchange, Config, and the
responseErr/synthesized-error/truncation logic when adding assertions so the new
tests cover both branches.

In `@internal/guardrails/provider.go`:
- Around line 1017-1019: Remove the no-op assignment "_ = typed" in the switch's
default branch and instead ignore the value at the switch declaration or case
pattern so the compiler doesn't require silencing; specifically locate the
switch that declares the variable "typed" and change the case that falls to
default to use the blank identifier (or bind to _ in the case pattern) rather
than assigning "_ = typed" in the default branch, then delete the "_ = typed"
line.

In `@internal/responsecache/responsecache.go`:
- Around line 180-188: The nil-receiver branch currently creates a new Echo
instance which hides a call-site bug; change HandleInternalRequest to fail fast
when m == nil by logging an error and returning a provider/internal server error
instead of allocating echo.New(); keep the existing behavior for the case where
m != nil but m.echo == nil (log and return provider error as done now). Update
the switch handling in HandleInternalRequest (the method on the response cache
type) to detect m == nil first and return an explicit error with a clear log
message rather than creating a new Echo.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 032aeb5e-921c-4975-b0d6-db642248c8de

📥 Commits

Reviewing files that changed from the base of the PR and between 072a120 and e1e438b.

📒 Files selected for processing (9)
  • config/config.example.yaml
  • config/config.go
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/auditlog/entry_capture.go
  • internal/auditlog/entry_capture_test.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/guardrails/provider.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +193 to +197
if m == nil {
err = next(c)
} else {
err = m.HandleRequest(c, body, func() error { return next(c) })
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

HandleInternalRequest already returns an error when the receiver is nil (switch case m == nil), so the later if m == nil { ... } else { ... } branch is unreachable on the m == nil path. This makes the control flow harder to reason about and can be simplified by removing the dead branch and directly running m.HandleRequest after the nil/uninitialized checks (or by making HandleInternalRequest a standalone function if nil receivers should be supported).

Suggested change
if m == nil {
err = next(c)
} else {
err = m.HandleRequest(c, body, func() error { return next(c) })
}
err = m.HandleRequest(c, body, func() error { return next(c) })

Copilot uses AI. Check for mistakes.
Comment on lines +548 to +555
if strings.Contains(text, alteringTextWrapperStart) {
text = strings.Replace(text, alteringTextWrapperStart, "", 1)
text = strings.TrimPrefix(text, "\n")
}
if strings.Contains(text, alteringTextWrapperEnd) {
text = strings.Replace(text, alteringTextWrapperEnd, "", 1)
text = strings.TrimSuffix(text, "\n")
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

unwrapAlteredText removes wrapper tokens using strings.Contains + strings.Replace, which will also strip the tokens if they appear in the actual rewritten content (e.g., user text legitimately contains <TEXT_TO_ALTER>). To avoid data loss, only remove wrappers when they are at the expected boundaries (e.g., HasPrefix/HasSuffix with the newline framing produced by wrapAlteringText), or otherwise parse more strictly and leave interior occurrences untouched.

Suggested change
if strings.Contains(text, alteringTextWrapperStart) {
text = strings.Replace(text, alteringTextWrapperStart, "", 1)
text = strings.TrimPrefix(text, "\n")
}
if strings.Contains(text, alteringTextWrapperEnd) {
text = strings.Replace(text, alteringTextWrapperEnd, "", 1)
text = strings.TrimSuffix(text, "\n")
}
prefix := alteringTextWrapperStart + "\n"
suffix := "\n" + alteringTextWrapperEnd
if strings.HasPrefix(text, prefix) && strings.HasSuffix(text, suffix) {
return strings.TrimSuffix(strings.TrimPrefix(text, prefix), suffix)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
internal/auditlog/entry_capture_test.go (1)

145-162: ⚠️ Potential issue | 🟡 Minor

Assert the oversized response was actually truncated.

ResponseBody != nil still passes if the full oversized payload is retained, and this branch never rechecks the user-path header it is supposed to preserve. Assert the truncation marker/shape here (or otherwise prove the full payload was not kept) plus core.UserPathHeader.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/entry_capture_test.go` around lines 145 - 162, The test
currently only checks ResponseBody != nil and ResponseBodyTooBigToHandle, which
doesn’t prove the oversized response was truncated or that the user-path header
was preserved; update the assertions around entry.Data.ResponseBody and
entry.Data.ResponseHeaders to (a) assert the ResponseBody shows the expected
truncation shape/marker or is shorter than the original oversized payload (so
the full payload wasn’t retained) and (b) assert
entry.Data.ResponseHeaders[http.CanonicalHeaderKey(core.UserPathHeader)] equals
the preserved user-path value; keep the existing checks for
ResponseBodyTooBigToHandle and RequestBodyTooBigToHandle and use the
entry.Data.* symbols to locate the checks.
internal/guardrails/provider.go (1)

1055-1060: ⚠️ Potential issue | 🟠 Major

Restore function_call_output.output on the typed-envelope path too.

Only the map-backed patch path reconstructs typed JSON output. This branch still writes modified.Content verbatim, so a no-op guardrail pass turns object/array/number/null outputs into strings for []core.ResponsesInputElement requests.

💡 Minimal fix
 case "function_call_output":
 	if modified.Role != "tool" {
 		return core.ResponsesInputElement{}, core.NewInvalidRequestError("guardrails cannot reorder or retag responses input items", nil)
 	}
-	preserved.Output = modified.Content
+	preserved.Output = restoreResponsesInputOutputValue(original.Output, modified.Content)
 	return preserved, nil

Also applies to: 1156-1157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/guardrails/provider.go` around lines 1055 - 1060, In the
"function_call_output" branch the code assigns preserved.Output =
modified.Content, which collapses typed JSON outputs into strings on the
typed-envelope path; change this to restore the original typed value (assign
preserved.Output from the field that holds the reconstructed/typed output rather
than modified.Content) so that object/array/number/null values survive no-op
guardrail passes—update the same pattern at the other occurrence referenced (the
similar assignment at the later branch around lines 1156-1157) and ensure you
use the typed output field (the value used by the map-backed patch path) when
setting preserved.Output.
internal/responsecache/responsecache.go (1)

179-199: ⚠️ Potential issue | 🟠 Major

Remove the unreachable nil-receiver branch; it is causing the current SA5011 failure.

HandleInternalRequest already returns when m == nil, so the later if m == nil block can never run. Keeping that dead path leaves m/e possibly nil in staticcheck's view and is what the linter is flagging now.

♻️ Simplify the guard flow
 	rec := httptest.NewRecorder()
-	var e *echo.Echo
-	switch {
-	case m == nil:
+	if m == nil {
 		slog.Error("response cache: HandleInternalRequest called on nil middleware")
 		return nil, core.NewProviderError("", http.StatusInternalServerError, "response cache middleware is not initialized", nil)
-	case m.echo == nil:
+	}
+	if m.echo == nil {
 		slog.Error("response cache: HandleInternalRequest called with uninitialized Echo instance")
 		return nil, core.NewProviderError("", http.StatusInternalServerError, "response cache middleware is not initialized", nil)
-	default:
-		e = m.echo
 	}
-	c := e.NewContext(req, rec)
+	c := m.echo.NewContext(req, rec)
 
-	var err error
-	if m == nil {
-		err = next(c)
-	} else {
-		err = m.HandleRequest(c, body, func() error { return next(c) })
-	}
-	if err != nil {
+	if err := m.HandleRequest(c, body, func() error { return next(c) }); err != nil {
 		return nil, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/responsecache/responsecache.go` around lines 179 - 199, The second
nil-receiver branch is unreachable and triggers SA5011; remove the later "if m
== nil { err = next(c) } else { err = m.HandleRequest(...)" block and always
call m.HandleRequest(c, body, func() error { return next(c) }) after you create
the Echo context (since the function already returned when m == nil), using the
previously assigned e and c; this ensures HandleInternalRequest,
m.HandleRequest, next and e.NewContext are used consistently and eliminates the
dead m==nil path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/app/app.go`:
- Around line 411-423: The failure branches after calling
guardrailResult.Service.SetExecutor and executionPlanResult.Service.Refresh
forget to close the response cache middleware (rcm) opened earlier by
responsecache.NewResponseCacheMiddleware; update both error cleanup sequences to
include rcm.Close() in the errors.Join(...) calls (and any subsequent closeErr
handling) so rcm is always closed when SetExecutor or Refresh fails.

In `@internal/auditlog/entry_capture_test.go`:
- Around line 105-121: The test currently only checks X-Request-ID and
error.message; extend it to assert the full synthesized-error contract by
verifying that entry.Data.RequestHeaders and entry.Data.ResponseHeaders include
core.UserPathHeader (using http.CanonicalHeaderKey lookup) and that
entry.Data.ResponseBody (the synthesized error envelope) contains an "error"
object with all OpenAI-compatible fields "type", "message", "param", and "code"
(with expected values or non-nil presence as appropriate) so the test fails if
any of those fields or headers are omitted.

In `@internal/executionplans/service_test.go`:
- Around line 625-628: Replace the inline map[string]any payload used in the
test (currently passed to mustMarshalJSON) with a small strongly-typed struct
(e.g., ModelConfig or TestModelConfig) that has fields matching the shape: Model
string and Roles []string; instantiate that struct with Model: "gpt-4o-mini" and
Roles: []string{"user"} and pass it to mustMarshalJSON instead of the map to
ensure compile-time safety and easier evolution of the payload shape.

In `@internal/responsecache/responsecache.go`:
- Around line 246-254: internalCacheType currently only matches exact header
values and returns empty for the "HIT (<cacheType>)" shape emitted by
stream_cache; update internalCacheType to parse that generic form: trim the
header, if it equals CacheHeaderExact or CacheHeaderSemantic return the
corresponding CacheType; else if it matches the pattern "HIT (<inner>)" (use
strings.HasPrefix/HasSuffix to detect and extract the inner value), trim the
inner value and run the same switch on it to return
CacheTypeExact/CacheTypeSemantic; otherwise return "" — make changes inside
internalCacheType to keep all other call sites unchanged.

---

Duplicate comments:
In `@internal/auditlog/entry_capture_test.go`:
- Around line 145-162: The test currently only checks ResponseBody != nil and
ResponseBodyTooBigToHandle, which doesn’t prove the oversized response was
truncated or that the user-path header was preserved; update the assertions
around entry.Data.ResponseBody and entry.Data.ResponseHeaders to (a) assert the
ResponseBody shows the expected truncation shape/marker or is shorter than the
original oversized payload (so the full payload wasn’t retained) and (b) assert
entry.Data.ResponseHeaders[http.CanonicalHeaderKey(core.UserPathHeader)] equals
the preserved user-path value; keep the existing checks for
ResponseBodyTooBigToHandle and RequestBodyTooBigToHandle and use the
entry.Data.* symbols to locate the checks.

In `@internal/guardrails/provider.go`:
- Around line 1055-1060: In the "function_call_output" branch the code assigns
preserved.Output = modified.Content, which collapses typed JSON outputs into
strings on the typed-envelope path; change this to restore the original typed
value (assign preserved.Output from the field that holds the reconstructed/typed
output rather than modified.Content) so that object/array/number/null values
survive no-op guardrail passes—update the same pattern at the other occurrence
referenced (the similar assignment at the later branch around lines 1156-1157)
and ensure you use the typed output field (the value used by the map-backed
patch path) when setting preserved.Output.

In `@internal/responsecache/responsecache.go`:
- Around line 179-199: The second nil-receiver branch is unreachable and
triggers SA5011; remove the later "if m == nil { err = next(c) } else { err =
m.HandleRequest(...)" block and always call m.HandleRequest(c, body, func()
error { return next(c) }) after you create the Echo context (since the function
already returned when m == nil), using the previously assigned e and c; this
ensures HandleInternalRequest, m.HandleRequest, next and e.NewContext are used
consistently and eliminates the dead m==nil path.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98fd0f2e-a9f7-481d-ae4b-4331d0483d57

📥 Commits

Reviewing files that changed from the base of the PR and between e1e438b and b29838c.

📒 Files selected for processing (6)
  • internal/app/app.go
  • internal/auditlog/entry_capture_test.go
  • internal/executionplans/service_test.go
  • internal/guardrails/provider.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 169-180: The current switch in CaptureInternalJSONExchange
prioritizes bodyValue over responseErr causing partial successful audit entries;
update the logic in CaptureInternalJSONExchange to check responseErr first and,
if non-nil, marshal the error envelope (using the same errors.As gatewayErr
handling and core.NewProviderError(...).ToJSON() fallback) into body, otherwise
fall back to marshalling bodyValue; keep the existing default return behavior
unchanged.

In `@internal/executionplans/service_test.go`:
- Around line 769-785: The test helper assertPipelineRewrite currently only
checks the rewritten message content; add an assertion to validate the message
role wasn't mutated. After calling pipeline.Process and before checking content,
assert that msgs[0].Role equals the original role ("user") (or change the helper
signature to accept a wantRole and assert msgs[0].Role == wantRole) so the test
fails if the rewrite changes role semantics; reference assertPipelineRewrite and
the guardrails.Message produced by pipeline.Process when adding this additional
check.

In `@internal/guardrails/llm_based_altering.go`:
- Around line 484-495: The current acceptance of the first choice in
llm_based_altering.go can let truncated or non-terminal auxiliary completions
(e.g., finish_reason != "stop") replace the original text; update the validation
to reject choices whose FinishReason is not the terminal value (e.g., not
"stop") by checking resp.Choices[0].FinishReason (and ideally any other selected
choice) and returning an error (so the caller falls back to fail-open) before
extracting content or returning unwrapAlteredText(content); keep the existing
checks for nil resp, tool calls, and empty content.

In `@internal/responsecache/responsecache.go`:
- Around line 244-248: The switch on headerValue contains dead comparisons to
CacheHeaderExact and CacheHeaderSemantic because headerValue has already been
stripped of the "HIT (...)" wrapper; remove the CacheHeaderExact and
CacheHeaderSemantic cases and only compare against CacheTypeExact and
CacheTypeSemantic (referencing the headerValue variable and the constants
CacheTypeExact/CacheTypeSemantic) so the switch is simplified to the effective,
reachable branches.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3090e2c3-556e-48b2-bd5d-0d7525d17923

📥 Commits

Reviewing files that changed from the base of the PR and between e1e438b and c42fe8d.

📒 Files selected for processing (10)
  • internal/app/app.go
  • internal/auditlog/entry_capture.go
  • internal/auditlog/entry_capture_test.go
  • internal/executionplans/service_test.go
  • internal/guardrails/llm_based_altering.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/guardrails/provider.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go
  • tests/perf/hotpath_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/templates/execution-plan-chart.html`:
- Line 65: The connector for async audit flows (the div with class "ep-conn
ep-conn-async") is currently hidden by x-show="{{.}}.showUsage &&
{{.}}.showAudit", which makes audit-only flows invisible when showAudit is true
but showUsage is false; change the x-show to rely on audit visibility (e.g.,
x-show="{{.}}.showAudit") or to show when either flag is set (e.g.,
x-show="{{.}}.showUsage || {{.}}.showAudit") so the Audit node remains
connected; update the attribute on the element with class ep-conn-async and
ensure this aligns with the flags set in execution-plans.js (showUsage and
showAudit).

In `@internal/auditlog/entry_capture_test.go`:
- Around line 48-50: The test inconsistently accesses the Traceparent header
directly; change the lookup to use http.CanonicalHeaderKey("Traceparent") like
the other assertions so it reads
entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")] and keep the
same t.Fatal message; this keeps the pattern consistent with the other checks
that use http.CanonicalHeaderKey.

In `@internal/responsecache/responsecache.go`:
- Around line 221-237: internalRequestHeaders currently copies all headers from
snapshot.GetHeaders(), which can leak sensitive credentials (Authorization,
X-Api-Key, Cookie, etc.); change it to filter using an explicit allowlist (e.g.,
Content-Type, Accept, User-Agent, standard tracing headers like
Traceparent/Request-Id and X-Request-ID) instead of copying everything from
snapshot.GetHeaders(), and preserve the existing behavior that sets Content-Type
and X-Request-ID when absent; update the logic in internalRequestHeaders to
iterate snapshot.GetHeaders() but only Add values for keys present in the
allowlist and ignore/omit sensitive names such as Authorization, Cookie, and
X-Api-Key.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80f584e4-d175-4f81-9563-cf6fff09c235

📥 Commits

Reviewing files that changed from the base of the PR and between c42fe8d and d5d27d7.

📒 Files selected for processing (8)
  • internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
  • internal/admin/dashboard/templates/execution-plan-chart.html
  • internal/auditlog/entry_capture.go
  • internal/auditlog/entry_capture_test.go
  • internal/executionplans/service_test.go
  • internal/guardrails/llm_based_altering.go
  • internal/guardrails/llm_based_altering_test.go
  • internal/responsecache/responsecache.go

<span class="ep-node-label">Usage</span>
</div>
<div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div>
<div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div>
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.

⚠️ Potential issue | 🟡 Minor

Restore connector visibility for audit-only async flows

At Line 65, gating the connector with showUsage && showAudit hides it in valid states where showAudit=true and showUsage=false (see internal/admin/dashboard/static/js/modules/execution-plans.js:1088-1122, where those flags are independently derived). This leaves the Audit node visually disconnected.

Suggested fix
-            <div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div>
+            <div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div>
<div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/execution-plan-chart.html` at line 65, The
connector for async audit flows (the div with class "ep-conn ep-conn-async") is
currently hidden by x-show="{{.}}.showUsage && {{.}}.showAudit", which makes
audit-only flows invisible when showAudit is true but showUsage is false; change
the x-show to rely on audit visibility (e.g., x-show="{{.}}.showAudit") or to
show when either flag is set (e.g., x-show="{{.}}.showUsage || {{.}}.showAudit")
so the Audit node remains connected; update the attribute on the element with
class ep-conn-async and ensure this aligns with the flags set in
execution-plans.js (showUsage and showAudit).

Comment on lines +48 to +50
if got := entry.Data.RequestHeaders["Traceparent"]; got == "" {
t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header")
}
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.

🧹 Nitpick | 🔵 Trivial

Inconsistent header key access compared to other assertions.

Lines 42-46 use http.CanonicalHeaderKey() for header lookups, but line 48 accesses "Traceparent" directly. While http.CanonicalHeaderKey("Traceparent") happens to return "Traceparent", using the function consistently makes the pattern clearer and more maintainable.

♻️ Minor consistency fix
-	if got := entry.Data.RequestHeaders["Traceparent"]; got == "" {
+	if got := entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")]; got == "" {
 		t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if got := entry.Data.RequestHeaders["Traceparent"]; got == "" {
t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header")
}
if got := entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")]; got == "" {
t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/entry_capture_test.go` around lines 48 - 50, The test
inconsistently accesses the Traceparent header directly; change the lookup to
use http.CanonicalHeaderKey("Traceparent") like the other assertions so it reads
entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")] and keep the
same t.Fatal message; this keeps the pattern consistent with the other checks
that use http.CanonicalHeaderKey.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/responsecache/responsecache.go`:
- Around line 23-36: The internalRequestHeaderAllowlist currently omits the
standard "Cache-Control" header, so HandleInternalRequest can forward requests
that then bypass the same cache-bypass behavior used by HandleRequest; add
http.CanonicalHeaderKey("Cache-Control") to internalRequestHeaderAllowlist in
internal/responsecache/responsecache.go so internal requests preserve the
standard Cache-Control semantics, and update the new allowlist unit test (the
test that asserts forwarded headers for internal requests) to include a case
asserting "Cache-Control" is allowed and preserved.
- Around line 205-208: m.HandleRequest's returned error must be normalized to a
core.GatewayError before returning: after calling err := m.HandleRequest(...),
if err != nil use errors.As to check whether err is already a *core.GatewayError
and if so return it unchanged, otherwise wrap it in a new core.GatewayError
(preserving the original message/cause) and return that; add errors to the
imports. Ensure you reference m.HandleRequest and core.GatewayError in the
change.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 71e10060-4d9c-4fa9-9b96-26ffe76d43fa

📥 Commits

Reviewing files that changed from the base of the PR and between d5d27d7 and 500272f.

📒 Files selected for processing (2)
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go

Comment on lines +205 to +208
err := m.HandleRequest(c, body, func() error { return next(c) })
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.

⚠️ Potential issue | 🟠 Major

Wrap non-core.GatewayError failures before returning here.

This transport-free path no longer goes through Echo's HTTP error handler, so returning m.HandleRequest's error verbatim lets plain cache/backend/provider errors escape unless every caller normalizes them. Preserve existing *core.GatewayErrors, but wrap unknown errors here before returning.

🩹 Suggested change
 err := m.HandleRequest(c, body, func() error { return next(c) })
 if err != nil {
-	return nil, err
+	var gatewayErr *core.GatewayError
+	if errors.As(err, &gatewayErr) {
+		return nil, gatewayErr
+	}
+	slog.Error("response cache: internal request failed", "error", err)
+	return nil, core.NewProviderError("", http.StatusInternalServerError, "internal cache request failed", nil)
 }

You'll also need to add errors to the imports.

As per coding guidelines: "All errors returned to clients must be instances of core.GatewayError."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/responsecache/responsecache.go` around lines 205 - 208,
m.HandleRequest's returned error must be normalized to a core.GatewayError
before returning: after calling err := m.HandleRequest(...), if err != nil use
errors.As to check whether err is already a *core.GatewayError and if so return
it unchanged, otherwise wrap it in a new core.GatewayError (preserving the
original message/cause) and return that; add errors to the imports. Ensure you
reference m.HandleRequest and core.GatewayError in the change.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
internal/responsecache/responsecache.go (1)

208-214: ⚠️ Potential issue | 🟠 Major

Don't expose raw internal failures in the public provider error.

This path now normalizes unknown errors to *core.GatewayError, but using err.Error() as the public message still leaks cache/backend internals to clients. Log the original failure server-side and return a stable message instead; TestHandleInternalRequest_NormalizesNonGatewayErrors can keep asserting errors.Is(...) without pinning the public text.

🩹 Minimal fix
 	if err != nil {
 		var gatewayErr *core.GatewayError
 		if errors.As(err, &gatewayErr) && gatewayErr != nil {
 			return nil, gatewayErr
 		}
-		return nil, core.NewProviderError("", http.StatusInternalServerError, err.Error(), err)
+		slog.Error("response cache: internal request failed", "error", err)
+		return nil, core.NewProviderError("", http.StatusInternalServerError, "internal cache request failed", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/responsecache/responsecache.go` around lines 208 - 214, The code
currently returns core.NewProviderError with err.Error(), leaking internal
cache/backend details; change the return to use a stable public message (e.g.
"internal server error") instead of err.Error(), and log the original err
server-side before returning (use the existing logger in this package or the
surrounding context). Update the branch that checks errors.As(err,
&core.GatewayError) to keep returning gatewayErr unchanged, but for the generic
path replace core.NewProviderError("", http.StatusInternalServerError,
err.Error(), err) with core.NewProviderError("", http.StatusInternalServerError,
"internal server error", err) and add a server-side log of err; ensure
TestHandleInternalRequest_NormalizesNonGatewayErrors still uses errors.Is(...)
and does not depend on the public message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/responsecache/responsecache.go`:
- Around line 208-214: The code currently returns core.NewProviderError with
err.Error(), leaking internal cache/backend details; change the return to use a
stable public message (e.g. "internal server error") instead of err.Error(), and
log the original err server-side before returning (use the existing logger in
this package or the surrounding context). Update the branch that checks
errors.As(err, &core.GatewayError) to keep returning gatewayErr unchanged, but
for the generic path replace core.NewProviderError("",
http.StatusInternalServerError, err.Error(), err) with core.NewProviderError("",
http.StatusInternalServerError, "internal server error", err) and add a
server-side log of err; ensure
TestHandleInternalRequest_NormalizesNonGatewayErrors still uses errors.Is(...)
and does not depend on the public message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: de6073ea-a089-484a-b6e3-d401ba3abca5

📥 Commits

Reviewing files that changed from the base of the PR and between 500272f and ad0f6d8.

📒 Files selected for processing (2)
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/responsecache.go

@SantiagoDePolonia SantiagoDePolonia merged commit dc2a5f4 into main Apr 5, 2026
16 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