gaze_proxy/adapters/
gemini.rs1use std::borrow::Cow;
2
3use http::Method;
4use serde_json::Value;
5use url::Url;
6
7use crate::adapter::{
8 walk_all_strings, AdapterContract, PiiSurface, ProviderAdapter, SseEvent, SurfaceSyntax,
9};
10
11fn canonical_field_name(key: &str) -> Cow<'_, str> {
21 if !key.contains('_') {
22 return Cow::Borrowed(key);
23 }
24 let mut canonical = String::with_capacity(key.len());
25 let mut capitalize_next = false;
26 for character in key.chars() {
27 if character == '_' {
28 capitalize_next = true;
29 } else if capitalize_next {
30 canonical.extend(character.to_uppercase());
31 capitalize_next = false;
32 } else {
33 canonical.push(character);
34 }
35 }
36 Cow::Owned(canonical)
37}
38
39#[derive(Clone, Debug)]
40#[non_exhaustive]
41pub struct GeminiAdapter {
42 upstream: Url,
43}
44
45impl GeminiAdapter {
46 pub fn new(upstream: Url) -> Self {
47 Self { upstream }
48 }
49}
50
51#[async_trait::async_trait]
52impl ProviderAdapter for GeminiAdapter {
53 fn contract(&self) -> AdapterContract<'_> {
54 AdapterContract::legacy()
55 }
56
57 fn name(&self) -> &'static str {
58 "gemini"
59 }
60
61 fn matches_path(&self, method: &Method, path: &str) -> bool {
62 method == Method::POST
63 && path.starts_with("/v1beta/models/")
64 && (path.ends_with(":generateContent")
65 || path.ends_with(":streamGenerateContent")
66 || path.ends_with(":countTokens"))
67 }
68
69 fn upstream_base(&self) -> &Url {
70 &self.upstream
71 }
72
73 fn request_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
74 collect_gemini_surfaces(body, true)
75 }
76
77 fn response_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
78 collect_gemini_surfaces(body, false)
79 }
80
81 fn sse_event_pii_surfaces<'a>(&self, event: &'a mut SseEvent) -> Vec<PiiSurface<'a>> {
82 self.response_pii_surfaces(&mut event.data)
83 }
84
85 fn requests_json_output(&self, request: &Value) -> bool {
88 let Value::Object(root) = request else {
89 return false;
90 };
91 root.iter()
92 .filter(|(key, _)| canonical_field_name(key) == "generationConfig")
93 .filter_map(|(_, config)| config.as_object())
94 .flatten()
95 .any(|(key, value)| {
96 canonical_field_name(key) == "responseMimeType"
97 && value
98 .as_str()
99 .is_some_and(|mime| mime.eq_ignore_ascii_case("application/json"))
100 })
101 }
102}
103
104fn collect_gemini_surfaces(body: &mut Value, request: bool) -> Vec<PiiSurface<'_>> {
115 let mut surfaces = Vec::new();
116 if let Value::Object(root) = body {
117 if request {
118 for (key, value) in root {
119 match canonical_field_name(key).as_ref() {
120 "contents" => {
121 if let Value::Array(contents) = value {
122 for (index, content) in contents.iter_mut().enumerate() {
123 collect_content(&mut surfaces, format!("{key}[{index}]"), content);
124 }
125 }
126 }
127 "systemInstruction" => {
128 collect_content(&mut surfaces, key.clone(), value);
129 }
130 "tools" | "toolConfig" => {
133 walk_all_strings(&mut surfaces, key.clone(), value);
134 }
135 "generationConfig" => {
139 walk_all_strings(&mut surfaces, key.clone(), value);
140 }
141 _ => {}
142 }
143 }
144 } else if let Some(Value::Array(candidates)) = root.get_mut("candidates") {
145 for (index, candidate) in candidates.iter_mut().enumerate() {
146 if let Value::Object(candidate) = candidate {
147 if let Some(content) = candidate.get_mut("content") {
148 collect_content(
149 &mut surfaces,
150 format!("candidates[{index}].content"),
151 content,
152 );
153 }
154 }
155 }
156 }
157 }
158 surfaces
159}
160
161fn collect_content<'a>(surfaces: &mut Vec<PiiSurface<'a>>, prefix: String, value: &'a mut Value) {
162 let Value::Object(content) = value else {
163 return;
164 };
165 let Some((parts_key, Value::Array(parts))) = content
166 .iter_mut()
167 .find(|(key, _)| canonical_field_name(key) == "parts")
168 else {
169 return;
170 };
171 let parts_prefix = format!("{prefix}.{parts_key}");
172 for (index, part) in parts.iter_mut().enumerate() {
173 let Value::Object(part) = part else {
174 continue;
175 };
176 let text_syntax = if part.get("thought").and_then(Value::as_bool) == Some(true) {
178 SurfaceSyntax::Text
179 } else {
180 SurfaceSyntax::ModelOutput
181 };
182 for (key, value) in part {
183 let part_prefix = format!("{parts_prefix}[{index}].{key}");
184 match canonical_field_name(key).as_ref() {
185 "text" => {
186 if let Value::String(text) = value {
187 surfaces.push(PiiSurface {
188 field_path: part_prefix,
189 text,
190 syntax: text_syntax,
191 });
192 }
193 }
194 "functionCall" => {
195 if let Some((args_key, args)) = value
196 .as_object_mut()
197 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "args"))
198 {
199 walk_all_strings(surfaces, format!("{part_prefix}.{args_key}"), args);
200 }
201 }
202 "functionResponse" => {
203 if let Some((response_key, response)) = value
204 .as_object_mut()
205 .and_then(|call| call.iter_mut().find(|(key, _)| *key == "response"))
206 {
207 walk_all_strings(
208 surfaces,
209 format!("{part_prefix}.{response_key}"),
210 response,
211 );
212 }
213 }
214 _ => {}
215 }
216 }
217 }
218}