Summary
When using the Anthropic native API path (not OpenAI compat) with Presidio guardrail and output_parse_pii: true, several bugs prevent end-to-end PII masking from working. PII is masked on input but never unmasked in responses, and using tools (e.g. via Claude Code) causes a 400 error.
Symptoms
-
400 error — Input tag 'function' found using 'type' does not match expected tags: 'bash_20250124', 'text_editor_20250124', ...
Triggered when any tool is present in the request.
-
PII never unmasked in non-streaming responses — <PERSON> appears in output instead of the original name.
-
PII never unmasked in streaming responses on Anthropic native path — same issue, different code path.
Root Causes
-
Tool key contamination: apply_guardrail() returns the entire mutated inputs dict including OpenAI-converted tool keys (tools, structured_messages, model, images) added by process_input_messages(). On the Anthropic native path this overwrites native tool definitions with the OpenAI type:"function" format.
-
No unmask path in apply_guardrail(): Only a masking path exists — when called with input_type="response" and output_parse_pii=True, PII tokens are never restored.
-
Abstract interface out of sync: All process_output_response() implementations accept request_data but the abstract base (BaseGuardrailTranslation) does not declare the param, causing a type/mypy mismatch.
-
Streaming hook handles only ModelResponseStream: The async_post_call_streaming_iterator_hook unmasking path iterates chunks expecting ModelResponseStream objects. With the Anthropic native API path, response chunks are raw bytes (SSE format) — these pass through unprocessed, so unmasking never runs.
Additionally, AnthropicMessagesHandler.process_output_response() creates a fresh request_data dict, discarding any pii_tokens stored during input masking.
Config
model_list:
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-5 # native, not openai-compat
guardrails:
- guardrail_name: pii_masking
litellm_params:
guardrail: presidio
default_on: true
output_parse_pii: true
Provider: anthropic/ native (not via OpenAI compat)
Fix PRs
Each root cause is addressed in a separate PR (will link once submitted):
- PR A —
fix/guardrail-request-data-passthrough: Sync abstract BaseGuardrailTranslation.process_output_response() signature with all implementations (root cause 3)
- PR B —
fix/presidio-native-tool-stripping: Strip OpenAI-converted keys before returning from apply_guardrail() (root cause 1)
- PR C —
fix/presidio-output-unmask: Add output unmask path to apply_guardrail() (root cause 2)
- PR D —
fix/presidio-anthropic-sse-unmask: Handle Anthropic native SSE bytes in streaming unmask hook + fix AnthropicMessagesHandler (root cause 4)
Summary
When using the Anthropic native API path (not OpenAI compat) with Presidio guardrail and
output_parse_pii: true, several bugs prevent end-to-end PII masking from working. PII is masked on input but never unmasked in responses, and using tools (e.g. via Claude Code) causes a 400 error.Symptoms
400 error —
Input tag 'function' found using 'type' does not match expected tags: 'bash_20250124', 'text_editor_20250124', ...Triggered when any tool is present in the request.
PII never unmasked in non-streaming responses —
<PERSON>appears in output instead of the original name.PII never unmasked in streaming responses on Anthropic native path — same issue, different code path.
Root Causes
Tool key contamination:
apply_guardrail()returns the entire mutatedinputsdict including OpenAI-converted tool keys (tools,structured_messages,model,images) added byprocess_input_messages(). On the Anthropic native path this overwrites native tool definitions with the OpenAItype:"function"format.No unmask path in
apply_guardrail(): Only a masking path exists — when called withinput_type="response"andoutput_parse_pii=True, PII tokens are never restored.Abstract interface out of sync: All
process_output_response()implementations acceptrequest_databut the abstract base (BaseGuardrailTranslation) does not declare the param, causing a type/mypy mismatch.Streaming hook handles only
ModelResponseStream: Theasync_post_call_streaming_iterator_hookunmasking path iterates chunks expectingModelResponseStreamobjects. With the Anthropic native API path, response chunks are rawbytes(SSE format) — these pass through unprocessed, so unmasking never runs.Additionally,
AnthropicMessagesHandler.process_output_response()creates a freshrequest_datadict, discarding anypii_tokensstored during input masking.Config
Provider:
anthropic/native (not via OpenAI compat)Fix PRs
Each root cause is addressed in a separate PR (will link once submitted):
fix/guardrail-request-data-passthrough: Sync abstractBaseGuardrailTranslation.process_output_response()signature with all implementations (root cause 3)fix/presidio-native-tool-stripping: Strip OpenAI-converted keys before returning fromapply_guardrail()(root cause 1)fix/presidio-output-unmask: Add output unmask path toapply_guardrail()(root cause 2)fix/presidio-anthropic-sse-unmask: Handle Anthropic native SSE bytes in streaming unmask hook + fixAnthropicMessagesHandler(root cause 4)