gaze_proxy/adapters/
gemini.rs1use std::borrow::Cow;
2
3use http::Method;
4use serde_json::Value;
5use url::Url;
6
7use crate::adapter::{walk_all_strings, PiiSurface, ProviderAdapter, SseEvent};
8
9fn canonical_field_name(key: &str) -> Cow<'_, str> {
19 if !key.contains('_') {
20 return Cow::Borrowed(key);
21 }
22 let mut canonical = String::with_capacity(key.len());
23 let mut capitalize_next = false;
24 for character in key.chars() {
25 if character == '_' {
26 capitalize_next = true;
27 } else if capitalize_next {
28 canonical.extend(character.to_uppercase());
29 capitalize_next = false;
30 } else {
31 canonical.push(character);
32 }
33 }
34 Cow::Owned(canonical)
35}
36
37#[derive(Clone, Debug)]
38#[non_exhaustive]
39pub struct GeminiAdapter {
40 upstream: Url,
41}
42
43impl GeminiAdapter {
44 pub fn new(upstream: Url) -> Self {
45 Self { upstream }
46 }
47}
48
49#[async_trait::async_trait]
50impl ProviderAdapter for GeminiAdapter {
51 fn name(&self) -> &'static str {
52 "gemini"
53 }
54
55 fn matches_path(&self, method: &Method, path: &str) -> bool {
56 method == Method::POST
57 && path.starts_with("/v1beta/models/")
58 && (path.ends_with(":generateContent")
59 || path.ends_with(":streamGenerateContent")
60 || path.ends_with(":countTokens"))
61 }
62
63 fn upstream_base(&self) -> &Url {
64 &self.upstream
65 }
66
67 fn request_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
68 collect_gemini_surfaces(body, true)
69 }
70
71 fn response_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
72 collect_gemini_surfaces(body, false)
73 }
74
75 fn sse_event_pii_surfaces<'a>(&self, event: &'a mut SseEvent) -> Vec<PiiSurface<'a>> {
76 self.response_pii_surfaces(&mut event.data)
77 }
78}
79
80fn collect_gemini_surfaces(body: &mut Value, request: bool) -> Vec<PiiSurface<'_>> {
91 let mut surfaces = Vec::new();
92 if let Value::Object(root) = body {
93 if request {
94 for (key, value) in root {
95 match canonical_field_name(key).as_ref() {
96 "contents" => {
97 if let Value::Array(contents) = value {
98 for (index, content) in contents.iter_mut().enumerate() {
99 collect_content(&mut surfaces, format!("{key}[{index}]"), content);
100 }
101 }
102 }
103 "systemInstruction" => {
104 collect_content(&mut surfaces, key.clone(), value);
105 }
106 "tools" | "toolConfig" => {
109 walk_all_strings(&mut surfaces, key.clone(), value);
110 }
111 "generationConfig" => {
115 walk_all_strings(&mut surfaces, key.clone(), value);
116 }
117 _ => {}
118 }
119 }
120 } else if let Some(Value::Array(candidates)) = root.get_mut("candidates") {
121 for (index, candidate) in candidates.iter_mut().enumerate() {
122 if let Value::Object(candidate) = candidate {
123 if let Some(content) = candidate.get_mut("content") {
124 collect_content(
125 &mut surfaces,
126 format!("candidates[{index}].content"),
127 content,
128 );
129 }
130 }
131 }
132 }
133 }
134 surfaces
135}
136
137fn collect_content<'a>(surfaces: &mut Vec<PiiSurface<'a>>, prefix: String, value: &'a mut Value) {
138 let Value::Object(content) = value else {
139 return;
140 };
141 let Some((parts_key, Value::Array(parts))) = content
142 .iter_mut()
143 .find(|(key, _)| canonical_field_name(key) == "parts")
144 else {
145 return;
146 };
147 let parts_prefix = format!("{prefix}.{parts_key}");
148 for (index, part) in parts.iter_mut().enumerate() {
149 let Value::Object(part) = part else {
150 continue;
151 };
152 for (key, value) in part {
153 let part_prefix = format!("{parts_prefix}[{index}].{key}");
154 match canonical_field_name(key).as_ref() {
155 "text" => {
156 if let Value::String(text) = value {
157 surfaces.push(PiiSurface {
158 field_path: part_prefix,
159 text,
160 });
161 }
162 }
163 "functionCall" => {
164 if let Some((args_key, args)) = value
165 .as_object_mut()
166 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "args"))
167 {
168 walk_all_strings(surfaces, format!("{part_prefix}.{args_key}"), args);
169 }
170 }
171 "functionResponse" => {
172 if let Some((response_key, response)) = value
173 .as_object_mut()
174 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "response"))
175 {
176 walk_all_strings(
177 surfaces,
178 format!("{part_prefix}.{response_key}"),
179 response,
180 );
181 }
182 }
183 _ => {}
184 }
185 }
186 }
187}