gaze-proxy 0.15.1

Feature-gated multi-provider HTTP proxy for Gaze PII pseudonymization
Documentation

gaze-proxy

Crates.io docs.rs License

gaze-proxy is the feature-gated HTTP proxy runtime for LLM SDK base-URL swaps. It preserves each provider's native wire shape and uses adapters only to locate PII-bearing fields before calling the supplied gaze::Pipeline.

Cargo

[dependencies]
gaze-proxy = "0.15.1"

Quickstart

cargo install gaze-cli --version 0.15.1
gaze proxy start

Then point SDKs at the daemon:

export OPENAI_BASE_URL=http://127.0.0.1:8787/v1
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
export GEMINI_BASE_URL=http://127.0.0.1:8787

The default config is written to ~/.config/gaze/proxy.toml and includes:

bind = "127.0.0.1:8787"
session_ttl = "30m"
rulepack = "core"

[adapters.openai]
upstream = "https://api.openai.com/"

[adapters.anthropic]
upstream = "https://api.anthropic.com/"

[adapters.gemini]
upstream = "https://generativelanguage.googleapis.com/"

Locale

Recognizers that declare locales = [...] — postal.de, postal.us, the national phone recognizers — only run when the active locale chain intersects that list. The proxy resolves that chain once and uses it for both the primary surface pass and the outbound residual re-scan, so the two can never disagree about what counts as PII.

gaze proxy has no --locale flag; the chain comes from the policy you pass:

locale = ["de-DE"]
gaze proxy serve --policy ./gaze.toml

With no policy the chain is ["global"] and locale-gated recognizers stay inert. Library adopters set it explicitly, passing the same chain the pipeline was assembled under:

let core = gaze_assembly::CorePipelineConfig::new()
    .with_bundled_rulepack("core-extended")
    .with_locale(&[gaze::LocaleTag::DeDe])
    .build()?;
let config = ProxyConfig::new(bind, adapters).with_locale_chain(core.locale_chain().clone());

A locale chain always ends in global, so configuring one can only widen detection.

Providers

  • OpenAI: POST /v1/chat/completions, /v1/completions, /v1/responses
  • Anthropic: POST /v1/messages
  • Gemini: POST /v1beta/models/*:{generateContent,streamGenerateContent,countTokens}

Each adapter walks text, tool-call, tool-result, and function argument surfaces in that provider's native JSON. The proxy does not transcode requests.

Daemon Commands

gaze proxy serve
gaze proxy start
gaze proxy status
gaze proxy stop
gaze proxy restart

Pidfiles live under the platform local-data directory, never /tmp. Stale pidfiles are revalidated with process liveness checks and cleaned before start. Lifecycle operations coordinate through a persistent <pidfile>.lock sidecar. Do not remove this sidecar or edit the pidfile while daemon commands are active; cleanup removes only the owned pidfile and leaves the sidecar in place.

gaze proxy logs --follow is also available for local daemon inspection.

Security Notes

The strict Anthropic Messages profile rebuilds its outbound headers from a closed allowlist: content-type, x-api-key, anthropic-version, and an optional, explicitly allowlisted anthropic-beta. Unconfigured Authorization, bearer credentials, cookies, and unknown SDK headers are accepted at ingress and dropped. If an embedding host configures Authorization as local listener authentication, it is a singleton consumed only by the trusted principal resolver and is still never forwarded.

AnthropicAdapter::new is an ephemeral, single-request profile and rejects x-gaze-session-id. Session continuity is an explicit builder/configuration choice and then requires that header with a canonical lowercase UUIDv4 value. The SDK base URL is the proxy root, while the only direct route is exactly POST /v1/messages.

The listener does not become an access-control boundary merely because provider credentials pass through it. Bind to loopback unless an explicit trusted principal resolver protects a non-loopback listener. See the strict Anthropic Messages contract for the complete wire, proof, inspection, migration, and manual SDK-test contract.

Configured safety nets at request admission

Surfaced request text in the direct Anthropic and legacy adapter paths first runs the safety-net Resolve step of gaze clean --safety-net-fallback strict: every span a configured net flags becomes a restorable token. Plain gaze clean defaults to the redact fallback, which also tokenizes what the nets' re-run flags and deletes what is left one way; the proxy refuses that request instead. The result then passes configured safety-net admission before provider I/O. Nets inspect the complete transformed surface with a manifest built from the actual session token ownership and restore boundaries. Token-contained reflags, including class disagreements, remain allowed; a flagged span Resolve could not tokenize, a malformed suspect, or a net execution error rejects the request with 422 and a typed refusal (variant, fallback reason, suspect classes; never text). No destructive clean fallback is used. See Safety nets and refusals.

Compatibility: a net-flagged span, including text preserved by primary policy, is now tokenized and forwarded instead of refused. Requests Resolve cannot protect are still denied before reaching the provider. Admission adds inference on surfaced text and codec validation views, bypasses observer skip optimizations, and runs all registry backends selected across the locale chain. Registry resolution and inference errors fail closed. This can increase latency and inference cost. Primary Preserve/Redact actions and public legacy clean defaults are unchanged; admission does not certify their reversibility.

No installed net, a custom net skipped for locale coverage, or a detector miss remains a coverage limitation. A model is not required globally. Existing strict protection still requires its full primary and locale contract. Direct failures abandon staged mappings before commit/send. Legacy mappings are already live and remain so on failure; there is no whole-request rollback guarantee. Response restoration and response residual validation retain their existing contracts.