vtcode_core/llm/providers/openai/
response_parser.rs1use 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}