Skip to content

feat(pii): apply PII middleware to the Open Responses API - #12708

Merged
mudler merged 3 commits into
mudler:masterfrom
walcz-de:feat/pii-open-responses
Oct 19, 2026
Merged

mudler merged 3 commits into
mudler:masterfrom
walcz-de:feat/pii-open-responses

Conversation

@walcz-de

Copy link
Copy Markdown
Contributor

Description

The PII middleware (pii.RequestMiddleware) protects /v1/chat/completions, /v1/completions, /v1/mcp/chat/completions, the Anthropic and the Ollama routes, but not the Open Responses API. A deployment that blocks or masks PII with a detector policy could therefore be bypassed by sending the same content to POST /v1/responses (or /responses).

This PR:

  • adds piiadapter.OpenResponses() for the Open Responses request schema: instructions, input as a string, and input items — message items (string content or input_text/output_text parts), reasoning content, function_call arguments and function_call_output (string, text parts, or object output scanned as the JSON the handler sends to the model). Image/file/audio/video parts are left untouched, like in the other adapters.
  • wires the middleware into the POST responses chain after model resolution (same position as on the chat route). The chain moved into openResponsesMiddleware() so the route test runs the real chain.
  • WebSocket GET /v1/responses: messages on the socket do not pass through HTTP middleware. Rather than serving them unfiltered, the socket handler now refuses with pii_unsupported_transport when the model has PII enabled with at least one detector (the same condition the HTTP middleware uses). Models without a PII policy are served as before.
  • docs: docs/content/operations/middleware.md lists /v1/responses, the covered fields and the known limits (requests whose model names an agent are routed by AgentResponsesInterceptor before model resolution and stay unfiltered, as before).

Notes for Reviewers

  • Tests (Ginkgo): core/services/routing/piiadapter 30/30 (12 new), core/services/routing/pii 76/76, core/http/endpoints/openresponses 68/68 (3 new), core/http/routes 203/203 (4 new). Each detect/redact spec has a counter-check that clean input stays byte-identical. Removing the middleware turns 3 of 4 route specs red; disabling the WebSocket guard turns the refusal spec red.
  • Not run locally: make lint (golangci-lint not installed here) and the e2e suites.
  • Behaviour change worth a look: cloud-proxy models have PII on by default, so with instance default detectors they now get the refusal over the WebSocket transport (HTTP keeps working). Happy to scope that differently if you prefer.

Signed commits

  • Yes, I signed my commits.
  • Documentation updated (docs/content/) for user-facing changes, or not applicable

🤖 Generated with Claude Code

walcz-de added a commit to walcz-de/LocalAI that referenced this pull request Oct 15, 2026
@localai-org-maint-bot

Copy link
Copy Markdown
Collaborator

@mudler Code-review sign-off at 93f96c239d19627ec40476a4cd5ef92eb6c27f40, subject to CI validation and human DCO certification of the fix commit.

I found and fixed one bypass in 93f96c239: the handler forwards every non-string tool output as JSON, but the adapter scanned arrays only as typed text parts. Values such as output:["sk-secret"] or output:[{"token":"sk-secret"}] therefore escaped scanning. The adapter now scans the complete JSON for non-string outputs. Added array redaction, clean-output, and route-blocking regression cases.

The docs also clarify an existing limitation: stored history from previous_response_id and item_reference is resolved after filtering and is not scanned against the current model's policy. The new request filter does not establish complete filtering of historical prompts.

Reviewed against master bee88623d5b7e0e1bc9b09c5e6aae67b6af5cd61. No remaining blocking source findings within this change. The HTTP middleware follows model resolution, and the WebSocket guard uses the effective PII policy.

Validation: an isolated module running the actual adapter and its actual Ginkgo specs, with only schema/PII carrier types stubbed, produced 12 passing and four failing specs before the fix, then 16 passing specs afterward. I independently repeated the passing run. This does not validate route or WebSocket integration. The repository package tests failed to compile because generated protobuf types (Animate3DRequest, MotionRequest, MotionResponse) are missing; the relevant source/generated files match master. Diff whitespace checks pass.

The fix commit carries Assisted-by attribution but no fabricated human Signed-off-by trailer. A human must certify that commit, and the full CI suite still needs to validate the branch before merging.

@walcz-de

Copy link
Copy Markdown
Contributor Author

Pushed two commits on top: the first is #12755 (pii.block_unscanned_media, OpenAI/Anthropic/Ollama shapes) as the base, the second extends the same option to /v1/responses (input_image/input_file/input_audio parts and bare items; tool output stays allowed since it is scanned as JSON). If you prefer to keep this PR to the original scope, I can move the second commit into #12755 once this one lands.

walcz-de and others added 3 commits October 18, 2026 22:22
The per-model PII filter ran on /v1/chat/completions, /v1/completions,
/v1/messages and the Ollama routes, but not on POST /v1/responses. A
model whose policy blocks or masks a secret on the chat route served the
same content unfiltered when it was sent through the Responses API.

Add a piiadapter.OpenResponses adapter and wire pii.RequestMiddleware
into the POST /v1/responses and /responses chain, after the model config
is resolved and merged and before the handler converts input items into
prompt messages. The adapter covers instructions, input as a string, and
in an input item array the text of message and reasoning items (string
content or input_text/output_text parts, including typeless
{role, content} items), function_call arguments and function_call_output
output; structured tool output is scanned as the JSON the handler
forwards. Image, file, audio and video parts are left untouched.

The Responses WebSocket mode (GET /v1/responses) is not HTTP middleware
and cannot filter, so a response.create for a model with PII filtering
enabled is now refused with pii_unsupported_transport instead of being
served unfiltered. The docs list the new route and this limit.

Tests: adapter specs for every covered field, each with a clean-text
counter-check; a route spec driving the real middleware chain with an
in-process pattern detector (block on string input, content part,
instructions and tool output; mask; clean input passes unchanged); and
WebSocket specs proving the refusal and that models without a PII
policy are still served.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
The Responses handler forwards non-string tool output as JSON. Scan
that full encoding so string arrays and extra fields cannot bypass PII
filtering. Cover array scans, redaction, clean values and route blocking.

Document that stored history is resolved after request filtering.

Assisted-by: Codex:gpt-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Extend block_unscanned_media to /v1/responses: input_image, input_file
and input_audio parts inside message items, and the same types as bare
input items, are reported as unscanned content. Tool output stays
allowed, as it is scanned as JSON text.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
@walcz-de
walcz-de force-pushed the feat/pii-open-responses branch from 242e1cb to 407fb2c Compare October 18, 2026 20:23
@mudler
mudler merged commit d768f1f into mudler:master Oct 19, 2026
1 check 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.

3 participants