1use serde::Deserialize;
10
11use crate::api::llm::LlmRequest;
12use crate::error::{FlowError, Result};
13use crate::json::Json;
14
15use super::request::{AnnotatedLlmRequest, GenerationParams, Message, ToolChoice, ToolDefinition};
16use super::response::{
17 AnnotatedLlmResponse, ApiSpecificResponse, FinishReason, RawUsageCost, ResponseToolCall, Usage,
18 estimate_cost_for_provider, infer_model_provider, provider_reported_cost,
19};
20use super::traits::{LlmCodec, LlmResponseCodec};
21
22pub struct OpenAIChatCodec;
28
29#[derive(Deserialize)]
34struct RawChatCompletion {
35 id: Option<String>,
36 model: Option<String>,
37 choices: Option<Vec<RawChoice>>,
38 usage: Option<RawChatUsage>,
39 system_fingerprint: Option<String>,
40 service_tier: Option<String>,
41 #[serde(flatten)]
42 extra: serde_json::Map<String, Json>,
43}
44
45#[derive(Deserialize)]
46struct RawChoice {
47 message: Option<RawMessage>,
48 finish_reason: Option<String>,
49 logprobs: Option<Json>,
50}
51
52#[derive(Deserialize)]
53struct RawMessage {
54 content: Option<String>,
55 tool_calls: Option<Vec<RawToolCall>>,
56}
57
58#[derive(Deserialize)]
59struct RawToolCall {
60 id: Option<String>,
61 function: Option<RawFunction>,
62}
63
64#[derive(Deserialize)]
65struct RawFunction {
66 name: Option<String>,
67 arguments: Option<String>,
68}
69
70#[derive(Deserialize)]
71struct RawChatUsage {
72 prompt_tokens: Option<u64>,
73 completion_tokens: Option<u64>,
74 total_tokens: Option<u64>,
75 prompt_tokens_details: Option<RawPromptTokensDetails>,
76 #[serde(rename = "cost_usd")]
77 provider_cost: Option<f64>,
78 cost: Option<RawUsageCost>,
79}
80
81#[derive(Deserialize)]
82struct RawPromptTokensDetails {
83 cached_tokens: Option<u64>,
84}
85
86fn map_chat_finish_reason(reason: &str) -> FinishReason {
92 match reason {
93 "stop" => FinishReason::Complete,
94 "length" => FinishReason::Length,
95 "tool_calls" | "function_call" => FinishReason::ToolUse,
96 "content_filter" => FinishReason::ContentFilter,
97 other => FinishReason::Unknown(other.to_string()),
98 }
99}
100
101fn parse_arguments(arguments: &str) -> Json {
105 serde_json::from_str(arguments).unwrap_or_else(|_| Json::String(arguments.to_string()))
106}
107
108const MODELED_REQUEST_KEYS: &[&str] = &[
110 "messages",
111 "model",
112 "temperature",
113 "max_tokens",
114 "max_completion_tokens",
115 "top_p",
116 "stop",
117 "tools",
118 "tool_choice",
119 "store",
120 "user",
121 "metadata",
122 "service_tier",
123 "parallel_tool_calls",
124 "top_logprobs",
125 "stream",
126];
127
128impl LlmResponseCodec for OpenAIChatCodec {
133 fn decode_response(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
134 let raw: RawChatCompletion = serde_json::from_value(response.clone())
135 .map_err(|e| FlowError::Internal(format!("OpenAI Chat response decode: {e}")))?;
136
137 let choice = raw.choices.as_ref().and_then(|c| c.first());
139
140 let message = choice
142 .and_then(|c| c.message.as_ref())
143 .and_then(|m| m.content.as_ref())
144 .map(|s| super::request::MessageContent::Text(s.clone()));
145
146 let tool_calls = choice
150 .and_then(|c| c.message.as_ref())
151 .and_then(|m| m.tool_calls.as_ref())
152 .map(|tcs| {
153 tcs.iter()
154 .filter_map(|tc| {
155 let func = tc.function.as_ref()?;
156 let name = func.name.as_ref()?;
157 Some(ResponseToolCall {
158 id: tc.id.clone().unwrap_or_default(),
159 name: name.clone(),
160 arguments: func
161 .arguments
162 .as_deref()
163 .map(parse_arguments)
164 .unwrap_or(Json::Object(Default::default())),
165 })
166 })
167 .collect::<Vec<_>>()
168 });
169
170 let finish_reason = choice
172 .and_then(|c| c.finish_reason.as_deref())
173 .map(map_chat_finish_reason);
174
175 let model_for_pricing = raw.model.as_deref();
177 let model_provider = infer_model_provider("openai", model_for_pricing);
178 let usage = raw.usage.map(|u| {
179 let mut usage = Usage {
180 prompt_tokens: u.prompt_tokens,
181 completion_tokens: u.completion_tokens,
182 total_tokens: u.total_tokens,
183 cache_read_tokens: u.prompt_tokens_details.and_then(|d| d.cached_tokens),
184 cache_write_tokens: None,
185 cost: provider_reported_cost(u.provider_cost, u.cost),
186 };
187 if usage.cost.is_none() {
188 usage.cost = model_for_pricing.and_then(|model| {
189 estimate_cost_for_provider(model_provider.as_deref(), model, &usage)
190 });
191 }
192 usage
193 });
194
195 let logprobs = choice.and_then(|c| c.logprobs.clone());
197 let api_specific = Some(ApiSpecificResponse::OpenAIChat {
198 logprobs,
199 system_fingerprint: raw.system_fingerprint,
200 service_tier: raw.service_tier,
201 });
202
203 Ok(AnnotatedLlmResponse {
204 id: raw.id,
205 model: raw.model,
206 message,
207 tool_calls,
208 finish_reason,
209 usage,
210 api_specific,
211 extra: raw.extra,
212 })
213 }
214}
215
216impl LlmCodec for OpenAIChatCodec {
221 fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
222 let obj = request
223 .content
224 .as_object()
225 .ok_or_else(|| FlowError::Internal("request content is not an object".into()))?;
226
227 let messages: Vec<Message> = obj
229 .get("messages")
230 .map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
231 .unwrap_or_default();
232
233 let model = obj.get("model").and_then(|v| v.as_str()).map(String::from);
235
236 let temperature = obj.get("temperature").and_then(|v| v.as_f64());
238 let top_p = obj.get("top_p").and_then(|v| v.as_f64());
239 let stop = obj
240 .get("stop")
241 .and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok());
242
243 let max_tokens = obj
245 .get("max_completion_tokens")
246 .and_then(|v| v.as_u64())
247 .or_else(|| obj.get("max_tokens").and_then(|v| v.as_u64()));
248
249 let params =
250 if temperature.is_some() || max_tokens.is_some() || top_p.is_some() || stop.is_some() {
251 Some(GenerationParams {
252 temperature,
253 max_tokens,
254 top_p,
255 stop,
256 })
257 } else {
258 None
259 };
260
261 let tools: Option<Vec<ToolDefinition>> = obj
263 .get("tools")
264 .map(|v| serde_json::from_value(v.clone()))
265 .transpose()
266 .map_err(|e| FlowError::Internal(format!("OpenAI Chat tools decode: {e}")))?;
267
268 let tool_choice: Option<ToolChoice> = obj
270 .get("tool_choice")
271 .map(|v| serde_json::from_value(v.clone()))
272 .transpose()
273 .map_err(|e| FlowError::Internal(format!("OpenAI Chat tool_choice decode: {e}")))?;
274
275 let extra: serde_json::Map<String, Json> = obj
277 .iter()
278 .filter(|(k, _)| !MODELED_REQUEST_KEYS.contains(&k.as_str()))
279 .map(|(k, v)| (k.clone(), v.clone()))
280 .collect();
281
282 Ok(AnnotatedLlmRequest {
283 messages,
284 model,
285 params,
286 tools,
287 tool_choice,
288 store: obj.get("store").and_then(|v| v.as_bool()),
289 previous_response_id: None,
290 truncation: None,
291 reasoning: None,
292 include: None,
293 user: obj.get("user").and_then(|v| v.as_str()).map(String::from),
294 metadata: obj.get("metadata").cloned(),
295 service_tier: obj
296 .get("service_tier")
297 .and_then(|v| v.as_str())
298 .map(String::from),
299 parallel_tool_calls: obj.get("parallel_tool_calls").and_then(|v| v.as_bool()),
300 max_output_tokens: None,
301 max_tool_calls: None,
302 top_logprobs: obj.get("top_logprobs").and_then(|v| v.as_u64()),
303 stream: obj.get("stream").and_then(|v| v.as_bool()),
304 extra,
305 })
306 }
307
308 fn encode(&self, annotated: &AnnotatedLlmRequest, original: &LlmRequest) -> Result<LlmRequest> {
309 let mut content = original.content.clone();
310 let obj = content
311 .as_object_mut()
312 .ok_or_else(|| FlowError::Internal("original content is not an object".into()))?;
313
314 insert_serialized(obj, "messages", &annotated.messages, "messages")?;
315
316 if let Some(ref model) = annotated.model {
317 obj.insert("model".into(), Json::String(model.clone()));
318 }
319
320 if let Some(ref params) = annotated.params {
321 overlay_generation_params(obj, params)?;
322 }
323
324 if let Some(ref tools) = annotated.tools {
325 insert_serialized(obj, "tools", tools, "tools")?;
326 }
327
328 if let Some(ref tool_choice) = annotated.tool_choice {
329 insert_serialized(obj, "tool_choice", tool_choice, "tool_choice")?;
330 }
331
332 if let Some(store) = annotated.store {
333 obj.insert("store".into(), Json::Bool(store));
334 }
335 if let Some(ref user) = annotated.user {
336 obj.insert("user".into(), Json::String(user.clone()));
337 }
338 if let Some(ref metadata) = annotated.metadata {
339 obj.insert("metadata".into(), metadata.clone());
340 }
341 if let Some(ref service_tier) = annotated.service_tier {
342 obj.insert("service_tier".into(), Json::String(service_tier.clone()));
343 }
344 if let Some(parallel_tool_calls) = annotated.parallel_tool_calls {
345 obj.insert(
346 "parallel_tool_calls".into(),
347 Json::Bool(parallel_tool_calls),
348 );
349 }
350 if let Some(top_logprobs) = annotated.top_logprobs {
351 obj.insert("top_logprobs".into(), Json::from(top_logprobs));
352 }
353 if let Some(stream) = annotated.stream {
354 obj.insert("stream".into(), Json::Bool(stream));
355 }
356
357 for (k, v) in &annotated.extra {
358 obj.insert(k.clone(), v.clone());
359 }
360
361 let is_streaming = obj.get("stream").and_then(|v| v.as_bool()).unwrap_or(false);
376 if is_streaming && !obj.contains_key("stream_options") {
377 obj.insert(
378 "stream_options".into(),
379 serde_json::json!({"include_usage": true}),
380 );
381 }
382
383 Ok(LlmRequest {
384 headers: original.headers.clone(),
385 content,
386 })
387 }
388}
389
390fn json_f64(v: f64) -> Json {
392 serde_json::Number::from_f64(v)
393 .map(Json::Number)
394 .unwrap_or(Json::Null)
395}
396
397fn insert_serialized<T: serde::Serialize>(
398 obj: &mut serde_json::Map<String, Json>,
399 key: &str,
400 value: &T,
401 context: &str,
402) -> Result<()> {
403 let json = serde_json::to_value(value)
404 .map_err(|e| FlowError::Internal(format!("OpenAI Chat {context} encode: {e}")))?;
405 obj.insert(key.into(), json);
406 Ok(())
407}
408
409fn overlay_generation_params(
410 obj: &mut serde_json::Map<String, Json>,
411 params: &GenerationParams,
412) -> Result<()> {
413 if let Some(temp) = params.temperature {
414 obj.insert("temperature".into(), json_f64(temp));
415 }
416 if let Some(top_p) = params.top_p {
417 obj.insert("top_p".into(), json_f64(top_p));
418 }
419 if let Some(ref stop) = params.stop {
420 insert_serialized(obj, "stop", stop, "stop")?;
421 }
422 if let Some(max_tokens) = params.max_tokens {
423 let key = if obj.contains_key("max_completion_tokens") {
424 "max_completion_tokens"
425 } else {
426 "max_tokens"
427 };
428 obj.insert(key.into(), Json::from(max_tokens));
429 }
430 Ok(())
431}
432
433pub struct OpenAIChatStreamingCodec {
463 state: std::sync::Arc<std::sync::Mutex<OpenAIChatStreamingState>>,
464}
465
466impl OpenAIChatStreamingCodec {
467 pub fn new() -> Self {
469 Self {
470 state: std::sync::Arc::new(std::sync::Mutex::new(OpenAIChatStreamingState::default())),
471 }
472 }
473}
474
475impl Default for OpenAIChatStreamingCodec {
476 fn default() -> Self {
477 Self::new()
478 }
479}
480
481impl super::streaming::StreamingCodec for OpenAIChatStreamingCodec {
482 fn collector(&self) -> crate::api::runtime::LlmCollectorFn {
483 let state = std::sync::Arc::clone(&self.state);
484 Box::new(move |event: Json| -> Result<()> {
485 let mut guard = state
486 .lock()
487 .unwrap_or_else(|poisoned| poisoned.into_inner());
488 guard.observe(&event);
489 Ok(())
490 })
491 }
492
493 fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn {
494 let state = std::sync::Arc::clone(&self.state);
495 Box::new(move || -> Json {
496 let mut guard = state
497 .lock()
498 .unwrap_or_else(|poisoned| poisoned.into_inner());
499 std::mem::take(&mut *guard).finalize()
500 })
501 }
502}
503
504#[derive(Debug, Default)]
505struct OpenAIChatStreamingState {
506 id: Option<String>,
507 object: Option<String>,
508 created: Option<u64>,
509 model: Option<String>,
510 choices: std::collections::BTreeMap<u64, ChoiceState>,
513 usage: Option<Json>,
515}
516
517#[derive(Debug, Default)]
518struct ChoiceState {
519 role: Option<String>,
520 content: String,
521 has_content: bool,
522 tool_calls: std::collections::BTreeMap<u64, ToolCallState>,
525 finish_reason: Option<String>,
526}
527
528#[derive(Debug, Default)]
529struct ToolCallState {
530 id: Option<String>,
531 type_: Option<String>,
532 name: Option<String>,
533 arguments: String,
534}
535
536impl OpenAIChatStreamingState {
537 fn observe(&mut self, chunk: &Json) {
538 if self.id.is_none()
541 && let Some(id) = chunk.get("id").and_then(Json::as_str)
542 {
543 self.id = Some(id.to_string());
544 }
545 if self.object.is_none()
546 && let Some(obj) = chunk.get("object").and_then(Json::as_str)
547 {
548 self.object = Some(obj.to_string());
549 }
550 if self.created.is_none()
551 && let Some(c) = chunk.get("created").and_then(Json::as_u64)
552 {
553 self.created = Some(c);
554 }
555 if self.model.is_none()
556 && let Some(m) = chunk.get("model").and_then(Json::as_str)
557 {
558 self.model = Some(m.to_string());
559 }
560 if let Some(usage) = chunk.get("usage") {
561 if !usage.is_null() {
564 self.usage = Some(usage.clone());
565 }
566 }
567 let Some(choices) = chunk.get("choices").and_then(Json::as_array) else {
568 return;
569 };
570 for choice in choices {
571 self.observe_choice(choice);
572 }
573 }
574
575 fn observe_choice(&mut self, choice: &Json) {
576 let index = choice.get("index").and_then(Json::as_u64).unwrap_or(0);
577 let entry = self.choices.entry(index).or_default();
578 entry.observe_finish_reason(choice);
579 entry.observe_delta(choice.get("delta"));
580 }
581
582 fn finalize(self) -> Json {
583 let mut output = serde_json::Map::new();
584 if let Some(id) = self.id {
585 output.insert("id".to_string(), Json::String(id));
586 }
587 if let Some(object) = self.object {
592 let normalized = object
593 .strip_suffix(".chunk")
594 .map(str::to_string)
595 .unwrap_or(object);
596 output.insert("object".to_string(), Json::String(normalized));
597 }
598 if let Some(created) = self.created {
599 output.insert("created".to_string(), Json::Number(created.into()));
600 }
601 if let Some(model) = self.model {
602 output.insert("model".to_string(), Json::String(model));
603 }
604 let choices: Vec<Json> = self
605 .choices
606 .into_iter()
607 .map(|(index, choice)| choice.finalize(index))
608 .collect();
609 output.insert("choices".to_string(), Json::Array(choices));
610 if let Some(usage) = self.usage {
611 output.insert("usage".to_string(), usage);
612 }
613 Json::Object(output)
614 }
615}
616
617impl ChoiceState {
618 fn observe_finish_reason(&mut self, choice: &Json) {
619 if let Some(reason) = choice.get("finish_reason").and_then(Json::as_str) {
620 self.finish_reason = Some(reason.to_string());
621 }
622 }
623
624 fn observe_delta(&mut self, delta: Option<&Json>) {
625 let Some(delta) = delta else {
626 return;
627 };
628 if let Some(role) = delta.get("role").and_then(Json::as_str) {
629 self.role = Some(role.to_string());
630 }
631 if let Some(content) = delta.get("content").and_then(Json::as_str) {
632 self.content.push_str(content);
633 self.has_content = true;
634 }
635 self.observe_tool_calls(delta);
636 }
637
638 fn observe_tool_calls(&mut self, delta: &Json) {
639 if let Some(tool_calls) = delta.get("tool_calls").and_then(Json::as_array) {
640 for tool_call in tool_calls {
641 self.observe_tool_call(tool_call);
642 }
643 }
644 }
645
646 fn observe_tool_call(&mut self, tool_call: &Json) {
647 let index = tool_call.get("index").and_then(Json::as_u64).unwrap_or(0);
648 let state = self.tool_calls.entry(index).or_default();
649 if let Some(id) = tool_call.get("id").and_then(Json::as_str) {
650 state.id = Some(id.to_string());
651 }
652 if let Some(type_) = tool_call.get("type").and_then(Json::as_str) {
653 state.type_ = Some(type_.to_string());
654 }
655 if let Some(function) = tool_call.get("function") {
656 state.observe_function(function);
657 }
658 }
659
660 fn finalize(self, index: u64) -> Json {
661 let mut message = serde_json::Map::new();
662 message.insert(
663 "role".to_string(),
664 Json::String(self.role.unwrap_or_else(|| "assistant".to_string())),
665 );
666 if self.has_content {
670 message.insert("content".to_string(), Json::String(self.content));
671 } else {
672 message.insert("content".to_string(), Json::Null);
673 }
674 if !self.tool_calls.is_empty() {
675 let tool_calls: Vec<Json> = self
676 .tool_calls
677 .into_values()
678 .map(ToolCallState::finalize)
679 .collect();
680 message.insert("tool_calls".to_string(), Json::Array(tool_calls));
681 }
682 let mut choice = serde_json::Map::new();
683 choice.insert("index".to_string(), Json::Number(index.into()));
684 choice.insert("message".to_string(), Json::Object(message));
685 if let Some(reason) = self.finish_reason {
686 choice.insert("finish_reason".to_string(), Json::String(reason));
687 } else {
688 choice.insert("finish_reason".to_string(), Json::Null);
689 }
690 Json::Object(choice)
691 }
692}
693
694impl ToolCallState {
695 fn observe_function(&mut self, function: &Json) {
696 if let Some(name) = function.get("name").and_then(Json::as_str) {
697 self.name = Some(name.to_string());
698 }
699 if let Some(args) = function.get("arguments").and_then(Json::as_str) {
700 self.arguments.push_str(args);
701 }
702 }
703
704 fn finalize(self) -> Json {
705 let mut function = serde_json::Map::new();
706 function.insert(
707 "name".to_string(),
708 Json::String(self.name.unwrap_or_default()),
709 );
710 function.insert("arguments".to_string(), Json::String(self.arguments));
711 let mut call = serde_json::Map::new();
712 if let Some(id) = self.id {
713 call.insert("id".to_string(), Json::String(id));
714 }
715 call.insert(
716 "type".to_string(),
717 Json::String(self.type_.unwrap_or_else(|| "function".to_string())),
718 );
719 call.insert("function".to_string(), Json::Object(function));
720 Json::Object(call)
721 }
722}
723
724#[cfg(test)]
729#[path = "../../tests/unit/codec/openai_chat_tests.rs"]
730mod tests;