xz_provider/protocol/
openai_chat.rs1use serde_json::Value;
6
7use crate::error::ProviderError;
8use crate::protocol::openai_wire::{
9 apply_chat_completion_options, map_openai_finish_reason, openai_style_auth_headers,
10 parse_chat_usage, to_openai_messages,
11};
12use crate::protocol::{AuthMethod, ProtocolAdapter};
13use crate::types::{CompletionRequest, CompletionResponse, StreamEvent, ToolCall};
14
15#[derive(Debug)]
17pub struct OpenAiChatAdapter;
18
19impl OpenAiChatAdapter {
20 pub fn new() -> Self {
22 Self
23 }
24}
25
26impl Default for OpenAiChatAdapter {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl ProtocolAdapter for OpenAiChatAdapter {
33 fn endpoint_path(&self) -> &str {
34 "/chat/completions"
35 }
36
37 fn build_request_body(
38 &self,
39 request: &CompletionRequest,
40 stream: bool,
41 ) -> Result<Value, ProviderError> {
42 let model = request.model.as_deref().unwrap_or("gpt-4o");
43 let messages = to_openai_messages(&request.messages)?;
44
45 let mut body = serde_json::json!({
46 "model": model,
47 "messages": messages,
48 "stream": stream,
49 });
50 apply_chat_completion_options(&mut body, request)?;
51 Ok(body)
52 }
53
54 fn build_auth_headers(&self, auth: &AuthMethod) -> Vec<(String, String)> {
55 openai_style_auth_headers(auth)
56 }
57
58 fn parse_response(&self, body: &Value) -> Result<CompletionResponse, ProviderError> {
59 let model = body["model"].as_str().unwrap_or("unknown").to_owned();
60
61 let choice = &body["choices"][0];
62 if choice.is_null() {
63 return Err(ProviderError::Format("missing choices[0] in response".to_owned()));
64 }
65
66 let message_obj = choice["message"]
67 .as_object()
68 .ok_or_else(|| ProviderError::Format("missing message in response".to_owned()))?;
69
70 let content = message_obj["content"].as_str().map(|s| s.to_owned());
71
72 let tool_calls: Vec<ToolCall> = message_obj
73 .get("tool_calls")
74 .and_then(|tc| tc.as_array())
75 .map(|calls| {
76 calls
77 .iter()
78 .filter_map(|call| {
79 Some(ToolCall {
80 id: call["id"].as_str()?.to_owned(),
81 function_name: call["function"]["name"].as_str()?.to_owned(),
82 arguments: serde_json::from_str(
83 call["function"]["arguments"].as_str()?,
84 )
85 .ok()?,
86 })
87 })
88 .collect()
89 })
90 .unwrap_or_default();
91
92 let finish_reason = map_openai_finish_reason(choice["finish_reason"].as_str());
93 let usage = parse_chat_usage(&body["usage"]);
94
95 let thinking =
96 message_obj.get("reasoning_content").and_then(|v| v.as_str()).map(|s| s.to_string());
97
98 let refusal = message_obj.get("refusal").and_then(|v| v.as_str()).map(|s| s.to_string());
99
100 Ok(CompletionResponse {
101 content,
102 thinking,
103 tool_calls,
104 usage,
105 model,
106 finish_reason,
107 id: body["id"].as_str().map(|s| s.to_string()),
108 created: body["created"].as_u64(),
109 system_fingerprint: body["system_fingerprint"].as_str().map(|s| s.to_string()),
110 refusal,
111 ..Default::default()
112 })
113 }
114
115 fn parse_sse_event(&self, data: &str) -> Result<Option<StreamEvent>, ProviderError> {
116 if data == "[DONE]" {
117 return Ok(None);
118 }
119
120 let parsed: Value = serde_json::from_str(data)?;
121
122 let has_choices =
123 parsed.get("choices").and_then(|c| c.as_array()).is_some_and(|arr| !arr.is_empty());
124
125 if has_choices {
126 let choice = &parsed["choices"][0];
127
128 if let Some(tool_calls) = choice["delta"]["tool_calls"].as_array() {
131 let mut chosen: Option<&Value> = None;
136 let mut chosen_index = usize::MAX;
137 for tc in tool_calls {
138 if let Some(index) = tc["index"].as_u64() {
139 let idx = index as usize;
140 if idx < chosen_index {
141 chosen_index = idx;
142 chosen = Some(tc);
143 }
144 }
145 }
146 if let Some(tc) = chosen {
147 return Ok(Some(StreamEvent::ToolCallDelta {
148 index: chosen_index,
149 id: tc["id"].as_str().map(String::from),
150 function_name: tc["function"]["name"].as_str().map(String::from),
151 arguments_delta: tc["function"]["arguments"]
152 .as_str()
153 .unwrap_or("")
154 .to_owned(),
155 }));
156 }
157 }
158
159 if let Some(reasoning) = choice["delta"]["reasoning_content"].as_str() {
160 if !reasoning.is_empty() {
161 return Ok(Some(StreamEvent::ThinkingDelta {
162 delta: reasoning.to_owned(),
163 }));
164 }
165 }
166
167 if let Some(content) = choice["delta"]["content"].as_str() {
168 if !content.is_empty() {
169 return Ok(Some(StreamEvent::ContentDelta { delta: content.to_owned() }));
170 }
171 }
172
173 if let Some(fr) = choice["finish_reason"].as_str() {
174 if !fr.is_empty() {
175 let finish_reason = map_openai_finish_reason(Some(fr));
176 let usage = parsed.get("usage").filter(|u| u.is_object()).map(parse_chat_usage);
177 return Ok(Some(StreamEvent::Done { finish_reason, usage }));
178 }
179 }
180 }
181
182 if let Some(usage_val) = parsed.get("usage").filter(|u| u.is_object()) {
183 return Ok(Some(StreamEvent::Usage { usage: parse_chat_usage(usage_val) }));
184 }
185
186 Ok(None)
187 }
188
189 fn protocol_name(&self) -> &str {
190 "openai-chat"
191 }
192}
193
194#[cfg(test)]
195#[path = "openai_chat_tests.rs"]
196mod tests;