gaze-proxy 0.11.3

Feature-gated multi-provider HTTP proxy for Gaze PII pseudonymization
Documentation
use http::Method;
use serde_json::Value;
use url::Url;

use crate::adapter::{
    push_string, push_text_blocks, walk_all_strings, PiiSurface, ProviderAdapter, SseEvent,
};

#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AnthropicAdapter {
    upstream: Url,
}

impl AnthropicAdapter {
    pub fn new(upstream: Url) -> Self {
        Self { upstream }
    }
}

#[async_trait::async_trait]
impl ProviderAdapter for AnthropicAdapter {
    fn name(&self) -> &'static str {
        "anthropic"
    }

    fn matches_path(&self, method: &Method, path: &str) -> bool {
        method == Method::POST && path == "/v1/messages"
    }

    fn upstream_base(&self) -> &Url {
        &self.upstream
    }

    fn request_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
        let mut surfaces = Vec::new();
        if let Value::Object(root) = body {
            for (key, value) in root {
                match key.as_str() {
                    "system" => push_text_blocks(&mut surfaces, "system", value),
                    "messages" => {
                        if let Value::Array(messages) = value {
                            for (index, message) in messages.iter_mut().enumerate() {
                                if let Value::Object(message) = message {
                                    for (message_key, message_value) in message {
                                        if message_key == "content" {
                                            collect_content_blocks(
                                                &mut surfaces,
                                                &format!("messages[{index}].content"),
                                                message_value,
                                            );
                                        }
                                    }
                                }
                            }
                        }
                    }
                    _ => {}
                }
            }
        }
        surfaces
    }

    fn response_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
        let mut surfaces = Vec::new();
        if let Value::Object(root) = body {
            if let Some(content) = root.get_mut("content") {
                collect_content_blocks(&mut surfaces, "content", content);
            }
        }
        surfaces
    }

    fn sse_event_pii_surfaces<'a>(&self, event: &'a mut SseEvent) -> Vec<PiiSurface<'a>> {
        let mut surfaces = Vec::new();
        if let Value::Object(root) = &mut event.data {
            if matches!(root.get("type"), Some(Value::String(kind)) if kind == "content_block_delta")
            {
                if let Some(Value::Object(delta)) = root.get_mut("delta") {
                    for (key, value) in delta {
                        if let Value::String(text) = value {
                            match key.as_str() {
                                "text" => surfaces.push(PiiSurface {
                                    field_path: "delta.text".to_string(),
                                    text,
                                }),
                                "partial_json" => surfaces.push(PiiSurface {
                                    field_path: "delta.partial_json".to_string(),
                                    text,
                                }),
                                _ => {}
                            }
                        }
                    }
                }
            }
        }
        surfaces
    }
}

fn collect_content_blocks<'a>(
    surfaces: &mut Vec<PiiSurface<'a>>,
    prefix: &str,
    content: &'a mut Value,
) {
    match content {
        Value::String(_) => push_string(surfaces, prefix, content),
        Value::Array(blocks) => {
            for (index, block) in blocks.iter_mut().enumerate() {
                let Value::Object(block) = block else {
                    continue;
                };
                match block.get("type").and_then(Value::as_str) {
                    Some("text") => {
                        if let Some(Value::String(text)) = block.get_mut("text") {
                            surfaces.push(PiiSurface {
                                field_path: format!("{prefix}[{index}].text"),
                                text,
                            });
                        }
                    }
                    Some("tool_use") => {
                        if let Some(input) = block.get_mut("input") {
                            walk_all_strings(surfaces, format!("{prefix}[{index}].input"), input);
                        }
                    }
                    Some("tool_result") => {
                        if let Some(result) = block.get_mut("content") {
                            collect_content_blocks(
                                surfaces,
                                &format!("{prefix}[{index}].content"),
                                result,
                            );
                        }
                    }
                    Some("image") | None | Some(_) => {}
                }
            }
        }
        _ => {}
    }
}