Execute a fully-built (non-streaming) chat-completions request.
(&self, request: serde_json::Value)
| 355 | |
| 356 | /// Execute a fully-built (non-streaming) chat-completions request. |
| 357 | async fn send_request(&self, request: serde_json::Value) -> Result<LlmResponse> { |
| 358 | { |
| 359 | let request_started_at = Instant::now(); |
| 360 | let url = format!("{}{}", self.base_url, self.chat_completions_path); |
| 361 | let request_headers = self.request_headers(); |
| 362 | |
| 363 | let response = crate::retry::with_retry(&self.retry_config, |_attempt| { |
| 364 | let http = &self.http; |
| 365 | let url = &url; |
| 366 | let request_headers = request_headers.clone(); |
| 367 | let request = &request; |
| 368 | async move { |
| 369 | let headers = request_headers |
| 370 | .iter() |
| 371 | .map(|(key, value)| (key.as_str(), value.as_str())) |
| 372 | .collect::<Vec<_>>(); |
| 373 | // Non-streaming: use a non-cancelled token for now |
| 374 | let cancel_token = tokio_util::sync::CancellationToken::new(); |
| 375 | match http.post(url, headers, request, cancel_token).await { |
| 376 | Ok(resp) => { |
| 377 | let status = reqwest::StatusCode::from_u16(resp.status) |
| 378 | .unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR); |
| 379 | if status.is_success() { |
| 380 | AttemptOutcome::Success(resp.body) |
| 381 | } else if self.retry_config.is_retryable_status(status) { |
| 382 | AttemptOutcome::Retryable { |
| 383 | status, |
| 384 | body: resp.body, |
| 385 | retry_after: None, |
| 386 | } |
| 387 | } else { |
| 388 | AttemptOutcome::Fatal(anyhow::anyhow!( |
| 389 | "OpenAI API error at {} ({}): {}", |
| 390 | url, |
| 391 | status, |
| 392 | resp.body |
| 393 | )) |
| 394 | } |
| 395 | } |
| 396 | Err(e) => { |
| 397 | tracing::error!("HTTP error: {e:?}"); |
| 398 | AttemptOutcome::Fatal(e) |
| 399 | } |
| 400 | } |
| 401 | } |
| 402 | }) |
| 403 | .await?; |
| 404 | |
| 405 | let parsed: OpenAiResponse = |
| 406 | serde_json::from_str(&response).context("Failed to parse OpenAI response")?; |
| 407 | |
| 408 | let choice = parsed.choices.into_iter().next().context("No choices")?; |
| 409 | |
| 410 | let mut content = vec![]; |
| 411 | |
| 412 | let reasoning_content = choice.message.reasoning_content; |
| 413 | |
| 414 | let text_content = choice.message.content; |
no test coverage detected