1use serde::Deserialize;
10
11use crate::api::llm::LlmRequest;
12use crate::api::runtime::{BuiltinLlmCodec, LlmCodecIdentity};
13use crate::error::{FlowError, Result};
14use crate::json::Json;
15
16use super::request::{
17 AnnotatedLlmRequest, ApiSpecificRequest, ContentPart, FunctionCall, FunctionDefinition,
18 GenerationParams, Message, MessageContent, OpenAiImageUrl, ProviderNativeComponent, ToolCall,
19 ToolChoice, ToolDefinition,
20};
21use super::resolve::{ProviderSurface, ProviderSurfaceDescriptor};
22use super::response::{
23 AnnotatedLlmResponse, ApiSpecificResponse, FinishReason, RawUsageCost, ResponseToolCall, Usage,
24 estimate_cost_for_provider, infer_model_provider, provider_reported_cost,
25};
26use super::traits::{LlmCodec, LlmResponseCodec};
27
28pub struct OpenAIChatCodec;
34
35pub(crate) const PROVIDER_SURFACE: ProviderSurfaceDescriptor = ProviderSurfaceDescriptor {
36 surface: ProviderSurface::OpenAIChat,
37 detect_request: |obj, _| obj.contains_key("messages"),
38 detect_response: |obj| obj.get("choices").is_some_and(Json::is_array),
39 decode_request: |request| OpenAIChatCodec.decode(request),
40 decode_response: |raw| OpenAIChatCodec.decode_response(raw),
41 codec_name: "openai_chat",
42 request_codec: || std::sync::Arc::new(OpenAIChatCodec),
43 response_codec: || std::sync::Arc::new(OpenAIChatCodec),
44 streaming_codec: || Box::new(OpenAIChatStreamingCodec::new()),
45};
46
47#[derive(Deserialize)]
52struct RawChatCompletion {
53 id: Option<String>,
54 model: Option<String>,
55 choices: Option<Vec<RawChoice>>,
56 usage: Option<RawChatUsage>,
57 system_fingerprint: Option<String>,
58 service_tier: Option<String>,
59 #[serde(flatten)]
60 extra: serde_json::Map<String, Json>,
61}
62
63#[derive(Deserialize)]
64struct RawChoice {
65 message: Option<RawMessage>,
66 finish_reason: Option<String>,
67 logprobs: Option<Json>,
68}
69
70#[derive(Deserialize)]
71struct RawMessage {
72 content: Option<String>,
73 tool_calls: Option<Vec<RawToolCall>>,
74}
75
76#[derive(Deserialize)]
77struct RawToolCall {
78 id: Option<String>,
79 function: Option<RawFunction>,
80}
81
82#[derive(Deserialize)]
83struct RawFunction {
84 name: Option<String>,
85 arguments: Option<String>,
86}
87
88#[derive(Deserialize)]
89struct RawChatUsage {
90 prompt_tokens: Option<u64>,
91 completion_tokens: Option<u64>,
92 total_tokens: Option<u64>,
93 prompt_tokens_details: Option<RawPromptTokensDetails>,
94 #[serde(rename = "cost_usd")]
95 provider_cost: Option<f64>,
96 cost: Option<RawUsageCost>,
97}
98
99#[derive(Deserialize)]
100struct RawPromptTokensDetails {
101 cached_tokens: Option<u64>,
102}
103
104fn map_chat_finish_reason(reason: &str) -> FinishReason {
110 match reason {
111 "stop" => FinishReason::Complete,
112 "length" => FinishReason::Length,
113 "tool_calls" | "function_call" => FinishReason::ToolUse,
114 "content_filter" => FinishReason::ContentFilter,
115 other => FinishReason::Unknown(other.to_string()),
116 }
117}
118
119fn parse_arguments(arguments: &str) -> Json {
123 serde_json::from_str(arguments).unwrap_or_else(|_| Json::String(arguments.to_string()))
124}
125
126const MODELED_REQUEST_KEYS: &[&str] = &[
128 "messages",
129 "model",
130 "temperature",
131 "max_tokens",
132 "max_completion_tokens",
133 "top_p",
134 "stop",
135 "tools",
136 "tool_choice",
137 "store",
138 "user",
139 "metadata",
140 "service_tier",
141 "parallel_tool_calls",
142 "top_logprobs",
143 "stream",
144 "audio",
145 "frequency_penalty",
146 "function_call",
147 "functions",
148 "logit_bias",
149 "logprobs",
150 "modalities",
151 "moderation",
152 "n",
153 "prediction",
154 "presence_penalty",
155 "prompt_cache_key",
156 "prompt_cache_options",
157 "prompt_cache_retention",
158 "reasoning_effort",
159 "response_format",
160 "safety_identifier",
161 "seed",
162 "stream_options",
163 "verbosity",
164 "web_search_options",
165];
166
167fn chat_native(kind: &str, value: &Json) -> ProviderNativeComponent {
168 ProviderNativeComponent {
169 provider: "openai_chat".into(),
170 kind: kind.to_string(),
171 value: value.clone(),
172 }
173}
174
175fn decode_chat_content(value: &Json) -> Result<MessageContent> {
176 if let Some(text) = value.as_str() {
177 return Ok(MessageContent::Text(text.to_string()));
178 }
179 let parts = value.as_array().ok_or_else(|| {
180 FlowError::InvalidArgument("OpenAI Chat message content must be a string or array".into())
181 })?;
182 Ok(MessageContent::Parts(
183 parts
184 .iter()
185 .map(decode_chat_content_part)
186 .collect::<Result<Vec<_>>>()?,
187 ))
188}
189
190fn decode_chat_content_part(value: &Json) -> Result<ContentPart> {
191 let obj = value.as_object().ok_or_else(|| {
192 FlowError::InvalidArgument("OpenAI Chat content part must be an object".into())
193 })?;
194 let kind = obj.get("type").and_then(Json::as_str).unwrap_or("unknown");
195 match kind {
196 "text" => Ok(ContentPart::Text {
197 text: obj
198 .get("text")
199 .and_then(Json::as_str)
200 .ok_or_else(|| {
201 FlowError::InvalidArgument("OpenAI Chat text part is missing text".into())
202 })?
203 .to_string(),
204 extra: obj
205 .iter()
206 .filter(|(key, _)| !matches!(key.as_str(), "type" | "text"))
207 .map(|(key, value)| (key.clone(), value.clone()))
208 .collect(),
209 }),
210 "image_url" => {
211 let image_url: OpenAiImageUrl =
212 serde_json::from_value(obj.get("image_url").cloned().ok_or_else(|| {
213 FlowError::InvalidArgument("OpenAI Chat image part is missing image_url".into())
214 })?)
215 .map_err(|error| {
216 FlowError::InvalidArgument(format!("invalid OpenAI Chat image_url: {error}"))
217 })?;
218 Ok(ContentPart::ImageUrl {
219 image_url,
220 extra: obj
221 .iter()
222 .filter(|(key, _)| !matches!(key.as_str(), "type" | "image_url"))
223 .map(|(key, value)| (key.clone(), value.clone()))
224 .collect(),
225 })
226 }
227 "input_audio" => Ok(ContentPart::Audio {
228 audio: obj.get("input_audio").cloned().ok_or_else(|| {
229 FlowError::InvalidArgument("OpenAI Chat audio part is missing input_audio".into())
230 })?,
231 extra: obj
232 .iter()
233 .filter(|(key, _)| !matches!(key.as_str(), "type" | "input_audio"))
234 .map(|(key, value)| (key.clone(), value.clone()))
235 .collect(),
236 }),
237 "file" => Ok(ContentPart::File {
238 file: obj.get("file").cloned().ok_or_else(|| {
239 FlowError::InvalidArgument("OpenAI Chat file part is missing file".into())
240 })?,
241 extra: obj
242 .iter()
243 .filter(|(key, _)| !matches!(key.as_str(), "type" | "file"))
244 .map(|(key, value)| (key.clone(), value.clone()))
245 .collect(),
246 }),
247 "refusal" => Ok(ContentPart::Refusal {
248 refusal: obj
249 .get("refusal")
250 .and_then(Json::as_str)
251 .ok_or_else(|| {
252 FlowError::InvalidArgument("OpenAI Chat refusal part is missing refusal".into())
253 })?
254 .to_string(),
255 extra: obj
256 .iter()
257 .filter(|(key, _)| !matches!(key.as_str(), "type" | "refusal"))
258 .map(|(key, value)| (key.clone(), value.clone()))
259 .collect(),
260 }),
261 _ => Ok(ContentPart::ProviderNative {
262 provider: "openai_chat".into(),
263 kind: kind.to_string(),
264 value: value.clone(),
265 }),
266 }
267}
268
269fn optional_chat_string(
270 obj: &serde_json::Map<String, Json>,
271 key: &str,
272 context: &str,
273) -> Result<Option<String>> {
274 match obj.get(key) {
275 Some(Json::Null) | None => Ok(None),
276 Some(Json::String(value)) => Ok(Some(value.clone())),
277 Some(_) => Err(FlowError::InvalidArgument(format!(
278 "OpenAI Chat {context} {key} must be a string or null"
279 ))),
280 }
281}
282
283fn decode_chat_tool_call(value: &Json) -> Result<Option<ToolCall>> {
284 let obj = value.as_object().ok_or_else(|| {
285 FlowError::InvalidArgument("OpenAI Chat tool call must be an object".into())
286 })?;
287 if obj.get("type").and_then(Json::as_str) != Some("function") {
288 return Ok(None);
289 }
290 if obj
291 .keys()
292 .any(|key| !matches!(key.as_str(), "id" | "type" | "function"))
293 {
294 return Ok(None);
295 }
296 let function = obj
297 .get("function")
298 .and_then(Json::as_object)
299 .ok_or_else(|| {
300 FlowError::InvalidArgument("OpenAI Chat function tool call is missing function".into())
301 })?;
302 if function
303 .keys()
304 .any(|key| !matches!(key.as_str(), "name" | "arguments"))
305 {
306 return Ok(None);
307 }
308 let id = obj.get("id").and_then(Json::as_str).ok_or_else(|| {
309 FlowError::InvalidArgument("OpenAI Chat function tool call is missing id".into())
310 })?;
311 let name = function.get("name").and_then(Json::as_str).ok_or_else(|| {
312 FlowError::InvalidArgument("OpenAI Chat function tool call is missing name".into())
313 })?;
314 let arguments = function
315 .get("arguments")
316 .and_then(Json::as_str)
317 .ok_or_else(|| {
318 FlowError::InvalidArgument("OpenAI Chat function tool call is missing arguments".into())
319 })?;
320 Ok(Some(ToolCall {
321 id: id.to_string(),
322 call_type: "function".into(),
323 function: FunctionCall {
324 name: name.to_string(),
325 arguments: arguments.to_string(),
326 },
327 }))
328}
329
330fn decode_chat_message(value: &Json) -> Result<Message> {
331 let obj = value.as_object().ok_or_else(|| {
332 FlowError::InvalidArgument("OpenAI Chat message must be an object".into())
333 })?;
334 let role = obj
335 .get("role")
336 .and_then(Json::as_str)
337 .ok_or_else(|| FlowError::InvalidArgument("OpenAI Chat message is missing role".into()))?;
338 let native = || Message::ProviderNative {
339 provider: "openai_chat".into(),
340 kind: role.to_string(),
341 value: value.clone(),
342 };
343 match role {
344 "system" | "developer" | "user" => {
345 if obj
346 .keys()
347 .any(|key| !matches!(key.as_str(), "role" | "content" | "name"))
348 {
349 return Ok(native());
350 }
351 let content = decode_chat_content(obj.get("content").ok_or_else(|| {
352 FlowError::InvalidArgument("OpenAI Chat message is missing content".into())
353 })?)?;
354 let name = optional_chat_string(obj, "name", "message")?;
355 Ok(match role {
356 "system" => Message::System { content, name },
357 "developer" => Message::Developer { content, name },
358 _ => Message::User { content, name },
359 })
360 }
361 "assistant" => {
362 if obj
363 .keys()
364 .any(|key| !matches!(key.as_str(), "role" | "content" | "tool_calls" | "name"))
365 {
366 return Ok(native());
367 }
368 let content = obj
369 .get("content")
370 .filter(|content| !content.is_null())
371 .map(decode_chat_content)
372 .transpose()?;
373 let tool_calls = match obj.get("tool_calls") {
374 Some(Json::Null) | None => None,
375 Some(Json::Array(calls)) => {
376 let decoded = calls
377 .iter()
378 .map(decode_chat_tool_call)
379 .collect::<Result<Vec<_>>>()?;
380 let Some(decoded) = decoded.into_iter().collect::<Option<Vec<_>>>() else {
381 return Ok(native());
382 };
383 Some(decoded)
384 }
385 Some(_) => {
386 return Err(FlowError::InvalidArgument(
387 "OpenAI Chat assistant tool_calls must be an array or null".into(),
388 ));
389 }
390 };
391 Ok(Message::Assistant {
392 content,
393 tool_calls,
394 name: optional_chat_string(obj, "name", "assistant message")?,
395 })
396 }
397 "tool"
398 if obj
399 .keys()
400 .all(|key| matches!(key.as_str(), "role" | "content" | "tool_call_id")) =>
401 {
402 Ok(Message::Tool {
403 content: decode_chat_content(obj.get("content").ok_or_else(|| {
404 FlowError::InvalidArgument("OpenAI Chat tool message is missing content".into())
405 })?)?,
406 tool_call_id: obj
407 .get("tool_call_id")
408 .and_then(Json::as_str)
409 .ok_or_else(|| {
410 FlowError::InvalidArgument(
411 "OpenAI Chat tool message is missing tool_call_id".into(),
412 )
413 })?
414 .to_string(),
415 })
416 }
417 "function"
418 if obj
419 .keys()
420 .all(|key| matches!(key.as_str(), "role" | "content" | "name")) =>
421 {
422 Ok(Message::Function {
423 content: optional_chat_string(obj, "content", "function message")?,
424 name: obj
425 .get("name")
426 .and_then(Json::as_str)
427 .ok_or_else(|| {
428 FlowError::InvalidArgument(
429 "OpenAI Chat function message is missing name".into(),
430 )
431 })?
432 .to_string(),
433 })
434 }
435 _ => Ok(native()),
436 }
437}
438
439fn encode_chat_content(content: &MessageContent) -> Result<Json> {
440 match content {
441 MessageContent::Text(text) => Ok(Json::String(text.clone())),
442 MessageContent::Parts(parts) => Ok(Json::Array(
443 parts
444 .iter()
445 .map(|part| match part {
446 ContentPart::Text { text, extra } => {
447 let mut obj = extra.clone();
448 obj.insert("type".into(), Json::String("text".into()));
449 obj.insert("text".into(), Json::String(text.clone()));
450 Ok(Json::Object(obj))
451 }
452 ContentPart::ImageUrl { image_url, extra } => {
453 let mut obj = extra.clone();
454 obj.insert("type".into(), Json::String("image_url".into()));
455 obj.insert(
456 "image_url".into(),
457 serde_json::to_value(image_url).map_err(|error| {
458 FlowError::Internal(format!(
459 "OpenAI Chat image URL encode: {error}"
460 ))
461 })?,
462 );
463 Ok(Json::Object(obj))
464 }
465 ContentPart::Audio { audio, extra } => {
466 let mut obj = extra.clone();
467 obj.insert("type".into(), Json::String("input_audio".into()));
468 obj.insert("input_audio".into(), audio.clone());
469 Ok(Json::Object(obj))
470 }
471 ContentPart::File { file, extra } => {
472 let mut obj = extra.clone();
473 obj.insert("type".into(), Json::String("file".into()));
474 obj.insert("file".into(), file.clone());
475 Ok(Json::Object(obj))
476 }
477 ContentPart::Refusal { refusal, extra } => {
478 let mut obj = extra.clone();
479 obj.insert("type".into(), Json::String("refusal".into()));
480 obj.insert("refusal".into(), Json::String(refusal.clone()));
481 Ok(Json::Object(obj))
482 }
483 ContentPart::ProviderNative {
484 provider, value, ..
485 } if provider == "openai_chat" => Ok(value.clone()),
486 other => Err(FlowError::InvalidArgument(format!(
487 "content part {other:?} cannot be encoded for OpenAI Chat"
488 ))),
489 })
490 .collect::<Result<Vec<_>>>()?,
491 )),
492 }
493}
494
495fn encode_chat_message(message: &Message) -> Result<Json> {
496 let message_with_content =
497 |role: &str, content: &MessageContent, name: &Option<String>| -> Result<Json> {
498 let mut obj = serde_json::Map::new();
499 obj.insert("role".into(), Json::String(role.into()));
500 obj.insert("content".into(), encode_chat_content(content)?);
501 if let Some(name) = name {
502 obj.insert("name".into(), Json::String(name.clone()));
503 }
504 Ok(Json::Object(obj))
505 };
506 match message {
507 Message::System { content, name } => message_with_content("system", content, name),
508 Message::Developer { content, name } => message_with_content("developer", content, name),
509 Message::User { content, name } => message_with_content("user", content, name),
510 Message::Assistant {
511 content,
512 tool_calls,
513 name,
514 } => {
515 let mut obj = serde_json::Map::new();
516 obj.insert("role".into(), Json::String("assistant".into()));
517 if let Some(content) = content {
518 obj.insert("content".into(), encode_chat_content(content)?);
519 }
520 if let Some(tool_calls) = tool_calls {
521 obj.insert(
522 "tool_calls".into(),
523 serde_json::to_value(tool_calls).map_err(|error| {
524 FlowError::Internal(format!("OpenAI Chat tool calls encode: {error}"))
525 })?,
526 );
527 }
528 if let Some(name) = name {
529 obj.insert("name".into(), Json::String(name.clone()));
530 }
531 Ok(Json::Object(obj))
532 }
533 Message::Tool {
534 content,
535 tool_call_id,
536 } => {
537 let mut obj = serde_json::Map::new();
538 obj.insert("role".into(), Json::String("tool".into()));
539 obj.insert("content".into(), encode_chat_content(content)?);
540 obj.insert("tool_call_id".into(), Json::String(tool_call_id.clone()));
541 Ok(Json::Object(obj))
542 }
543 Message::Function { content, name } => {
544 let mut obj = serde_json::Map::new();
545 obj.insert("role".into(), Json::String("function".into()));
546 obj.insert(
547 "content".into(),
548 content.clone().map(Json::String).unwrap_or(Json::Null),
549 );
550 obj.insert("name".into(), Json::String(name.clone()));
551 Ok(Json::Object(obj))
552 }
553 Message::ProviderNative {
554 provider, value, ..
555 } if provider == "openai_chat" => Ok(value.clone()),
556 other => Err(FlowError::InvalidArgument(format!(
557 "message {other:?} cannot be encoded for OpenAI Chat"
558 ))),
559 }
560}
561
562fn decode_chat_tool(value: &Json) -> Result<ToolDefinition> {
563 let obj = value
564 .as_object()
565 .ok_or_else(|| FlowError::InvalidArgument("OpenAI Chat tool must be an object".into()))?;
566 if obj.get("type").and_then(Json::as_str) != Some("function") {
567 let native = chat_native(
568 obj.get("type").and_then(Json::as_str).unwrap_or("unknown"),
569 value,
570 );
571 return Ok(ToolDefinition::ProviderNative {
572 provider: native.provider,
573 kind: native.kind,
574 value: native.value,
575 });
576 }
577 let function = obj
578 .get("function")
579 .and_then(Json::as_object)
580 .ok_or_else(|| {
581 FlowError::InvalidArgument("OpenAI Chat function tool is missing function".into())
582 })?;
583 let name = function.get("name").and_then(Json::as_str).ok_or_else(|| {
584 FlowError::InvalidArgument("OpenAI Chat function tool is missing name".into())
585 })?;
586 let description = super::optional_string(function, "description", "OpenAI Chat function tool")?;
587 let strict = super::optional_bool(function, "strict", "OpenAI Chat function tool")?;
588 Ok(ToolDefinition::Function {
589 function: FunctionDefinition {
590 name: name.to_string(),
591 description,
592 parameters: function.get("parameters").cloned(),
593 strict,
594 extra: function
595 .iter()
596 .filter(|(key, _)| {
597 !matches!(
598 key.as_str(),
599 "name" | "description" | "parameters" | "strict"
600 )
601 })
602 .map(|(key, value)| (key.clone(), value.clone()))
603 .collect(),
604 },
605 extra: obj
606 .iter()
607 .filter(|(key, _)| !matches!(key.as_str(), "type" | "function"))
608 .map(|(key, value)| (key.clone(), value.clone()))
609 .collect(),
610 })
611}
612
613fn encode_chat_tool(tool: &ToolDefinition) -> Result<Json> {
614 match tool {
615 ToolDefinition::Function { function, extra } => {
616 let mut function_obj = function.extra.clone();
617 function_obj.insert("name".into(), Json::String(function.name.clone()));
618 if let Some(description) = &function.description {
619 function_obj.insert("description".into(), Json::String(description.clone()));
620 }
621 if let Some(parameters) = &function.parameters {
622 function_obj.insert("parameters".into(), parameters.clone());
623 }
624 if let Some(strict) = function.strict {
625 function_obj.insert("strict".into(), Json::Bool(strict));
626 }
627 let mut obj = extra.clone();
628 obj.insert("type".into(), Json::String("function".into()));
629 obj.insert("function".into(), Json::Object(function_obj));
630 Ok(Json::Object(obj))
631 }
632 ToolDefinition::ProviderNative {
633 provider, value, ..
634 } if provider == "openai_chat" => Ok(value.clone()),
635 other => Err(FlowError::InvalidArgument(format!(
636 "tool {other:?} cannot be encoded for OpenAI Chat"
637 ))),
638 }
639}
640
641fn decode_chat_tool_choice(value: &Json) -> ToolChoice {
642 match value.as_str() {
643 Some("auto") => ToolChoice::Auto,
644 Some("none") => ToolChoice::None,
645 Some("required") => ToolChoice::Required,
646 _ => {
647 if let Some(function) = value
648 .as_object()
649 .filter(|obj| obj.get("type").and_then(Json::as_str) == Some("function"))
650 .and_then(|obj| obj.get("function"))
651 .and_then(Json::as_object)
652 .and_then(|function| function.get("name"))
653 .and_then(Json::as_str)
654 {
655 ToolChoice::Specific(super::request::ToolChoiceFunction {
656 choice_type: "function".into(),
657 function: super::request::ToolChoiceFunctionName {
658 name: function.to_string(),
659 },
660 })
661 } else {
662 ToolChoice::ProviderNative(chat_native("tool_choice", value))
663 }
664 }
665 }
666}
667
668fn encode_chat_tool_choice(choice: &ToolChoice) -> Result<Json> {
669 match choice {
670 ToolChoice::Auto => Ok(Json::String("auto".into())),
671 ToolChoice::None => Ok(Json::String("none".into())),
672 ToolChoice::Required => Ok(Json::String("required".into())),
673 ToolChoice::Specific(choice) => Ok(serde_json::json!({
674 "type":"function",
675 "function":{"name":choice.function.name}
676 })),
677 ToolChoice::ProviderNative(native) if native.provider == "openai_chat" => {
678 Ok(native.value.clone())
679 }
680 ToolChoice::ProviderNative(native) => Err(FlowError::InvalidArgument(format!(
681 "tool choice for {} cannot be encoded for OpenAI Chat",
682 native.provider
683 ))),
684 }
685}
686
687fn patch_extra_fields(
688 obj: &mut serde_json::Map<String, Json>,
689 baseline: &serde_json::Map<String, Json>,
690 edited: &serde_json::Map<String, Json>,
691) {
692 for key in baseline.keys().filter(|key| !edited.contains_key(*key)) {
693 obj.remove(key);
694 }
695 for (key, value) in edited {
696 if baseline.get(key) != Some(value) {
697 obj.insert(key.clone(), value.clone());
698 }
699 }
700}
701
702fn set_or_remove_json(obj: &mut serde_json::Map<String, Json>, key: &str, value: Option<Json>) {
703 if let Some(value) = value {
704 obj.insert(key.into(), value);
705 } else {
706 obj.remove(key);
707 }
708}
709
710fn patch_chat_messages_and_validate(
711 obj: &mut serde_json::Map<String, Json>,
712 annotated: &AnnotatedLlmRequest,
713 baseline: &AnnotatedLlmRequest,
714) -> Result<()> {
715 if annotated.messages != baseline.messages {
716 let original_messages = obj.get("messages").and_then(Json::as_array);
717 obj.insert(
718 "messages".into(),
719 Json::Array(super::encode_changed_items(
720 &annotated.messages,
721 &baseline.messages,
722 original_messages.map(Vec::as_slice),
723 encode_chat_message,
724 )?),
725 );
726 }
727 let unsupported = [
728 annotated.instructions != baseline.instructions,
729 annotated.previous_response_id != baseline.previous_response_id,
730 annotated.truncation != baseline.truncation,
731 annotated.reasoning != baseline.reasoning,
732 annotated.include != baseline.include,
733 annotated.max_output_tokens != baseline.max_output_tokens,
734 annotated.max_tool_calls != baseline.max_tool_calls,
735 ]
736 .into_iter()
737 .any(|changed| changed);
738 if unsupported {
739 return Err(FlowError::InvalidArgument(
740 "request contains fields that cannot be encoded for OpenAI Chat".into(),
741 ));
742 }
743 Ok(())
744}
745
746fn patch_chat_model_and_params(
747 obj: &mut serde_json::Map<String, Json>,
748 annotated: &AnnotatedLlmRequest,
749 baseline: &AnnotatedLlmRequest,
750) {
751 if annotated.model != baseline.model {
752 set_or_remove_json(obj, "model", annotated.model.clone().map(Json::String));
753 }
754 if annotated.params == baseline.params {
755 return;
756 }
757 let edited = annotated.params.as_ref();
758 let before = baseline.params.as_ref();
759 patch_chat_optional_params(obj, edited, before);
760 patch_chat_max_tokens(obj, edited, before);
761 patch_chat_stop_sequences(obj, edited, before);
762}
763
764fn patch_chat_optional_params(
765 obj: &mut serde_json::Map<String, Json>,
766 edited: Option<&GenerationParams>,
767 before: Option<&GenerationParams>,
768) {
769 for (key, value, old_value) in [
770 (
771 "temperature",
772 edited.and_then(|params| params.temperature),
773 before.and_then(|params| params.temperature),
774 ),
775 (
776 "top_p",
777 edited.and_then(|params| params.top_p),
778 before.and_then(|params| params.top_p),
779 ),
780 ] {
781 if value != old_value {
782 set_or_remove_json(obj, key, value.map(json_f64));
783 }
784 }
785}
786
787fn patch_chat_max_tokens(
788 obj: &mut serde_json::Map<String, Json>,
789 edited: Option<&GenerationParams>,
790 before: Option<&GenerationParams>,
791) {
792 let max_tokens = edited.and_then(|params| params.max_tokens);
793 if max_tokens == before.and_then(|params| params.max_tokens) {
794 return;
795 }
796 let max_tokens = max_tokens.map(Json::from);
797 if max_tokens.is_none() {
798 obj.remove("max_completion_tokens");
799 obj.remove("max_tokens");
800 return;
801 }
802 let key = if obj.contains_key("max_completion_tokens") || !obj.contains_key("max_tokens") {
803 "max_completion_tokens"
804 } else {
805 "max_tokens"
806 };
807 set_or_remove_json(obj, key, max_tokens);
808}
809
810fn patch_chat_stop_sequences(
811 obj: &mut serde_json::Map<String, Json>,
812 edited: Option<&GenerationParams>,
813 before: Option<&GenerationParams>,
814) {
815 let stop = edited.and_then(|params| params.stop.as_ref());
816 if stop == before.and_then(|params| params.stop.as_ref()) {
817 return;
818 }
819 let stop = stop.map(|values| {
820 if obj.get("stop").is_some_and(Json::is_string) && values.len() == 1 {
821 Json::String(values[0].clone())
822 } else {
823 serde_json::json!(values)
824 }
825 });
826 set_or_remove_json(obj, "stop", stop);
827}
828
829fn patch_chat_tools(
830 obj: &mut serde_json::Map<String, Json>,
831 annotated: &AnnotatedLlmRequest,
832 baseline: &AnnotatedLlmRequest,
833) -> Result<()> {
834 if annotated.tools != baseline.tools {
835 let tools = annotated
836 .tools
837 .as_deref()
838 .map(|tools| {
839 super::encode_changed_items(
840 tools,
841 baseline.tools.as_deref().unwrap_or(&[]),
842 obj.get("tools").and_then(Json::as_array).map(Vec::as_slice),
843 encode_chat_tool,
844 )
845 })
846 .transpose()?
847 .map(Json::Array);
848 set_or_remove_json(obj, "tools", tools);
849 }
850 if annotated.tool_choice != baseline.tool_choice {
851 let tool_choice = match (&annotated.tool_choice, &baseline.tool_choice) {
852 (Some(edited), Some(before)) => {
853 let edited = encode_chat_tool_choice(edited)?;
854 let before = encode_chat_tool_choice(before)?;
855 Some(match obj.get("tool_choice") {
856 Some(original) => super::patch_changed_json(original, &before, &edited)?,
857 None => edited,
858 })
859 }
860 (Some(edited), None) => Some(encode_chat_tool_choice(edited)?),
861 (None, _) => None,
862 };
863 set_or_remove_json(obj, "tool_choice", tool_choice);
864 }
865 Ok(())
866}
867
868fn patch_chat_common_fields(
869 obj: &mut serde_json::Map<String, Json>,
870 annotated: &AnnotatedLlmRequest,
871 baseline: &AnnotatedLlmRequest,
872) {
873 if annotated.metadata != baseline.metadata {
874 set_or_remove_json(obj, "metadata", annotated.metadata.clone());
875 }
876 for (key, edited, before) in [
877 ("store", annotated.store, baseline.store),
878 (
879 "parallel_tool_calls",
880 annotated.parallel_tool_calls,
881 baseline.parallel_tool_calls,
882 ),
883 ("stream", annotated.stream, baseline.stream),
884 ] {
885 if edited != before {
886 set_or_remove_json(obj, key, edited.map(Json::Bool));
887 }
888 }
889 for (key, edited, before) in [
890 ("user", &annotated.user, &baseline.user),
891 (
892 "service_tier",
893 &annotated.service_tier,
894 &baseline.service_tier,
895 ),
896 ] {
897 if edited != before {
898 set_or_remove_json(obj, key, edited.clone().map(Json::String));
899 }
900 }
901 if annotated.top_logprobs != baseline.top_logprobs {
902 set_or_remove_json(obj, "top_logprobs", annotated.top_logprobs.map(Json::from));
903 }
904}
905
906fn patch_optional_json_fields(
907 obj: &mut serde_json::Map<String, Json>,
908 fields: &[(&str, &Option<Json>, &Option<Json>)],
909) {
910 for (key, edited, before) in fields {
911 if edited != before {
912 set_or_remove_json(obj, key, (*edited).clone());
913 }
914 }
915}
916
917fn patch_optional_string_fields(
918 obj: &mut serde_json::Map<String, Json>,
919 fields: &[(&str, &Option<String>, &Option<String>)],
920) {
921 for (key, edited, before) in fields {
922 if edited != before {
923 set_or_remove_json(obj, key, (*edited).clone().map(Json::String));
924 }
925 }
926}
927
928fn patch_optional_f64_fields(
929 obj: &mut serde_json::Map<String, Json>,
930 fields: &[(&str, &Option<f64>, &Option<f64>)],
931) {
932 for (key, edited, before) in fields {
933 if edited != before {
934 set_or_remove_json(obj, key, (*edited).map(json_f64));
935 }
936 }
937}
938
939fn patch_chat_api_specific(
940 obj: &mut serde_json::Map<String, Json>,
941 edited: &Option<ApiSpecificRequest>,
942 baseline: &Option<ApiSpecificRequest>,
943) -> Result<()> {
944 match (edited, baseline) {
945 (
946 Some(edited @ ApiSpecificRequest::OpenAIChat { .. }),
947 Some(baseline @ ApiSpecificRequest::OpenAIChat { .. }),
948 ) => {
949 patch_chat_api_fields(obj, edited, baseline);
950 Ok(())
951 }
952 (None, Some(ApiSpecificRequest::OpenAIChat { .. })) => {
953 for key in [
954 "audio",
955 "frequency_penalty",
956 "function_call",
957 "functions",
958 "logit_bias",
959 "logprobs",
960 "modalities",
961 "moderation",
962 "n",
963 "prediction",
964 "presence_penalty",
965 "prompt_cache_key",
966 "prompt_cache_options",
967 "prompt_cache_retention",
968 "reasoning_effort",
969 "response_format",
970 "safety_identifier",
971 "seed",
972 "stream_options",
973 "verbosity",
974 "web_search_options",
975 ] {
976 obj.remove(key);
977 }
978 Ok(())
979 }
980 (Some(_), _) | (None, Some(_)) => Err(FlowError::InvalidArgument(
981 "api_specific provider does not match OpenAI Chat".into(),
982 )),
983 (None, None) => Ok(()),
984 }
985}
986
987fn patch_chat_api_fields(
988 obj: &mut serde_json::Map<String, Json>,
989 edited: &ApiSpecificRequest,
990 baseline: &ApiSpecificRequest,
991) {
992 let (
993 ApiSpecificRequest::OpenAIChat {
994 audio,
995 frequency_penalty,
996 function_call,
997 functions,
998 logit_bias,
999 logprobs,
1000 modalities,
1001 moderation,
1002 n,
1003 prediction,
1004 presence_penalty,
1005 prompt_cache_key,
1006 prompt_cache_options,
1007 prompt_cache_retention,
1008 reasoning_effort,
1009 response_format,
1010 safety_identifier,
1011 seed,
1012 stream_options,
1013 verbosity,
1014 web_search_options,
1015 },
1016 ApiSpecificRequest::OpenAIChat {
1017 audio: old_audio,
1018 frequency_penalty: old_frequency_penalty,
1019 function_call: old_function_call,
1020 functions: old_functions,
1021 logit_bias: old_logit_bias,
1022 logprobs: old_logprobs,
1023 modalities: old_modalities,
1024 moderation: old_moderation,
1025 n: old_n,
1026 prediction: old_prediction,
1027 presence_penalty: old_presence_penalty,
1028 prompt_cache_key: old_prompt_cache_key,
1029 prompt_cache_options: old_prompt_cache_options,
1030 prompt_cache_retention: old_prompt_cache_retention,
1031 reasoning_effort: old_reasoning_effort,
1032 response_format: old_response_format,
1033 safety_identifier: old_safety_identifier,
1034 seed: old_seed,
1035 stream_options: old_stream_options,
1036 verbosity: old_verbosity,
1037 web_search_options: old_web_search_options,
1038 },
1039 ) = (edited, baseline)
1040 else {
1041 unreachable!("OpenAI Chat variants checked by caller");
1042 };
1043 patch_optional_json_fields(
1044 obj,
1045 &[
1046 ("audio", audio, old_audio),
1047 ("function_call", function_call, old_function_call),
1048 ("logit_bias", logit_bias, old_logit_bias),
1049 ("moderation", moderation, old_moderation),
1050 ("prediction", prediction, old_prediction),
1051 (
1052 "prompt_cache_options",
1053 prompt_cache_options,
1054 old_prompt_cache_options,
1055 ),
1056 ("response_format", response_format, old_response_format),
1057 ("stream_options", stream_options, old_stream_options),
1058 (
1059 "web_search_options",
1060 web_search_options,
1061 old_web_search_options,
1062 ),
1063 ],
1064 );
1065 patch_optional_f64_fields(
1066 obj,
1067 &[
1068 (
1069 "frequency_penalty",
1070 frequency_penalty,
1071 old_frequency_penalty,
1072 ),
1073 ("presence_penalty", presence_penalty, old_presence_penalty),
1074 ],
1075 );
1076 patch_optional_string_fields(
1077 obj,
1078 &[
1079 ("prompt_cache_key", prompt_cache_key, old_prompt_cache_key),
1080 (
1081 "prompt_cache_retention",
1082 prompt_cache_retention,
1083 old_prompt_cache_retention,
1084 ),
1085 ("reasoning_effort", reasoning_effort, old_reasoning_effort),
1086 (
1087 "safety_identifier",
1088 safety_identifier,
1089 old_safety_identifier,
1090 ),
1091 ("verbosity", verbosity, old_verbosity),
1092 ],
1093 );
1094 if functions != old_functions {
1095 set_or_remove_json(obj, "functions", functions.clone().map(Json::Array));
1096 }
1097 if modalities != old_modalities {
1098 set_or_remove_json(
1099 obj,
1100 "modalities",
1101 modalities.as_ref().map(|value| serde_json::json!(value)),
1102 );
1103 }
1104 if logprobs != old_logprobs {
1105 set_or_remove_json(obj, "logprobs", logprobs.map(Json::Bool));
1106 }
1107 if n != old_n {
1108 set_or_remove_json(obj, "n", n.map(Json::from));
1109 }
1110 if seed != old_seed {
1111 set_or_remove_json(obj, "seed", seed.map(Json::from));
1112 }
1113}
1114
1115impl LlmResponseCodec for OpenAIChatCodec {
1120 fn codec_identity(&self) -> LlmCodecIdentity {
1121 LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat)
1122 }
1123
1124 fn decode_response(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
1125 let raw: RawChatCompletion = serde_json::from_value(response.clone())
1126 .map_err(|e| FlowError::Internal(format!("OpenAI Chat response decode: {e}")))?;
1127
1128 let choice = raw.choices.as_ref().and_then(|c| c.first());
1130
1131 let message = choice
1133 .and_then(|c| c.message.as_ref())
1134 .and_then(|m| m.content.as_ref())
1135 .map(|s| super::request::MessageContent::Text(s.clone()));
1136
1137 let tool_calls = choice
1141 .and_then(|c| c.message.as_ref())
1142 .and_then(|m| m.tool_calls.as_ref())
1143 .map(|tcs| {
1144 tcs.iter()
1145 .filter_map(|tc| {
1146 let func = tc.function.as_ref()?;
1147 let name = func.name.as_ref()?;
1148 Some(ResponseToolCall {
1149 id: tc.id.clone().unwrap_or_default(),
1150 name: name.clone(),
1151 arguments: func
1152 .arguments
1153 .as_deref()
1154 .map(parse_arguments)
1155 .unwrap_or(Json::Object(Default::default())),
1156 })
1157 })
1158 .collect::<Vec<_>>()
1159 });
1160
1161 let finish_reason = choice
1163 .and_then(|c| c.finish_reason.as_deref())
1164 .map(map_chat_finish_reason);
1165
1166 let model_for_pricing = raw.model.as_deref();
1168 let model_provider = infer_model_provider("openai", model_for_pricing);
1169 let usage = raw.usage.map(|u| {
1170 let mut usage = Usage {
1171 prompt_tokens: u.prompt_tokens,
1172 completion_tokens: u.completion_tokens,
1173 total_tokens: u.total_tokens,
1174 cache_read_tokens: u.prompt_tokens_details.and_then(|d| d.cached_tokens),
1175 cache_write_tokens: None,
1176 cost: provider_reported_cost(u.provider_cost, u.cost),
1177 };
1178 if usage.cost.is_none() {
1179 usage.cost = model_for_pricing.and_then(|model| {
1180 estimate_cost_for_provider(model_provider.as_deref(), model, &usage)
1181 });
1182 }
1183 usage
1184 });
1185
1186 let logprobs = choice.and_then(|c| c.logprobs.clone());
1188 let api_specific = Some(ApiSpecificResponse::OpenAIChat {
1189 logprobs,
1190 system_fingerprint: raw.system_fingerprint,
1191 service_tier: raw.service_tier,
1192 });
1193
1194 Ok(AnnotatedLlmResponse {
1195 id: raw.id,
1196 model: raw.model,
1197 message,
1198 tool_calls,
1199 finish_reason,
1200 usage,
1201 optimization_summary: None,
1202 api_specific,
1203 extra: raw.extra,
1204 })
1205 }
1206}
1207
1208impl LlmCodec for OpenAIChatCodec {
1213 fn codec_identity(&self) -> LlmCodecIdentity {
1214 LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat)
1215 }
1216
1217 fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
1218 let obj = request
1219 .content
1220 .as_object()
1221 .ok_or_else(|| FlowError::Internal("request content is not an object".into()))?;
1222 let messages = obj
1223 .get("messages")
1224 .ok_or_else(|| {
1225 FlowError::InvalidArgument("OpenAI Chat request is missing messages".into())
1226 })?
1227 .as_array()
1228 .ok_or_else(|| {
1229 FlowError::InvalidArgument("OpenAI Chat messages must be an array".into())
1230 })?
1231 .iter()
1232 .map(decode_chat_message)
1233 .collect::<Result<Vec<_>>>()?;
1234 let model = super::optional_string(obj, "model", "OpenAI Chat")?;
1235 let temperature = super::optional_f64(obj, "temperature", "OpenAI Chat")?;
1236 let top_p = super::optional_f64(obj, "top_p", "OpenAI Chat")?;
1237 let stop = match obj.get("stop") {
1238 Some(Json::String(stop)) => Some(vec![stop.clone()]),
1239 Some(Json::Array(_)) => Some(
1240 serde_json::from_value::<Vec<String>>(obj["stop"].clone()).map_err(|error| {
1241 FlowError::InvalidArgument(format!("invalid OpenAI Chat stop value: {error}"))
1242 })?,
1243 ),
1244 Some(Json::Null) | None => None,
1245 Some(_) => {
1246 return Err(FlowError::InvalidArgument(
1247 "OpenAI Chat stop must be a string, array, or null".into(),
1248 ));
1249 }
1250 };
1251 let max_tokens = super::optional_u64(obj, "max_completion_tokens", "OpenAI Chat")?
1252 .or(super::optional_u64(obj, "max_tokens", "OpenAI Chat")?);
1253 let params =
1254 if temperature.is_some() || max_tokens.is_some() || top_p.is_some() || stop.is_some() {
1255 Some(GenerationParams {
1256 temperature,
1257 max_tokens,
1258 top_p,
1259 stop,
1260 })
1261 } else {
1262 None
1263 };
1264 let tools = obj
1265 .get("tools")
1266 .map(|value| {
1267 value
1268 .as_array()
1269 .ok_or_else(|| {
1270 FlowError::InvalidArgument("OpenAI Chat tools must be an array".into())
1271 })?
1272 .iter()
1273 .map(decode_chat_tool)
1274 .collect::<Result<Vec<_>>>()
1275 })
1276 .transpose()?;
1277 let tool_choice = obj.get("tool_choice").map(decode_chat_tool_choice);
1278 let store = super::optional_bool(obj, "store", "OpenAI Chat")?;
1279 let user = super::optional_string(obj, "user", "OpenAI Chat")?;
1280 let service_tier = super::optional_string(obj, "service_tier", "OpenAI Chat")?;
1281 let parallel_tool_calls = super::optional_bool(obj, "parallel_tool_calls", "OpenAI Chat")?;
1282 let top_logprobs = super::optional_u64(obj, "top_logprobs", "OpenAI Chat")?;
1283 let stream = super::optional_bool(obj, "stream", "OpenAI Chat")?;
1284 let frequency_penalty = super::optional_f64(obj, "frequency_penalty", "OpenAI Chat")?;
1285 let functions = match obj.get("functions") {
1286 Some(Json::Null) | None => None,
1287 Some(Json::Array(functions)) => Some(functions.clone()),
1288 Some(_) => {
1289 return Err(FlowError::InvalidArgument(
1290 "OpenAI Chat functions must be an array or null".into(),
1291 ));
1292 }
1293 };
1294 let logprobs = super::optional_bool(obj, "logprobs", "OpenAI Chat")?;
1295 let modalities = match obj.get("modalities") {
1296 Some(Json::Null) | None => None,
1297 Some(value) => Some(serde_json::from_value(value.clone()).map_err(|error| {
1298 FlowError::InvalidArgument(format!("invalid OpenAI Chat modalities: {error}"))
1299 })?),
1300 };
1301 let n = super::optional_u64(obj, "n", "OpenAI Chat")?;
1302 let presence_penalty = super::optional_f64(obj, "presence_penalty", "OpenAI Chat")?;
1303 let prompt_cache_key = super::optional_string(obj, "prompt_cache_key", "OpenAI Chat")?;
1304 let prompt_cache_retention =
1305 super::optional_string(obj, "prompt_cache_retention", "OpenAI Chat")?;
1306 let reasoning_effort = super::optional_string(obj, "reasoning_effort", "OpenAI Chat")?;
1307 let safety_identifier = super::optional_string(obj, "safety_identifier", "OpenAI Chat")?;
1308 let seed = super::optional_i64(obj, "seed", "OpenAI Chat")?;
1309 let verbosity = super::optional_string(obj, "verbosity", "OpenAI Chat")?;
1310 let metadata = super::optional_object(obj, "metadata", "OpenAI Chat")?;
1311 let audio = super::optional_object(obj, "audio", "OpenAI Chat")?;
1312 let logit_bias = super::optional_object(obj, "logit_bias", "OpenAI Chat")?;
1313 let moderation = super::optional_object(obj, "moderation", "OpenAI Chat")?;
1314 let prediction = super::optional_object(obj, "prediction", "OpenAI Chat")?;
1315 let prompt_cache_options =
1316 super::optional_object(obj, "prompt_cache_options", "OpenAI Chat")?;
1317 let response_format = super::optional_object(obj, "response_format", "OpenAI Chat")?;
1318 let stream_options = super::optional_object(obj, "stream_options", "OpenAI Chat")?;
1319 let web_search_options = super::optional_object(obj, "web_search_options", "OpenAI Chat")?;
1320 let extra: serde_json::Map<String, Json> = obj
1321 .iter()
1322 .filter(|(k, _)| !MODELED_REQUEST_KEYS.contains(&k.as_str()))
1323 .map(|(k, v)| (k.clone(), v.clone()))
1324 .collect();
1325
1326 Ok(AnnotatedLlmRequest {
1327 messages,
1328 instructions: None,
1329 model,
1330 params,
1331 tools,
1332 tool_choice,
1333 store,
1334 previous_response_id: None,
1335 truncation: None,
1336 reasoning: None,
1337 include: None,
1338 user,
1339 metadata,
1340 service_tier,
1341 parallel_tool_calls,
1342 max_output_tokens: None,
1343 max_tool_calls: None,
1344 top_logprobs,
1345 stream,
1346 api_specific: Some(ApiSpecificRequest::OpenAIChat {
1347 audio,
1348 frequency_penalty,
1349 function_call: obj.get("function_call").cloned(),
1350 functions,
1351 logit_bias,
1352 logprobs,
1353 modalities,
1354 moderation,
1355 n,
1356 prediction,
1357 presence_penalty,
1358 prompt_cache_key,
1359 prompt_cache_options,
1360 prompt_cache_retention,
1361 reasoning_effort,
1362 response_format,
1363 safety_identifier,
1364 seed,
1365 stream_options,
1366 verbosity,
1367 web_search_options,
1368 }),
1369 extra,
1370 })
1371 }
1372
1373 fn encode(&self, annotated: &AnnotatedLlmRequest, original: &LlmRequest) -> Result<LlmRequest> {
1374 let baseline = self.decode(original)?;
1375 let mut content = original.content.clone();
1376 let obj = content
1377 .as_object_mut()
1378 .ok_or_else(|| FlowError::Internal("original content is not an object".into()))?;
1379 patch_chat_messages_and_validate(obj, annotated, &baseline)?;
1380 patch_chat_model_and_params(obj, annotated, &baseline);
1381 patch_chat_tools(obj, annotated, &baseline)?;
1382 patch_chat_common_fields(obj, annotated, &baseline);
1383 patch_chat_api_specific(obj, &annotated.api_specific, &baseline.api_specific)?;
1384 patch_extra_fields(obj, &baseline.extra, &annotated.extra);
1385
1386 Ok(LlmRequest {
1387 headers: original.headers.clone(),
1388 content,
1389 })
1390 }
1391}
1392
1393fn json_f64(v: f64) -> Json {
1395 serde_json::Number::from_f64(v)
1396 .map(Json::Number)
1397 .unwrap_or(Json::Null)
1398}
1399
1400pub struct OpenAIChatStreamingCodec {
1430 state: std::sync::Arc<std::sync::Mutex<OpenAIChatStreamingState>>,
1431}
1432
1433impl OpenAIChatStreamingCodec {
1434 pub fn new() -> Self {
1436 Self {
1437 state: std::sync::Arc::new(std::sync::Mutex::new(OpenAIChatStreamingState::default())),
1438 }
1439 }
1440}
1441
1442impl Default for OpenAIChatStreamingCodec {
1443 fn default() -> Self {
1444 Self::new()
1445 }
1446}
1447
1448impl super::streaming::StreamingCodec for OpenAIChatStreamingCodec {
1449 fn collector(&self) -> crate::api::runtime::LlmCollectorFn {
1450 let state = std::sync::Arc::clone(&self.state);
1451 Box::new(move |event: Json| -> Result<()> {
1452 let mut guard = state
1453 .lock()
1454 .unwrap_or_else(|poisoned| poisoned.into_inner());
1455 guard.observe(&event);
1456 Ok(())
1457 })
1458 }
1459
1460 fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn {
1461 let state = std::sync::Arc::clone(&self.state);
1462 Box::new(move || -> Json {
1463 let mut guard = state
1464 .lock()
1465 .unwrap_or_else(|poisoned| poisoned.into_inner());
1466 std::mem::take(&mut *guard).finalize()
1467 })
1468 }
1469}
1470
1471#[derive(Debug, Default)]
1472struct OpenAIChatStreamingState {
1473 id: Option<String>,
1474 object: Option<String>,
1475 created: Option<u64>,
1476 model: Option<String>,
1477 choices: std::collections::BTreeMap<u64, ChoiceState>,
1480 usage: Option<Json>,
1482}
1483
1484#[derive(Debug, Default)]
1485struct ChoiceState {
1486 role: Option<String>,
1487 content: String,
1488 has_content: bool,
1489 tool_calls: std::collections::BTreeMap<u64, ToolCallState>,
1492 finish_reason: Option<String>,
1493}
1494
1495#[derive(Debug, Default)]
1496struct ToolCallState {
1497 id: Option<String>,
1498 type_: Option<String>,
1499 name: Option<String>,
1500 arguments: String,
1501}
1502
1503impl OpenAIChatStreamingState {
1504 fn observe(&mut self, chunk: &Json) {
1505 if self.id.is_none()
1508 && let Some(id) = chunk.get("id").and_then(Json::as_str)
1509 {
1510 self.id = Some(id.to_string());
1511 }
1512 if self.object.is_none()
1513 && let Some(obj) = chunk.get("object").and_then(Json::as_str)
1514 {
1515 self.object = Some(obj.to_string());
1516 }
1517 if self.created.is_none()
1518 && let Some(c) = chunk.get("created").and_then(Json::as_u64)
1519 {
1520 self.created = Some(c);
1521 }
1522 if self.model.is_none()
1523 && let Some(m) = chunk.get("model").and_then(Json::as_str)
1524 {
1525 self.model = Some(m.to_string());
1526 }
1527 if let Some(usage) = chunk.get("usage") {
1528 if !usage.is_null() {
1531 self.usage = Some(usage.clone());
1532 }
1533 }
1534 let Some(choices) = chunk.get("choices").and_then(Json::as_array) else {
1535 return;
1536 };
1537 for choice in choices {
1538 self.observe_choice(choice);
1539 }
1540 }
1541
1542 fn observe_choice(&mut self, choice: &Json) {
1543 let index = choice.get("index").and_then(Json::as_u64).unwrap_or(0);
1544 let entry = self.choices.entry(index).or_default();
1545 entry.observe_finish_reason(choice);
1546 entry.observe_delta(choice.get("delta"));
1547 }
1548
1549 fn finalize(self) -> Json {
1550 let mut output = serde_json::Map::new();
1551 if let Some(id) = self.id {
1552 output.insert("id".to_string(), Json::String(id));
1553 }
1554 if let Some(object) = self.object {
1559 let normalized = object
1560 .strip_suffix(".chunk")
1561 .map(str::to_string)
1562 .unwrap_or(object);
1563 output.insert("object".to_string(), Json::String(normalized));
1564 }
1565 if let Some(created) = self.created {
1566 output.insert("created".to_string(), Json::Number(created.into()));
1567 }
1568 if let Some(model) = self.model {
1569 output.insert("model".to_string(), Json::String(model));
1570 }
1571 let choices: Vec<Json> = self
1572 .choices
1573 .into_iter()
1574 .map(|(index, choice)| choice.finalize(index))
1575 .collect();
1576 output.insert("choices".to_string(), Json::Array(choices));
1577 if let Some(usage) = self.usage {
1578 output.insert("usage".to_string(), usage);
1579 }
1580 Json::Object(output)
1581 }
1582}
1583
1584impl ChoiceState {
1585 fn observe_finish_reason(&mut self, choice: &Json) {
1586 if let Some(reason) = choice.get("finish_reason").and_then(Json::as_str) {
1587 self.finish_reason = Some(reason.to_string());
1588 }
1589 }
1590
1591 fn observe_delta(&mut self, delta: Option<&Json>) {
1592 let Some(delta) = delta else {
1593 return;
1594 };
1595 if let Some(role) = delta.get("role").and_then(Json::as_str) {
1596 self.role = Some(role.to_string());
1597 }
1598 if let Some(content) = delta.get("content").and_then(Json::as_str) {
1599 self.content.push_str(content);
1600 self.has_content = true;
1601 }
1602 self.observe_tool_calls(delta);
1603 }
1604
1605 fn observe_tool_calls(&mut self, delta: &Json) {
1606 if let Some(tool_calls) = delta.get("tool_calls").and_then(Json::as_array) {
1607 for tool_call in tool_calls {
1608 self.observe_tool_call(tool_call);
1609 }
1610 }
1611 }
1612
1613 fn observe_tool_call(&mut self, tool_call: &Json) {
1614 let index = tool_call.get("index").and_then(Json::as_u64).unwrap_or(0);
1615 let state = self.tool_calls.entry(index).or_default();
1616 if let Some(id) = tool_call.get("id").and_then(Json::as_str) {
1617 state.id = Some(id.to_string());
1618 }
1619 if let Some(type_) = tool_call.get("type").and_then(Json::as_str) {
1620 state.type_ = Some(type_.to_string());
1621 }
1622 if let Some(function) = tool_call.get("function") {
1623 state.observe_function(function);
1624 }
1625 }
1626
1627 fn finalize(self, index: u64) -> Json {
1628 let mut message = serde_json::Map::new();
1629 message.insert(
1630 "role".to_string(),
1631 Json::String(self.role.unwrap_or_else(|| "assistant".to_string())),
1632 );
1633 if self.has_content {
1637 message.insert("content".to_string(), Json::String(self.content));
1638 } else {
1639 message.insert("content".to_string(), Json::Null);
1640 }
1641 if !self.tool_calls.is_empty() {
1642 let tool_calls: Vec<Json> = self
1643 .tool_calls
1644 .into_values()
1645 .map(ToolCallState::finalize)
1646 .collect();
1647 message.insert("tool_calls".to_string(), Json::Array(tool_calls));
1648 }
1649 let mut choice = serde_json::Map::new();
1650 choice.insert("index".to_string(), Json::Number(index.into()));
1651 choice.insert("message".to_string(), Json::Object(message));
1652 if let Some(reason) = self.finish_reason {
1653 choice.insert("finish_reason".to_string(), Json::String(reason));
1654 } else {
1655 choice.insert("finish_reason".to_string(), Json::Null);
1656 }
1657 Json::Object(choice)
1658 }
1659}
1660
1661impl ToolCallState {
1662 fn observe_function(&mut self, function: &Json) {
1663 if let Some(name) = function.get("name").and_then(Json::as_str) {
1664 self.name = Some(name.to_string());
1665 }
1666 if let Some(args) = function.get("arguments").and_then(Json::as_str) {
1667 self.arguments.push_str(args);
1668 }
1669 }
1670
1671 fn finalize(self) -> Json {
1672 let mut function = serde_json::Map::new();
1673 function.insert(
1674 "name".to_string(),
1675 Json::String(self.name.unwrap_or_default()),
1676 );
1677 function.insert("arguments".to_string(), Json::String(self.arguments));
1678 let mut call = serde_json::Map::new();
1679 if let Some(id) = self.id {
1680 call.insert("id".to_string(), Json::String(id));
1681 }
1682 call.insert(
1683 "type".to_string(),
1684 Json::String(self.type_.unwrap_or_else(|| "function".to_string())),
1685 );
1686 call.insert("function".to_string(), Json::Object(function));
1687 Json::Object(call)
1688 }
1689}
1690
1691#[cfg(test)]
1696#[path = "../../tests/unit/codec/openai_chat_tests.rs"]
1697mod tests;