Parse `Azure LLM` response to `GraphBit` response
(&self, response: AzureLlmResponse)
| 336 | |
| 337 | /// Parse `Azure LLM` response to `GraphBit` response |
| 338 | fn parse_response(&self, response: AzureLlmResponse) -> GraphBitResult<LlmResponse> { |
| 339 | let choice = response |
| 340 | .choices |
| 341 | .into_iter() |
| 342 | .next() |
| 343 | .ok_or_else(|| GraphBitError::llm_provider("azurellm", "No choices in response"))?; |
| 344 | |
| 345 | let finish_reason = match choice.finish_reason.as_str() { |
| 346 | "stop" => FinishReason::Stop, |
| 347 | "length" => FinishReason::Length, |
| 348 | "tool_calls" => FinishReason::ToolCalls, |
| 349 | "content_filter" => FinishReason::ContentFilter, |
| 350 | _ => FinishReason::Other(choice.finish_reason), |
| 351 | }; |
| 352 | |
| 353 | let tool_calls = if let Some(tool_calls) = choice.message.tool_calls { |
| 354 | tool_calls |
| 355 | .into_iter() |
| 356 | .map(|tc| LlmToolCall { |
| 357 | id: tc.id, |
| 358 | name: tc.function.name, |
| 359 | parameters: serde_json::from_str(&tc.function.arguments).unwrap_or_default(), |
| 360 | }) |
| 361 | .collect() |
| 362 | } else { |
| 363 | Vec::new() |
| 364 | }; |
| 365 | |
| 366 | // Handle Azure's null/empty content bug |
| 367 | // When finish_reason is "length" or "content_filter", Azure returns EMPTY STRING (not null!) |
| 368 | // despite consuming completion tokens. This is a known Azure API quirk. |
| 369 | let has_content = choice |
| 370 | .message |
| 371 | .content |
| 372 | .as_ref() |
| 373 | .map(|s| !s.is_empty()) |
| 374 | .unwrap_or(false); |
| 375 | let content_value = choice.message.content.unwrap_or_default(); |
| 376 | |
| 377 | let content = if has_content { |
| 378 | // Normal case: content exists and is not empty |
| 379 | tracing::debug!("Azure response has content: {} chars", content_value.len()); |
| 380 | content_value |
| 381 | } else if response.usage.completion_tokens > 0 { |
| 382 | // Bug case: empty/null content but tokens were used |
| 383 | match &finish_reason { |
| 384 | FinishReason::Length => { |
| 385 | tracing::warn!( |
| 386 | "Azure API returned empty content despite using {} completion tokens (finish_reason: Length). \ |
| 387 | This typically occurs with very low max_tokens limits. Consider increasing max_tokens for better results.", |
| 388 | response.usage.completion_tokens |
| 389 | ); |
| 390 | let msg = format!( |
| 391 | "[Azure API used {} tokens but returned no content. This occurs when max_tokens is set too low. \ |
| 392 | The model may have started generating a response but was cut off before producing visible text. \ |
| 393 | Increase max_tokens for meaningful output.]", |
| 394 | response.usage.completion_tokens |
| 395 | ); |
no test coverage detected