(&self, request: ChatRequest)
| 252 | } |
| 253 | |
| 254 | async fn chat_stream(&self, request: ChatRequest) -> Result<StreamResult, ProviderError> { |
| 255 | let url = self.build_url(&request.model, "streamGenerateContent"); |
| 256 | let vertex_request = self.convert_request(request); |
| 257 | |
| 258 | let response = self |
| 259 | .client |
| 260 | .post(&url) |
| 261 | .header("Content-Type", "application/json") |
| 262 | .header( |
| 263 | "Authorization", |
| 264 | format!("Bearer {}", self.config.access_token), |
| 265 | ) |
| 266 | .header("Accept", "text/event-stream") |
| 267 | .json(&vertex_request) |
| 268 | .send() |
| 269 | .await |
| 270 | .map_err(|e| ProviderError::NetworkError(e.to_string()))?; |
| 271 | |
| 272 | if !response.status().is_success() { |
| 273 | let status = response.status(); |
| 274 | let body = response.text().await.unwrap_or_default(); |
| 275 | return Err(ProviderError::api_error_with_status( |
| 276 | format!("{}: {}", status, body), |
| 277 | status.as_u16(), |
| 278 | )); |
| 279 | } |
| 280 | |
| 281 | let stream = response.bytes_stream().map(move |chunk_result| { |
| 282 | match chunk_result { |
| 283 | Ok(bytes) => { |
| 284 | let text = String::from_utf8_lossy(&bytes); |
| 285 | for line in text.lines() { |
| 286 | if line.starts_with("data: ") { |
| 287 | let data = &line[6..]; |
| 288 | if let Some(event) = parse_vertex_sse(data) { |
| 289 | return Ok(event); |
| 290 | } |
| 291 | } else if !line.is_empty() && !line.starts_with(':') { |
| 292 | // Vertex sometimes returns raw JSON without "data: " prefix |
| 293 | if let Some(event) = parse_vertex_sse(line) { |
| 294 | return Ok(event); |
| 295 | } |
| 296 | } |
| 297 | } |
| 298 | Ok(StreamEvent::TextDelta(String::new())) |
| 299 | } |
| 300 | Err(e) => Err(ProviderError::StreamError(e.to_string())), |
| 301 | } |
| 302 | }); |
| 303 | |
| 304 | Ok(Box::pin(stream)) |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | #[derive(Debug, Serialize)] |
nothing calls this directly
no test coverage detected