systemprompt_api/services/gateway/protocol/outbound/gemini/
mod.rs1use anyhow::{Result, anyhow};
10use async_trait::async_trait;
11use serde_json::Value;
12use systemprompt_models::profile::WireProtocol;
13use systemprompt_models::wire::gemini;
14
15use super::super::canonical_response::CanonicalResponse;
16use super::{
17 OutboundAdapter, OutboundCtx, OutboundOutcome, UpstreamError, extract_upstream_message,
18};
19
20#[cfg(feature = "test-api")]
21pub mod test_api {
22 pub use systemprompt_models::wire::gemini::{
23 build_request_body, parse_response, sse_to_canonical_events,
24 };
25}
26
27#[derive(Debug, Clone, Copy, Default)]
28pub struct GeminiOutbound;
29
30#[async_trait]
31impl OutboundAdapter for GeminiOutbound {
32 fn provider_tag(&self) -> &'static str {
33 WireProtocol::Gemini.as_tag()
34 }
35
36 async fn send(&self, ctx: OutboundCtx<'_>) -> Result<OutboundOutcome> {
37 let body = gemini::build_request_body(
38 ctx.request,
39 ctx.model_limits.and_then(|l| l.max_thinking_budget),
40 );
41 let path = gemini::upstream_path(ctx.upstream_model, ctx.request.stream);
42 let url = format!("{}{path}", ctx.endpoint.trim_end_matches('/'));
43
44 let client = reqwest::Client::new();
45 let mut req = client
46 .post(&url)
47 .header(gemini::API_KEY_HEADER, ctx.api_key)
48 .header("content-type", "application/json")
49 .json(&body);
50 for (name, value) in &ctx.route.extra_headers {
51 req = req.header(name.as_str(), value.as_str());
52 }
53
54 let upstream_response = req.send().await.map_err(|e| {
55 anyhow::Error::new(UpstreamError::Transport {
56 provider: self.provider_tag(),
57 source: e,
58 })
59 })?;
60
61 let status = upstream_response.status();
62 if !status.is_success() {
63 let err = upstream_response
64 .text()
65 .await
66 .unwrap_or_else(|e| format!("<failed to read upstream body: {e}>"));
67 return Err(anyhow::Error::new(UpstreamError::Status {
68 provider: self.provider_tag(),
69 status: status.as_u16(),
70 message: extract_upstream_message(&err),
71 }));
72 }
73
74 if ctx.request.stream {
75 let stream = upstream_response.bytes_stream();
76 let event_stream = gemini::sse_to_canonical_events(stream, ctx.request.model.clone());
77 return Ok(OutboundOutcome::Streaming(event_stream));
78 }
79
80 let bytes = upstream_response
81 .bytes()
82 .await
83 .map_err(|e| anyhow!("Failed to read Gemini response: {e}"))?;
84 let value: Value = serde_json::from_slice(&bytes)
85 .map_err(|e| anyhow!("Gemini response not valid JSON: {e}"))?;
86 let canon: CanonicalResponse = gemini::parse_response(&value, ctx.request.model.as_str());
87 Ok(OutboundOutcome::Buffered(Box::new(canon)))
88 }
89}