Skip to main content

vtcode_core/llm/providers/openai/
response_parser.rs

1//! Chat Completions response parsing for OpenAI-compatible APIs.
2
3use crate::llm::error_display;
4use crate::llm::provider;
5use crate::llm::providers::extract_reasoning_trace;
6use crate::llm::providers::shared::parse_openai_tool_calls;
7use serde_json::Value;
8
9pub(crate) fn parse_chat_response(
10    response_json: Value,
11    model: String,
12    include_cached_prompt_tokens: bool,
13) -> Result<provider::LLMResponse, provider::LLMError> {
14    let choices = response_json
15        .get("choices")
16        .and_then(|c| c.as_array())
17        .ok_or_else(|| {
18            let formatted_error = error_display::format_llm_error(
19                "OpenAI",
20                "Invalid response format: missing choices",
21            );
22            provider::LLMError::Provider {
23                message: formatted_error,
24                metadata: None,
25            }
26        })?;
27
28    if choices.is_empty() {
29        let formatted_error = error_display::format_llm_error("OpenAI", "No choices in response");
30        return Err(provider::LLMError::Provider {
31            message: formatted_error,
32            metadata: None,
33        });
34    }
35
36    let choice = &choices[0];
37    let message = choice.get("message").ok_or_else(|| {
38        let formatted_error =
39            error_display::format_llm_error("OpenAI", "Invalid response format: missing message");
40        provider::LLMError::Provider {
41            message: formatted_error,
42            metadata: None,
43        }
44    })?;
45
46    let content = match message.get("content") {
47        Some(Value::String(text)) => Some(text.to_string()),
48        Some(Value::Array(parts)) => {
49            let text = parts
50                .iter()
51                .filter_map(|part| part.get("text").and_then(|t| t.as_str()))
52                .collect::<Vec<_>>()
53                .join("");
54            (!text.is_empty()).then_some(text)
55        }
56        _ => None,
57    };
58
59    let tool_calls = message
60        .get("tool_calls")
61        .and_then(|tc| tc.as_array())
62        .map(|calls| parse_openai_tool_calls(calls))
63        .filter(|calls| !calls.is_empty());
64
65    let reasoning = message
66        .get("reasoning_content")
67        .and_then(extract_reasoning_trace)
68        .or_else(|| message.get("reasoning").and_then(extract_reasoning_trace))
69        .or_else(|| {
70            choice
71                .get("reasoning_content")
72                .and_then(extract_reasoning_trace)
73        })
74        .or_else(|| choice.get("reasoning").and_then(extract_reasoning_trace))
75        .or_else(|| {
76            content.as_ref().and_then(|c| {
77                let (reasoning_parts, _) = crate::llm::utils::extract_reasoning_content(c);
78                if reasoning_parts.is_empty() {
79                    None
80                } else {
81                    Some(reasoning_parts.join("\n\n"))
82                }
83            })
84        });
85
86    let finish_reason = choice
87        .get("finish_reason")
88        .and_then(|fr| fr.as_str())
89        .map(|fr| match fr {
90            "stop" => provider::FinishReason::Stop,
91            "length" => provider::FinishReason::Length,
92            "tool_calls" => provider::FinishReason::ToolCalls,
93            "content_filter" => provider::FinishReason::ContentFilter,
94            other => provider::FinishReason::Error(other.to_string()),
95        })
96        .unwrap_or(provider::FinishReason::Stop);
97
98    Ok(provider::LLMResponse {
99        content,
100        tool_calls,
101        model,
102        usage: response_json.get("usage").map(|usage_value| {
103            let cached_prompt_tokens = if include_cached_prompt_tokens {
104                usage_value
105                    .get("prompt_tokens_details")
106                    .and_then(|details| details.get("cached_tokens"))
107                    .and_then(|value| value.as_u64())
108                    .map(|value| value as u32)
109            } else {
110                None
111            };
112
113            provider::Usage {
114                prompt_tokens: usage_value
115                    .get("prompt_tokens")
116                    .and_then(|pt| pt.as_u64())
117                    .and_then(|v| u32::try_from(v).ok())
118                    .unwrap_or(0),
119                completion_tokens: usage_value
120                    .get("completion_tokens")
121                    .and_then(|ct| ct.as_u64())
122                    .and_then(|v| u32::try_from(v).ok())
123                    .unwrap_or(0),
124                total_tokens: usage_value
125                    .get("total_tokens")
126                    .and_then(|tt| tt.as_u64())
127                    .and_then(|v| u32::try_from(v).ok())
128                    .unwrap_or(0),
129                cached_prompt_tokens,
130                cache_creation_tokens: None,
131                cache_read_tokens: None,
132            }
133        }),
134        finish_reason,
135        reasoning,
136        reasoning_details: None,
137        tool_references: Vec::new(),
138        request_id: None,
139        organization_id: None,
140        compaction: None,
141    })
142}