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, SurfaceSyntax};
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 fn requests_json_output(&self, request: &Value) -> bool {
82 let Value::Object(root) = request else {
83 return false;
84 };
85 root.iter()
86 .filter(|(key, _)| canonical_field_name(key) == "generationConfig")
87 .filter_map(|(_, config)| config.as_object())
88 .flatten()
89 .any(|(key, value)| {
90 canonical_field_name(key) == "responseMimeType"
91 && value
92 .as_str()
93 .is_some_and(|mime| mime.eq_ignore_ascii_case("application/json"))
94 })
95 }
96}
97
98fn collect_gemini_surfaces(body: &mut Value, request: bool) -> Vec<PiiSurface<'_>> {
109 let mut surfaces = Vec::new();
110 if let Value::Object(root) = body {
111 if request {
112 for (key, value) in root {
113 match canonical_field_name(key).as_ref() {
114 "contents" => {
115 if let Value::Array(contents) = value {
116 for (index, content) in contents.iter_mut().enumerate() {
117 collect_content(&mut surfaces, format!("{key}[{index}]"), content);
118 }
119 }
120 }
121 "systemInstruction" => {
122 collect_content(&mut surfaces, key.clone(), value);
123 }
124 "tools" | "toolConfig" => {
127 walk_all_strings(&mut surfaces, key.clone(), value);
128 }
129 "generationConfig" => {
133 walk_all_strings(&mut surfaces, key.clone(), value);
134 }
135 _ => {}
136 }
137 }
138 } else if let Some(Value::Array(candidates)) = root.get_mut("candidates") {
139 for (index, candidate) in candidates.iter_mut().enumerate() {
140 if let Value::Object(candidate) = candidate {
141 if let Some(content) = candidate.get_mut("content") {
142 collect_content(
143 &mut surfaces,
144 format!("candidates[{index}].content"),
145 content,
146 );
147 }
148 }
149 }
150 }
151 }
152 surfaces
153}
154
155fn collect_content<'a>(surfaces: &mut Vec<PiiSurface<'a>>, prefix: String, value: &'a mut Value) {
156 let Value::Object(content) = value else {
157 return;
158 };
159 let Some((parts_key, Value::Array(parts))) = content
160 .iter_mut()
161 .find(|(key, _)| canonical_field_name(key) == "parts")
162 else {
163 return;
164 };
165 let parts_prefix = format!("{prefix}.{parts_key}");
166 for (index, part) in parts.iter_mut().enumerate() {
167 let Value::Object(part) = part else {
168 continue;
169 };
170 let text_syntax = if part.get("thought").and_then(Value::as_bool) == Some(true) {
172 SurfaceSyntax::Text
173 } else {
174 SurfaceSyntax::ModelOutput
175 };
176 for (key, value) in part {
177 let part_prefix = format!("{parts_prefix}[{index}].{key}");
178 match canonical_field_name(key).as_ref() {
179 "text" => {
180 if let Value::String(text) = value {
181 surfaces.push(PiiSurface {
182 field_path: part_prefix,
183 text,
184 syntax: text_syntax,
185 });
186 }
187 }
188 "functionCall" => {
189 if let Some((args_key, args)) = value
190 .as_object_mut()
191 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "args"))
192 {
193 walk_all_strings(surfaces, format!("{part_prefix}.{args_key}"), args);
194 }
195 }
196 "functionResponse" => {
197 if let Some((response_key, response)) = value
198 .as_object_mut()
199 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "response"))
200 {
201 walk_all_strings(
202 surfaces,
203 format!("{part_prefix}.{response_key}"),
204 response,
205 );
206 }
207 }
208 _ => {}
209 }
210 }
211 }
212}