use crate::codecs::GeminiCodec;
use crate::codecs::codec::{BoxByteStream, BoxDeltaStream, Codec, EncodedRequest};
use crate::error::Result;
use crate::ir::{Capabilities, ModelRequest, ModelResponse, ModelWarning, OutputStrategy};
use crate::rate_limit::RateLimitSnapshot;
#[derive(Clone, Copy, Debug, Default)]
pub struct VertexGeminiCodec {
inner: GeminiCodec,
}
impl VertexGeminiCodec {
#[must_use]
pub const fn new() -> Self {
Self {
inner: GeminiCodec::new(),
}
}
}
impl Codec for VertexGeminiCodec {
fn name(&self) -> &'static str {
"vertex-gemini"
}
fn capabilities(&self, model: &str) -> Capabilities {
self.inner.capabilities(model)
}
fn auto_output_strategy(&self, model: &str) -> OutputStrategy {
self.inner.auto_output_strategy(model)
}
fn encode(&self, request: &ModelRequest) -> Result<EncodedRequest> {
let mut encoded = self.inner.encode(request)?;
rewrite_path_for_vertex(&mut encoded, &request.model, false);
Ok(encoded)
}
fn encode_streaming(&self, request: &ModelRequest) -> Result<EncodedRequest> {
let mut encoded = self.inner.encode_streaming(request)?;
rewrite_path_for_vertex(&mut encoded, &request.model, true);
Ok(encoded)
}
fn decode_stream<'a>(
&'a self,
bytes: BoxByteStream<'a>,
warnings_in: Vec<ModelWarning>,
) -> BoxDeltaStream<'a> {
self.inner.decode_stream(bytes, warnings_in)
}
fn decode(&self, body: &[u8], warnings_in: Vec<ModelWarning>) -> Result<ModelResponse> {
self.inner.decode(body, warnings_in)
}
fn extract_rate_limit(&self, headers: &http::HeaderMap) -> Option<RateLimitSnapshot> {
self.inner.extract_rate_limit(headers)
}
}
fn rewrite_path_for_vertex(encoded: &mut EncodedRequest, model: &str, streaming: bool) {
let action = if streaming {
"streamGenerateContent?alt=sse"
} else {
"generateContent"
};
encoded.path = format!("/publishers/google/models/{model}:{action}");
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::ir::{Message, ModelRequest};
fn req() -> ModelRequest {
ModelRequest {
model: "gemini-3.1-pro".into(),
messages: vec![Message::user("hi")],
max_tokens: Some(16),
..ModelRequest::default()
}
}
#[test]
fn encode_emits_publisher_partial_path() {
let codec = VertexGeminiCodec::new();
let encoded = codec.encode(&req()).unwrap();
assert_eq!(
encoded.path, "/publishers/google/models/gemini-3.1-pro:generateContent",
"Vertex Gemini codec must emit the publisher-partial path so VertexTransport can prefix project + location"
);
}
#[test]
fn encode_streaming_emits_publisher_partial_path_with_sse_alt() {
let codec = VertexGeminiCodec::new();
let encoded = codec.encode_streaming(&req()).unwrap();
assert!(encoded.streaming);
assert_eq!(
encoded.path,
"/publishers/google/models/gemini-3.1-pro:streamGenerateContent?alt=sse",
);
}
#[test]
fn encode_body_delegates_to_inner_unchanged() {
let codec = VertexGeminiCodec::new();
let direct = GeminiCodec::new();
let body_v = codec.encode(&req()).unwrap().body;
let body_d = direct.encode(&req()).unwrap().body;
assert_eq!(
body_v, body_d,
"Vertex Gemini body shape is identical to direct Gemini — only the URL path differs"
);
}
}