(
&self,
request: LlmRequest,
)
| 274 | } |
| 275 | |
| 276 | async fn stream( |
| 277 | &self, |
| 278 | request: LlmRequest, |
| 279 | ) -> GraphBitResult<Box<dyn Stream<Item = GraphBitResult<LlmResponse>> + Unpin + Send>> { |
| 280 | let url = format!("{}/chat/completions", self.base_url); |
| 281 | |
| 282 | let messages: Vec<OpenAiMessage> = |
| 283 | request.messages.iter().map(Self::convert_message).collect(); |
| 284 | |
| 285 | let tools: Option<Vec<OpenAiTool>> = if request.tools.is_empty() { |
| 286 | None |
| 287 | } else { |
| 288 | Some(request.tools.iter().map(Self::convert_tool).collect()) |
| 289 | }; |
| 290 | |
| 291 | let body = OpenAiRequest { |
| 292 | model: self.model.clone(), |
| 293 | messages, |
| 294 | max_completion_tokens: request.max_tokens, |
| 295 | temperature: request.temperature, |
| 296 | top_p: request.top_p, |
| 297 | tools: tools.clone(), |
| 298 | tool_choice: if tools.is_some() { |
| 299 | Some("auto".to_string()) |
| 300 | } else { |
| 301 | None |
| 302 | }, |
| 303 | stream: Some(true), // Enable streaming |
| 304 | // Request terminal usage chunk so workflow metadata token usage matches non-streaming. |
| 305 | stream_options: Some(OpenAiStreamOptions { |
| 306 | include_usage: true, |
| 307 | }), |
| 308 | }; |
| 309 | |
| 310 | // Add extra parameters |
| 311 | let mut request_json = serde_json::to_value(&body)?; |
| 312 | if let serde_json::Value::Object(ref mut map) = request_json { |
| 313 | for (key, value) in request.extra_params { |
| 314 | map.insert(key, value); |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | let mut req_builder = self |
| 319 | .client |
| 320 | .post(&url) |
| 321 | .header("Authorization", format!("Bearer {}", self.api_key)) |
| 322 | .header("Content-Type", "application/json") |
| 323 | .json(&request_json); |
| 324 | |
| 325 | if let Some(org) = &self.organization { |
| 326 | req_builder = req_builder.header("OpenAI-Organization", org); |
| 327 | } |
| 328 | |
| 329 | // Timeout constants for different phases of the request |
| 330 | // CONNECTION_TIMEOUT: Covers DNS resolution, TCP connection, TLS handshake, and first byte |
| 331 | // Should be generous for slow networks but prevent infinite hangs |
| 332 | const CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); |
| 333 | // ERROR_BODY_TIMEOUT: Time to read error response body (usually small) |
nothing calls this directly
no test coverage detected