1use anyhow::{Result, anyhow};
2use collections::HashMap;
3use futures::{Stream, StreamExt};
4use language_model_core::{
5 LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
6 LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
7 LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
8 Role, StopReason, TokenUsage,
9 util::{fix_streamed_json, parse_tool_arguments},
10};
11use std::pin::Pin;
12use std::sync::Arc;
13
14use crate::responses::{
15 Request as ResponseRequest, ResponseFunctionCallItem, ResponseFunctionCallOutputContent,
16 ResponseFunctionCallOutputItem, ResponseInputContent, ResponseInputItem, ResponseMessageItem,
17 ResponseOutputItem, ResponseSummary as ResponsesSummary, ResponseUsage as ResponsesUsage,
18 StreamEvent as ResponsesStreamEvent,
19};
20use crate::{
21 FunctionContent, FunctionDefinition, ImageUrl, MessagePart, ReasoningEffort,
22 ResponseStreamEvent, ToolCall, ToolCallContent,
23};
24
25pub fn into_open_ai(
26 request: LanguageModelRequest,
27 model_id: &str,
28 supports_parallel_tool_calls: bool,
29 supports_prompt_cache_key: bool,
30 max_output_tokens: Option<u64>,
31 reasoning_effort: Option<ReasoningEffort>,
32 interleaved_reasoning: bool,
33) -> crate::Request {
34 let stream = !model_id.starts_with("o1-");
35
36 let mut messages = Vec::new();
37 let mut current_reasoning: Option<String> = None;
38 for message in request.messages {
39 for content in message.content {
40 match content {
41 MessageContent::Thinking { text, .. } if interleaved_reasoning => {
42 current_reasoning.get_or_insert_default().push_str(&text);
43 }
44 MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
45 let should_add = if message.role == Role::User {
46 // Including whitespace-only user messages can cause error with OpenAI compatible APIs
47 // See https://github.com/zed-industries/zed/issues/40097
48 !text.trim().is_empty()
49 } else {
50 !text.is_empty()
51 };
52 if should_add {
53 add_message_content_part(
54 MessagePart::Text { text },
55 message.role,
56 &mut messages,
57 );
58 if let Some(reasoning) = current_reasoning.take() {
59 if let Some(crate::RequestMessage::Assistant {
60 reasoning_content,
61 ..
62 }) = messages.last_mut()
63 {
64 *reasoning_content = Some(reasoning);
65 }
66 }
67 }
68 }
69 MessageContent::RedactedThinking(_) => {}
70 MessageContent::Image(image) => {
71 add_message_content_part(
72 MessagePart::Image {
73 image_url: ImageUrl {
74 url: image.to_base64_url(),
75 detail: None,
76 },
77 },
78 message.role,
79 &mut messages,
80 );
81 }
82 MessageContent::ToolUse(tool_use) => {
83 let tool_call = ToolCall {
84 id: tool_use.id.to_string(),
85 content: ToolCallContent::Function {
86 function: FunctionContent {
87 name: tool_use.name.to_string(),
88 arguments: serde_json::to_string(&tool_use.input)
89 .unwrap_or_default(),
90 },
91 },
92 };
93
94 if let Some(crate::RequestMessage::Assistant { tool_calls, .. }) =
95 messages.last_mut()
96 {
97 tool_calls.push(tool_call);
98 } else {
99 messages.push(crate::RequestMessage::Assistant {
100 content: None,
101 tool_calls: vec![tool_call],
102 reasoning_content: current_reasoning.take(),
103 });
104 }
105 }
106 MessageContent::ToolResult(tool_result) => {
107 let content: Vec<MessagePart> = tool_result
108 .content
109 .iter()
110 .map(|part| match part {
111 LanguageModelToolResultContent::Text(text) => MessagePart::Text {
112 text: text.to_string(),
113 },
114 LanguageModelToolResultContent::Image(image) => MessagePart::Image {
115 image_url: ImageUrl {
116 url: image.to_base64_url(),
117 detail: None,
118 },
119 },
120 })
121 .collect();
122
123 messages.push(crate::RequestMessage::Tool {
124 content: content.into(),
125 tool_call_id: tool_result.tool_use_id.to_string(),
126 });
127 }
128 }
129 }
130 }
131
132 crate::Request {
133 model: model_id.into(),
134 messages,
135 stream,
136 stream_options: if stream {
137 Some(crate::StreamOptions::default())
138 } else {
139 None
140 },
141 stop: request.stop,
142 temperature: request.temperature.or(Some(1.0)),
143 max_completion_tokens: max_output_tokens,
144 parallel_tool_calls: if supports_parallel_tool_calls && !request.tools.is_empty() {
145 Some(supports_parallel_tool_calls)
146 } else {
147 None
148 },
149 prompt_cache_key: if supports_prompt_cache_key {
150 request.thread_id
151 } else {
152 None
153 },
154 tools: request
155 .tools
156 .into_iter()
157 .map(|tool| crate::ToolDefinition::Function {
158 function: FunctionDefinition {
159 name: tool.name,
160 description: Some(tool.description),
161 parameters: Some(tool.input_schema),
162 },
163 })
164 .collect(),
165 tool_choice: request.tool_choice.map(|choice| match choice {
166 LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
167 LanguageModelToolChoice::Any => crate::ToolChoice::Required,
168 LanguageModelToolChoice::None => crate::ToolChoice::None,
169 }),
170 reasoning_effort,
171 }
172}
173
174pub fn into_open_ai_response(
175 request: LanguageModelRequest,
176 model_id: &str,
177 supports_parallel_tool_calls: bool,
178 supports_prompt_cache_key: bool,
179 max_output_tokens: Option<u64>,
180 reasoning_effort: Option<ReasoningEffort>,
181) -> ResponseRequest {
182 let stream = !model_id.starts_with("o1-");
183
184 let LanguageModelRequest {
185 thread_id,
186 prompt_id: _,
187 intent: _,
188 messages,
189 tools,
190 tool_choice,
191 stop: _,
192 temperature,
193 thinking_allowed: _,
194 thinking_effort: _,
195 speed: _,
196 } = request;
197
198 let mut input_items = Vec::new();
199 for (index, message) in messages.into_iter().enumerate() {
200 append_message_to_response_items(message, index, &mut input_items);
201 }
202
203 let tools: Vec<_> = tools
204 .into_iter()
205 .map(|tool| crate::responses::ToolDefinition::Function {
206 name: tool.name,
207 description: Some(tool.description),
208 parameters: Some(tool.input_schema),
209 strict: None,
210 })
211 .collect();
212
213 ResponseRequest {
214 model: model_id.into(),
215 input: input_items,
216 stream,
217 temperature,
218 top_p: None,
219 max_output_tokens,
220 parallel_tool_calls: if tools.is_empty() {
221 None
222 } else {
223 Some(supports_parallel_tool_calls)
224 },
225 tool_choice: tool_choice.map(|choice| match choice {
226 LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
227 LanguageModelToolChoice::Any => crate::ToolChoice::Required,
228 LanguageModelToolChoice::None => crate::ToolChoice::None,
229 }),
230 tools,
231 prompt_cache_key: if supports_prompt_cache_key {
232 thread_id
233 } else {
234 None
235 },
236 reasoning: reasoning_effort.map(|effort| crate::responses::ReasoningConfig {
237 effort,
238 summary: Some(crate::responses::ReasoningSummaryMode::Auto),
239 }),
240 }
241}
242
243fn append_message_to_response_items(
244 message: LanguageModelRequestMessage,
245 index: usize,
246 input_items: &mut Vec<ResponseInputItem>,
247) {
248 let mut content_parts: Vec<ResponseInputContent> = Vec::new();
249
250 for content in message.content {
251 match content {
252 MessageContent::Text(text) => {
253 push_response_text_part(&message.role, text, &mut content_parts);
254 }
255 MessageContent::Thinking { text, .. } => {
256 push_response_text_part(&message.role, text, &mut content_parts);
257 }
258 MessageContent::RedactedThinking(_) => {}
259 MessageContent::Image(image) => {
260 push_response_image_part(&message.role, image, &mut content_parts);
261 }
262 MessageContent::ToolUse(tool_use) => {
263 flush_response_parts(&message.role, index, &mut content_parts, input_items);
264 let call_id = tool_use.id.to_string();
265 input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem {
266 call_id,
267 name: tool_use.name.to_string(),
268 arguments: tool_use.raw_input,
269 }));
270 }
271 MessageContent::ToolResult(tool_result) => {
272 flush_response_parts(&message.role, index, &mut content_parts, input_items);
273 let output = match tool_result.content.as_slice() {
274 [LanguageModelToolResultContent::Text(text)] => {
275 ResponseFunctionCallOutputContent::Text(text.to_string())
276 }
277 _ => {
278 let parts = tool_result
279 .content
280 .into_iter()
281 .map(|part| match part {
282 LanguageModelToolResultContent::Text(text) => {
283 ResponseInputContent::Text {
284 text: text.to_string(),
285 }
286 }
287 LanguageModelToolResultContent::Image(image) => {
288 ResponseInputContent::Image {
289 image_url: image.to_base64_url(),
290 }
291 }
292 })
293 .collect();
294 ResponseFunctionCallOutputContent::List(parts)
295 }
296 };
297 input_items.push(ResponseInputItem::FunctionCallOutput(
298 ResponseFunctionCallOutputItem {
299 call_id: tool_result.tool_use_id.to_string(),
300 output,
301 },
302 ));
303 }
304 }
305 }
306
307 flush_response_parts(&message.role, index, &mut content_parts, input_items);
308}
309
310fn push_response_text_part(
311 role: &Role,
312 text: impl Into<String>,
313 parts: &mut Vec<ResponseInputContent>,
314) {
315 let text = text.into();
316 if text.trim().is_empty() {
317 return;
318 }
319
320 match role {
321 Role::Assistant => parts.push(ResponseInputContent::OutputText {
322 text,
323 annotations: Vec::new(),
324 }),
325 _ => parts.push(ResponseInputContent::Text { text }),
326 }
327}
328
329fn push_response_image_part(
330 role: &Role,
331 image: LanguageModelImage,
332 parts: &mut Vec<ResponseInputContent>,
333) {
334 match role {
335 Role::Assistant => parts.push(ResponseInputContent::OutputText {
336 text: "[image omitted]".to_string(),
337 annotations: Vec::new(),
338 }),
339 _ => parts.push(ResponseInputContent::Image {
340 image_url: image.to_base64_url(),
341 }),
342 }
343}
344
345fn flush_response_parts(
346 role: &Role,
347 _index: usize,
348 parts: &mut Vec<ResponseInputContent>,
349 input_items: &mut Vec<ResponseInputItem>,
350) {
351 if parts.is_empty() {
352 return;
353 }
354
355 let item = ResponseInputItem::Message(ResponseMessageItem {
356 role: match role {
357 Role::User => crate::Role::User,
358 Role::Assistant => crate::Role::Assistant,
359 Role::System => crate::Role::System,
360 },
361 content: parts.clone(),
362 });
363
364 input_items.push(item);
365 parts.clear();
366}
367
368fn add_message_content_part(
369 new_part: MessagePart,
370 role: Role,
371 messages: &mut Vec<crate::RequestMessage>,
372) {
373 match (role, messages.last_mut()) {
374 (Role::User, Some(crate::RequestMessage::User { content }))
375 | (
376 Role::Assistant,
377 Some(crate::RequestMessage::Assistant {
378 content: Some(content),
379 ..
380 }),
381 )
382 | (Role::System, Some(crate::RequestMessage::System { content, .. })) => {
383 content.push_part(new_part);
384 }
385 _ => {
386 messages.push(match role {
387 Role::User => crate::RequestMessage::User {
388 content: crate::MessageContent::from(vec![new_part]),
389 },
390 Role::Assistant => crate::RequestMessage::Assistant {
391 content: Some(crate::MessageContent::from(vec![new_part])),
392 tool_calls: Vec::new(),
393 reasoning_content: None,
394 },
395 Role::System => crate::RequestMessage::System {
396 content: crate::MessageContent::from(vec![new_part]),
397 },
398 });
399 }
400 }
401}
402
403pub struct OpenAiEventMapper {
404 tool_calls_by_index: HashMap<usize, RawToolCall>,
405}
406
407impl OpenAiEventMapper {
408 pub fn new() -> Self {
409 Self {
410 tool_calls_by_index: HashMap::default(),
411 }
412 }
413
414 pub fn map_stream(
415 mut self,
416 events: Pin<Box<dyn Send + Stream<Item = Result<ResponseStreamEvent>>>>,
417 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
418 {
419 events.flat_map(move |event| {
420 futures::stream::iter(match event {
421 Ok(event) => self.map_event(event),
422 Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
423 })
424 })
425 }
426
427 pub fn map_event(
428 &mut self,
429 event: ResponseStreamEvent,
430 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
431 let mut events = Vec::new();
432 if let Some(usage) = event.usage {
433 events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
434 input_tokens: usage.prompt_tokens,
435 output_tokens: usage.completion_tokens,
436 cache_creation_input_tokens: 0,
437 cache_read_input_tokens: 0,
438 })));
439 }
440
441 let Some(choice) = event.choices.first() else {
442 return events;
443 };
444
445 if let Some(delta) = choice.delta.as_ref() {
446 if let Some(reasoning_content) = delta.reasoning_content.clone() {
447 if !reasoning_content.is_empty() {
448 events.push(Ok(LanguageModelCompletionEvent::Thinking {
449 text: reasoning_content,
450 signature: None,
451 }));
452 }
453 }
454 if let Some(content) = delta.content.clone() {
455 if !content.is_empty() {
456 events.push(Ok(LanguageModelCompletionEvent::Text(content)));
457 }
458 }
459
460 if let Some(tool_calls) = delta.tool_calls.as_ref() {
461 for tool_call in tool_calls {
462 let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
463
464 if let Some(tool_id) = tool_call.id.clone() {
465 entry.id = tool_id;
466 }
467
468 if let Some(function) = tool_call.function.as_ref() {
469 if let Some(name) = function.name.clone() {
470 entry.name = name;
471 }
472
473 if let Some(arguments) = function.arguments.clone() {
474 entry.arguments.push_str(&arguments);
475 }
476 }
477
478 if !entry.id.is_empty() && !entry.name.is_empty() {
479 if let Ok(input) = serde_json::from_str::<serde_json::Value>(
480 &fix_streamed_json(&entry.arguments),
481 ) {
482 events.push(Ok(LanguageModelCompletionEvent::ToolUse(
483 LanguageModelToolUse {
484 id: entry.id.clone().into(),
485 name: entry.name.as_str().into(),
486 is_input_complete: false,
487 input,
488 raw_input: entry.arguments.clone(),
489 thought_signature: None,
490 },
491 )));
492 }
493 }
494 }
495 }
496 }
497
498 match choice.finish_reason.as_deref() {
499 Some("stop") => {
500 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
501 }
502 Some("tool_calls") => {
503 events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
504 match parse_tool_arguments(&tool_call.arguments) {
505 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
506 LanguageModelToolUse {
507 id: tool_call.id.clone().into(),
508 name: tool_call.name.as_str().into(),
509 is_input_complete: true,
510 input,
511 raw_input: tool_call.arguments.clone(),
512 thought_signature: None,
513 },
514 )),
515 Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
516 id: tool_call.id.into(),
517 tool_name: tool_call.name.into(),
518 raw_input: tool_call.arguments.clone().into(),
519 json_parse_error: error.to_string(),
520 }),
521 }
522 }));
523
524 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
525 }
526 Some(stop_reason) => {
527 log::error!("Unexpected OpenAI stop_reason: {stop_reason:?}",);
528 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
529 }
530 None => {}
531 }
532
533 events
534 }
535}
536
537#[derive(Default)]
538struct RawToolCall {
539 id: String,
540 name: String,
541 arguments: String,
542}
543
544pub struct OpenAiResponseEventMapper {
545 function_calls_by_item: HashMap<String, PendingResponseFunctionCall>,
546 pending_stop_reason: Option<StopReason>,
547}
548
549#[derive(Default)]
550struct PendingResponseFunctionCall {
551 call_id: String,
552 name: Arc<str>,
553 arguments: String,
554}
555
556impl OpenAiResponseEventMapper {
557 pub fn new() -> Self {
558 Self {
559 function_calls_by_item: HashMap::default(),
560 pending_stop_reason: None,
561 }
562 }
563
564 pub fn map_stream(
565 mut self,
566 events: Pin<Box<dyn Send + Stream<Item = Result<ResponsesStreamEvent>>>>,
567 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
568 {
569 events.flat_map(move |event| {
570 futures::stream::iter(match event {
571 Ok(event) => self.map_event(event),
572 Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
573 })
574 })
575 }
576
577 pub fn map_event(
578 &mut self,
579 event: ResponsesStreamEvent,
580 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
581 match event {
582 ResponsesStreamEvent::OutputItemAdded { item, .. } => {
583 let mut events = Vec::new();
584
585 match &item {
586 ResponseOutputItem::Message(message) => {
587 if let Some(id) = &message.id {
588 events.push(Ok(LanguageModelCompletionEvent::StartMessage {
589 message_id: id.clone(),
590 }));
591 }
592 }
593 ResponseOutputItem::FunctionCall(function_call) => {
594 if let Some(item_id) = function_call.id.clone() {
595 let call_id = function_call
596 .call_id
597 .clone()
598 .or_else(|| function_call.id.clone())
599 .unwrap_or_else(|| item_id.clone());
600 let entry = PendingResponseFunctionCall {
601 call_id,
602 name: Arc::<str>::from(
603 function_call.name.clone().unwrap_or_default(),
604 ),
605 arguments: function_call.arguments.clone(),
606 };
607 self.function_calls_by_item.insert(item_id, entry);
608 }
609 }
610 ResponseOutputItem::Reasoning(_) | ResponseOutputItem::Unknown => {}
611 }
612 events
613 }
614 ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. } => {
615 if delta.is_empty() {
616 Vec::new()
617 } else {
618 vec![Ok(LanguageModelCompletionEvent::Thinking {
619 text: delta,
620 signature: None,
621 })]
622 }
623 }
624 ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
625 if delta.is_empty() {
626 Vec::new()
627 } else {
628 vec![Ok(LanguageModelCompletionEvent::Text(delta))]
629 }
630 }
631 ResponsesStreamEvent::FunctionCallArgumentsDelta { item_id, delta, .. } => {
632 if let Some(entry) = self.function_calls_by_item.get_mut(&item_id) {
633 entry.arguments.push_str(&delta);
634 if let Ok(input) = serde_json::from_str::<serde_json::Value>(
635 &fix_streamed_json(&entry.arguments),
636 ) {
637 return vec![Ok(LanguageModelCompletionEvent::ToolUse(
638 LanguageModelToolUse {
639 id: LanguageModelToolUseId::from(entry.call_id.clone()),
640 name: entry.name.clone(),
641 is_input_complete: false,
642 input,
643 raw_input: entry.arguments.clone(),
644 thought_signature: None,
645 },
646 ))];
647 }
648 }
649 Vec::new()
650 }
651 ResponsesStreamEvent::FunctionCallArgumentsDone {
652 item_id, arguments, ..
653 } => {
654 if let Some(mut entry) = self.function_calls_by_item.remove(&item_id) {
655 if !arguments.is_empty() {
656 entry.arguments = arguments;
657 }
658 let raw_input = entry.arguments.clone();
659 self.pending_stop_reason = Some(StopReason::ToolUse);
660 match parse_tool_arguments(&entry.arguments) {
661 Ok(input) => {
662 vec![Ok(LanguageModelCompletionEvent::ToolUse(
663 LanguageModelToolUse {
664 id: LanguageModelToolUseId::from(entry.call_id.clone()),
665 name: entry.name.clone(),
666 is_input_complete: true,
667 input,
668 raw_input,
669 thought_signature: None,
670 },
671 ))]
672 }
673 Err(error) => {
674 vec![Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
675 id: LanguageModelToolUseId::from(entry.call_id.clone()),
676 tool_name: entry.name.clone(),
677 raw_input: Arc::<str>::from(raw_input),
678 json_parse_error: error.to_string(),
679 })]
680 }
681 }
682 } else {
683 Vec::new()
684 }
685 }
686 ResponsesStreamEvent::Completed { response } => {
687 self.handle_completion(response, StopReason::EndTurn)
688 }
689 ResponsesStreamEvent::Incomplete { response } => {
690 let reason = response
691 .status_details
692 .as_ref()
693 .and_then(|details| details.reason.as_deref());
694 let stop_reason = match reason {
695 Some("max_output_tokens") => StopReason::MaxTokens,
696 Some("content_filter") => {
697 self.pending_stop_reason = Some(StopReason::Refusal);
698 StopReason::Refusal
699 }
700 _ => self
701 .pending_stop_reason
702 .take()
703 .unwrap_or(StopReason::EndTurn),
704 };
705
706 let mut events = Vec::new();
707 if self.pending_stop_reason.is_none() {
708 events.extend(self.emit_tool_calls_from_output(&response.output));
709 }
710 if let Some(usage) = response.usage.as_ref() {
711 events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
712 token_usage_from_response_usage(usage),
713 )));
714 }
715 events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
716 events
717 }
718 ResponsesStreamEvent::Failed { response } => {
719 let message = response
720 .status_details
721 .and_then(|details| details.error)
722 .map(|error| error.to_string())
723 .unwrap_or_else(|| "response failed".to_string());
724 vec![Err(LanguageModelCompletionError::Other(anyhow!(message)))]
725 }
726 ResponsesStreamEvent::Error { error }
727 | ResponsesStreamEvent::GenericError { error } => {
728 vec![Err(LanguageModelCompletionError::Other(anyhow!(
729 error.message
730 )))]
731 }
732 ResponsesStreamEvent::ReasoningSummaryPartAdded { summary_index, .. } => {
733 if summary_index > 0 {
734 vec![Ok(LanguageModelCompletionEvent::Thinking {
735 text: "\n\n".to_string(),
736 signature: None,
737 })]
738 } else {
739 Vec::new()
740 }
741 }
742 ResponsesStreamEvent::OutputTextDone { .. }
743 | ResponsesStreamEvent::OutputItemDone { .. }
744 | ResponsesStreamEvent::ContentPartAdded { .. }
745 | ResponsesStreamEvent::ContentPartDone { .. }
746 | ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
747 | ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
748 | ResponsesStreamEvent::Created { .. }
749 | ResponsesStreamEvent::InProgress { .. }
750 | ResponsesStreamEvent::Unknown => Vec::new(),
751 }
752 }
753
754 fn handle_completion(
755 &mut self,
756 response: ResponsesSummary,
757 default_reason: StopReason,
758 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
759 let mut events = Vec::new();
760
761 if self.pending_stop_reason.is_none() {
762 events.extend(self.emit_tool_calls_from_output(&response.output));
763 }
764
765 if let Some(usage) = response.usage.as_ref() {
766 events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(
767 token_usage_from_response_usage(usage),
768 )));
769 }
770
771 let stop_reason = self.pending_stop_reason.take().unwrap_or(default_reason);
772 events.push(Ok(LanguageModelCompletionEvent::Stop(stop_reason)));
773 events
774 }
775
776 fn emit_tool_calls_from_output(
777 &mut self,
778 output: &[ResponseOutputItem],
779 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
780 let mut events = Vec::new();
781 for item in output {
782 if let ResponseOutputItem::FunctionCall(function_call) = item {
783 let Some(call_id) = function_call
784 .call_id
785 .clone()
786 .or_else(|| function_call.id.clone())
787 else {
788 log::error!(
789 "Function call item missing both call_id and id: {:?}",
790 function_call
791 );
792 continue;
793 };
794 let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
795 let arguments = &function_call.arguments;
796 self.pending_stop_reason = Some(StopReason::ToolUse);
797 match parse_tool_arguments(arguments) {
798 Ok(input) => {
799 events.push(Ok(LanguageModelCompletionEvent::ToolUse(
800 LanguageModelToolUse {
801 id: LanguageModelToolUseId::from(call_id.clone()),
802 name: name.clone(),
803 is_input_complete: true,
804 input,
805 raw_input: arguments.clone(),
806 thought_signature: None,
807 },
808 )));
809 }
810 Err(error) => {
811 events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
812 id: LanguageModelToolUseId::from(call_id.clone()),
813 tool_name: name.clone(),
814 raw_input: Arc::<str>::from(arguments.clone()),
815 json_parse_error: error.to_string(),
816 }));
817 }
818 }
819 }
820 }
821 events
822 }
823}
824
825fn token_usage_from_response_usage(usage: &ResponsesUsage) -> TokenUsage {
826 TokenUsage {
827 input_tokens: usage.input_tokens.unwrap_or_default(),
828 output_tokens: usage.output_tokens.unwrap_or_default(),
829 cache_creation_input_tokens: 0,
830 cache_read_input_tokens: 0,
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use crate::responses::{
837 ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
838 ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
839 StreamEvent as ResponsesStreamEvent,
840 };
841 use futures::{StreamExt, executor::block_on};
842 use language_model_core::{
843 LanguageModelImage, LanguageModelRequestMessage, LanguageModelRequestTool,
844 LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
845 LanguageModelToolUseId, SharedString,
846 };
847 use pretty_assertions::assert_eq;
848 use serde_json::json;
849
850 use super::*;
851
852 fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
853 block_on(async {
854 OpenAiResponseEventMapper::new()
855 .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
856 .collect::<Vec<_>>()
857 .await
858 .into_iter()
859 .map(Result::unwrap)
860 .collect()
861 })
862 }
863
864 fn response_item_message(id: &str) -> ResponseOutputItem {
865 ResponseOutputItem::Message(ResponseOutputMessage {
866 id: Some(id.to_string()),
867 role: Some("assistant".to_string()),
868 status: Some("in_progress".to_string()),
869 content: vec![],
870 })
871 }
872
873 fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
874 ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
875 id: Some(id.to_string()),
876 status: Some("in_progress".to_string()),
877 name: Some("get_weather".to_string()),
878 call_id: Some("call_123".to_string()),
879 arguments: args.map(|s| s.to_string()).unwrap_or_default(),
880 })
881 }
882
883 #[test]
884 fn responses_stream_maps_text_and_usage() {
885 let events = vec![
886 ResponsesStreamEvent::OutputItemAdded {
887 output_index: 0,
888 sequence_number: None,
889 item: response_item_message("msg_123"),
890 },
891 ResponsesStreamEvent::OutputTextDelta {
892 item_id: "msg_123".into(),
893 output_index: 0,
894 content_index: Some(0),
895 delta: "Hello".into(),
896 },
897 ResponsesStreamEvent::Completed {
898 response: ResponseSummary {
899 usage: Some(ResponseUsage {
900 input_tokens: Some(5),
901 output_tokens: Some(3),
902 total_tokens: Some(8),
903 }),
904 ..Default::default()
905 },
906 },
907 ];
908
909 let mapped = map_response_events(events);
910 assert!(matches!(
911 mapped[0],
912 LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
913 ));
914 assert!(matches!(
915 mapped[1],
916 LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
917 ));
918 assert!(matches!(
919 mapped[2],
920 LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
921 input_tokens: 5,
922 output_tokens: 3,
923 ..
924 })
925 ));
926 assert!(matches!(
927 mapped[3],
928 LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
929 ));
930 }
931
932 #[test]
933 fn into_open_ai_response_builds_complete_payload() {
934 let tool_call_id = LanguageModelToolUseId::from("call-42");
935 let tool_input = json!({ "city": "Boston" });
936 let tool_arguments = serde_json::to_string(&tool_input).unwrap();
937 let tool_use = LanguageModelToolUse {
938 id: tool_call_id.clone(),
939 name: Arc::from("get_weather"),
940 raw_input: tool_arguments.clone(),
941 input: tool_input,
942 is_input_complete: true,
943 thought_signature: None,
944 };
945 let tool_result = LanguageModelToolResult {
946 tool_use_id: tool_call_id,
947 tool_name: Arc::from("get_weather"),
948 is_error: false,
949 content: vec![LanguageModelToolResultContent::Text(Arc::from("Sunny"))],
950 output: Some(json!({ "forecast": "Sunny" })),
951 };
952 let user_image = LanguageModelImage {
953 source: SharedString::from("aGVsbG8="),
954 size: None,
955 };
956 let expected_image_url = user_image.to_base64_url();
957
958 let request = LanguageModelRequest {
959 thread_id: Some("thread-123".into()),
960 prompt_id: None,
961 intent: None,
962 messages: vec![
963 LanguageModelRequestMessage {
964 role: Role::System,
965 content: vec![MessageContent::Text("System context".into())],
966 cache: false,
967 reasoning_details: None,
968 },
969 LanguageModelRequestMessage {
970 role: Role::User,
971 content: vec![
972 MessageContent::Text("Please check the weather.".into()),
973 MessageContent::Image(user_image),
974 ],
975 cache: false,
976 reasoning_details: None,
977 },
978 LanguageModelRequestMessage {
979 role: Role::Assistant,
980 content: vec![
981 MessageContent::Text("Looking that up.".into()),
982 MessageContent::ToolUse(tool_use),
983 ],
984 cache: false,
985 reasoning_details: None,
986 },
987 LanguageModelRequestMessage {
988 role: Role::Assistant,
989 content: vec![MessageContent::ToolResult(tool_result)],
990 cache: false,
991 reasoning_details: None,
992 },
993 ],
994 tools: vec![LanguageModelRequestTool {
995 name: "get_weather".into(),
996 description: "Fetches the weather".into(),
997 input_schema: json!({ "type": "object" }),
998 use_input_streaming: false,
999 }],
1000 tool_choice: Some(LanguageModelToolChoice::Any),
1001 stop: vec!["<STOP>".into()],
1002 temperature: None,
1003 thinking_allowed: false,
1004 thinking_effort: None,
1005 speed: None,
1006 };
1007
1008 let response = into_open_ai_response(
1009 request,
1010 "custom-model",
1011 true,
1012 true,
1013 Some(2048),
1014 Some(ReasoningEffort::Low),
1015 );
1016
1017 let serialized = serde_json::to_value(&response).unwrap();
1018 let expected = json!({
1019 "model": "custom-model",
1020 "input": [
1021 {
1022 "type": "message",
1023 "role": "system",
1024 "content": [
1025 { "type": "input_text", "text": "System context" }
1026 ]
1027 },
1028 {
1029 "type": "message",
1030 "role": "user",
1031 "content": [
1032 { "type": "input_text", "text": "Please check the weather." },
1033 { "type": "input_image", "image_url": expected_image_url }
1034 ]
1035 },
1036 {
1037 "type": "message",
1038 "role": "assistant",
1039 "content": [
1040 { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1041 ]
1042 },
1043 {
1044 "type": "function_call",
1045 "call_id": "call-42",
1046 "name": "get_weather",
1047 "arguments": tool_arguments
1048 },
1049 {
1050 "type": "function_call_output",
1051 "call_id": "call-42",
1052 "output": "Sunny"
1053 }
1054 ],
1055 "stream": true,
1056 "max_output_tokens": 2048,
1057 "parallel_tool_calls": true,
1058 "tool_choice": "required",
1059 "tools": [
1060 {
1061 "type": "function",
1062 "name": "get_weather",
1063 "description": "Fetches the weather",
1064 "parameters": { "type": "object" }
1065 }
1066 ],
1067 "prompt_cache_key": "thread-123",
1068 "reasoning": { "effort": "low", "summary": "auto" }
1069 });
1070
1071 assert_eq!(serialized, expected);
1072 }
1073
1074 #[test]
1075 fn responses_stream_maps_tool_calls() {
1076 let events = vec![
1077 ResponsesStreamEvent::OutputItemAdded {
1078 output_index: 0,
1079 sequence_number: None,
1080 item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
1081 },
1082 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1083 item_id: "item_fn".into(),
1084 output_index: 0,
1085 delta: "ton\"}".into(),
1086 sequence_number: None,
1087 },
1088 ResponsesStreamEvent::FunctionCallArgumentsDone {
1089 item_id: "item_fn".into(),
1090 output_index: 0,
1091 arguments: "{\"city\":\"Boston\"}".into(),
1092 sequence_number: None,
1093 },
1094 ResponsesStreamEvent::Completed {
1095 response: ResponseSummary::default(),
1096 },
1097 ];
1098
1099 let mapped = map_response_events(events);
1100 assert_eq!(mapped.len(), 3);
1101 assert!(matches!(
1102 mapped[0],
1103 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1104 is_input_complete: false,
1105 ..
1106 })
1107 ));
1108 assert!(matches!(
1109 mapped[1],
1110 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1111 ref id,
1112 ref name,
1113 ref raw_input,
1114 is_input_complete: true,
1115 ..
1116 }) if id.to_string() == "call_123"
1117 && name.as_ref() == "get_weather"
1118 && raw_input == "{\"city\":\"Boston\"}"
1119 ));
1120 assert!(matches!(
1121 mapped[2],
1122 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1123 ));
1124 }
1125
1126 #[test]
1127 fn responses_stream_uses_max_tokens_stop_reason() {
1128 let events = vec![ResponsesStreamEvent::Incomplete {
1129 response: ResponseSummary {
1130 status_details: Some(ResponseStatusDetails {
1131 reason: Some("max_output_tokens".into()),
1132 r#type: Some("incomplete".into()),
1133 error: None,
1134 }),
1135 usage: Some(ResponseUsage {
1136 input_tokens: Some(10),
1137 output_tokens: Some(20),
1138 total_tokens: Some(30),
1139 }),
1140 ..Default::default()
1141 },
1142 }];
1143
1144 let mapped = map_response_events(events);
1145 assert!(matches!(
1146 mapped[0],
1147 LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1148 input_tokens: 10,
1149 output_tokens: 20,
1150 ..
1151 })
1152 ));
1153 assert!(matches!(
1154 mapped[1],
1155 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1156 ));
1157 }
1158
1159 #[test]
1160 fn responses_stream_handles_multiple_tool_calls() {
1161 let events = vec![
1162 ResponsesStreamEvent::OutputItemAdded {
1163 output_index: 0,
1164 sequence_number: None,
1165 item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
1166 },
1167 ResponsesStreamEvent::FunctionCallArgumentsDone {
1168 item_id: "item_fn1".into(),
1169 output_index: 0,
1170 arguments: "{\"city\":\"NYC\"}".into(),
1171 sequence_number: None,
1172 },
1173 ResponsesStreamEvent::OutputItemAdded {
1174 output_index: 1,
1175 sequence_number: None,
1176 item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
1177 },
1178 ResponsesStreamEvent::FunctionCallArgumentsDone {
1179 item_id: "item_fn2".into(),
1180 output_index: 1,
1181 arguments: "{\"city\":\"LA\"}".into(),
1182 sequence_number: None,
1183 },
1184 ResponsesStreamEvent::Completed {
1185 response: ResponseSummary::default(),
1186 },
1187 ];
1188
1189 let mapped = map_response_events(events);
1190 assert_eq!(mapped.len(), 3);
1191 assert!(matches!(
1192 mapped[0],
1193 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1194 if raw_input == "{\"city\":\"NYC\"}"
1195 ));
1196 assert!(matches!(
1197 mapped[1],
1198 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1199 if raw_input == "{\"city\":\"LA\"}"
1200 ));
1201 assert!(matches!(
1202 mapped[2],
1203 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1204 ));
1205 }
1206
1207 #[test]
1208 fn responses_stream_handles_mixed_text_and_tool_calls() {
1209 let events = vec![
1210 ResponsesStreamEvent::OutputItemAdded {
1211 output_index: 0,
1212 sequence_number: None,
1213 item: response_item_message("msg_123"),
1214 },
1215 ResponsesStreamEvent::OutputTextDelta {
1216 item_id: "msg_123".into(),
1217 output_index: 0,
1218 content_index: Some(0),
1219 delta: "Let me check that".into(),
1220 },
1221 ResponsesStreamEvent::OutputItemAdded {
1222 output_index: 1,
1223 sequence_number: None,
1224 item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
1225 },
1226 ResponsesStreamEvent::FunctionCallArgumentsDone {
1227 item_id: "item_fn".into(),
1228 output_index: 1,
1229 arguments: "{\"query\":\"test\"}".into(),
1230 sequence_number: None,
1231 },
1232 ResponsesStreamEvent::Completed {
1233 response: ResponseSummary::default(),
1234 },
1235 ];
1236
1237 let mapped = map_response_events(events);
1238 assert!(matches!(
1239 mapped[0],
1240 LanguageModelCompletionEvent::StartMessage { .. }
1241 ));
1242 assert!(
1243 matches!(mapped[1], LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that")
1244 );
1245 assert!(
1246 matches!(mapped[2], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"query\":\"test\"}")
1247 );
1248 assert!(matches!(
1249 mapped[3],
1250 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1251 ));
1252 }
1253
1254 #[test]
1255 fn responses_stream_handles_json_parse_error() {
1256 let events = vec![
1257 ResponsesStreamEvent::OutputItemAdded {
1258 output_index: 0,
1259 sequence_number: None,
1260 item: response_item_function_call("item_fn", Some("{invalid json")),
1261 },
1262 ResponsesStreamEvent::FunctionCallArgumentsDone {
1263 item_id: "item_fn".into(),
1264 output_index: 0,
1265 arguments: "{invalid json".into(),
1266 sequence_number: None,
1267 },
1268 ResponsesStreamEvent::Completed {
1269 response: ResponseSummary::default(),
1270 },
1271 ];
1272
1273 let mapped = map_response_events(events);
1274 assert!(matches!(
1275 mapped[0],
1276 LanguageModelCompletionEvent::ToolUseJsonParseError { ref raw_input, .. }
1277 if raw_input.as_ref() == "{invalid json"
1278 ));
1279 }
1280
1281 #[test]
1282 fn responses_stream_handles_incomplete_function_call() {
1283 let events = vec![
1284 ResponsesStreamEvent::OutputItemAdded {
1285 output_index: 0,
1286 sequence_number: None,
1287 item: response_item_function_call("item_fn", Some("{\"city\":")),
1288 },
1289 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1290 item_id: "item_fn".into(),
1291 output_index: 0,
1292 delta: "\"Boston\"".into(),
1293 sequence_number: None,
1294 },
1295 ResponsesStreamEvent::Incomplete {
1296 response: ResponseSummary {
1297 status_details: Some(ResponseStatusDetails {
1298 reason: Some("max_output_tokens".into()),
1299 r#type: Some("incomplete".into()),
1300 error: None,
1301 }),
1302 output: vec![response_item_function_call(
1303 "item_fn",
1304 Some("{\"city\":\"Boston\"}"),
1305 )],
1306 ..Default::default()
1307 },
1308 },
1309 ];
1310
1311 let mapped = map_response_events(events);
1312 assert_eq!(mapped.len(), 3);
1313 assert!(matches!(
1314 mapped[0],
1315 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1316 is_input_complete: false,
1317 ..
1318 })
1319 ));
1320 assert!(
1321 matches!(mapped[1], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, is_input_complete: true, .. }) if raw_input == "{\"city\":\"Boston\"}")
1322 );
1323 assert!(matches!(
1324 mapped[2],
1325 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1326 ));
1327 }
1328
1329 #[test]
1330 fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
1331 let events = vec![
1332 ResponsesStreamEvent::OutputItemAdded {
1333 output_index: 0,
1334 sequence_number: None,
1335 item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
1336 },
1337 ResponsesStreamEvent::FunctionCallArgumentsDone {
1338 item_id: "item_fn".into(),
1339 output_index: 0,
1340 arguments: "{\"city\":\"Boston\"}".into(),
1341 sequence_number: None,
1342 },
1343 ResponsesStreamEvent::Incomplete {
1344 response: ResponseSummary {
1345 status_details: Some(ResponseStatusDetails {
1346 reason: Some("max_output_tokens".into()),
1347 r#type: Some("incomplete".into()),
1348 error: None,
1349 }),
1350 output: vec![response_item_function_call(
1351 "item_fn",
1352 Some("{\"city\":\"Boston\"}"),
1353 )],
1354 ..Default::default()
1355 },
1356 },
1357 ];
1358
1359 let mapped = map_response_events(events);
1360 assert_eq!(mapped.len(), 2);
1361 assert!(
1362 matches!(mapped[0], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"city\":\"Boston\"}")
1363 );
1364 assert!(matches!(
1365 mapped[1],
1366 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1367 ));
1368 }
1369
1370 #[test]
1371 fn responses_stream_handles_empty_tool_arguments() {
1372 let events = vec![
1373 ResponsesStreamEvent::OutputItemAdded {
1374 output_index: 0,
1375 sequence_number: None,
1376 item: response_item_function_call("item_fn", Some("")),
1377 },
1378 ResponsesStreamEvent::FunctionCallArgumentsDone {
1379 item_id: "item_fn".into(),
1380 output_index: 0,
1381 arguments: "".into(),
1382 sequence_number: None,
1383 },
1384 ResponsesStreamEvent::Completed {
1385 response: ResponseSummary::default(),
1386 },
1387 ];
1388
1389 let mapped = map_response_events(events);
1390 assert_eq!(mapped.len(), 2);
1391 assert!(matches!(
1392 &mapped[0],
1393 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1394 id, name, raw_input, input, ..
1395 }) if id.to_string() == "call_123"
1396 && name.as_ref() == "get_weather"
1397 && raw_input == ""
1398 && input.is_object()
1399 && input.as_object().unwrap().is_empty()
1400 ));
1401 assert!(matches!(
1402 mapped[1],
1403 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1404 ));
1405 }
1406
1407 #[test]
1408 fn responses_stream_emits_partial_tool_use_events() {
1409 let events = vec![
1410 ResponsesStreamEvent::OutputItemAdded {
1411 output_index: 0,
1412 sequence_number: None,
1413 item: ResponseOutputItem::FunctionCall(
1414 crate::responses::ResponseFunctionToolCall {
1415 id: Some("item_fn".to_string()),
1416 status: Some("in_progress".to_string()),
1417 name: Some("get_weather".to_string()),
1418 call_id: Some("call_abc".to_string()),
1419 arguments: String::new(),
1420 },
1421 ),
1422 },
1423 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1424 item_id: "item_fn".into(),
1425 output_index: 0,
1426 delta: "{\"city\":\"Bos".into(),
1427 sequence_number: None,
1428 },
1429 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1430 item_id: "item_fn".into(),
1431 output_index: 0,
1432 delta: "ton\"}".into(),
1433 sequence_number: None,
1434 },
1435 ResponsesStreamEvent::FunctionCallArgumentsDone {
1436 item_id: "item_fn".into(),
1437 output_index: 0,
1438 arguments: "{\"city\":\"Boston\"}".into(),
1439 sequence_number: None,
1440 },
1441 ResponsesStreamEvent::Completed {
1442 response: ResponseSummary::default(),
1443 },
1444 ];
1445
1446 let mapped = map_response_events(events);
1447 assert!(mapped.len() >= 3);
1448
1449 let complete_tool_use = mapped.iter().find(|e| {
1450 matches!(
1451 e,
1452 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1453 is_input_complete: true,
1454 ..
1455 })
1456 )
1457 });
1458 assert!(
1459 complete_tool_use.is_some(),
1460 "should have a complete tool use event"
1461 );
1462
1463 let tool_uses: Vec<_> = mapped
1464 .iter()
1465 .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
1466 .collect();
1467 assert!(
1468 tool_uses.len() >= 2,
1469 "should have at least one partial and one complete event"
1470 );
1471 assert!(matches!(
1472 tool_uses.last().unwrap(),
1473 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1474 is_input_complete: true,
1475 ..
1476 })
1477 ));
1478 }
1479
1480 #[test]
1481 fn responses_stream_maps_reasoning_summary_deltas() {
1482 let events = vec![
1483 ResponsesStreamEvent::OutputItemAdded {
1484 output_index: 0,
1485 sequence_number: None,
1486 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1487 id: Some("rs_123".into()),
1488 summary: vec![],
1489 }),
1490 },
1491 ResponsesStreamEvent::ReasoningSummaryPartAdded {
1492 item_id: "rs_123".into(),
1493 output_index: 0,
1494 summary_index: 0,
1495 },
1496 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1497 item_id: "rs_123".into(),
1498 output_index: 0,
1499 delta: "Thinking about".into(),
1500 },
1501 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1502 item_id: "rs_123".into(),
1503 output_index: 0,
1504 delta: " the answer".into(),
1505 },
1506 ResponsesStreamEvent::ReasoningSummaryTextDone {
1507 item_id: "rs_123".into(),
1508 output_index: 0,
1509 text: "Thinking about the answer".into(),
1510 },
1511 ResponsesStreamEvent::ReasoningSummaryPartDone {
1512 item_id: "rs_123".into(),
1513 output_index: 0,
1514 summary_index: 0,
1515 },
1516 ResponsesStreamEvent::ReasoningSummaryPartAdded {
1517 item_id: "rs_123".into(),
1518 output_index: 0,
1519 summary_index: 1,
1520 },
1521 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1522 item_id: "rs_123".into(),
1523 output_index: 0,
1524 delta: "Second part".into(),
1525 },
1526 ResponsesStreamEvent::ReasoningSummaryTextDone {
1527 item_id: "rs_123".into(),
1528 output_index: 0,
1529 text: "Second part".into(),
1530 },
1531 ResponsesStreamEvent::ReasoningSummaryPartDone {
1532 item_id: "rs_123".into(),
1533 output_index: 0,
1534 summary_index: 1,
1535 },
1536 ResponsesStreamEvent::OutputItemDone {
1537 output_index: 0,
1538 sequence_number: None,
1539 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1540 id: Some("rs_123".into()),
1541 summary: vec![
1542 ReasoningSummaryPart::SummaryText {
1543 text: "Thinking about the answer".into(),
1544 },
1545 ReasoningSummaryPart::SummaryText {
1546 text: "Second part".into(),
1547 },
1548 ],
1549 }),
1550 },
1551 ResponsesStreamEvent::OutputItemAdded {
1552 output_index: 1,
1553 sequence_number: None,
1554 item: response_item_message("msg_456"),
1555 },
1556 ResponsesStreamEvent::OutputTextDelta {
1557 item_id: "msg_456".into(),
1558 output_index: 1,
1559 content_index: Some(0),
1560 delta: "The answer is 42".into(),
1561 },
1562 ResponsesStreamEvent::Completed {
1563 response: ResponseSummary::default(),
1564 },
1565 ];
1566
1567 let mapped = map_response_events(events);
1568
1569 let thinking_events: Vec<_> = mapped
1570 .iter()
1571 .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
1572 .collect();
1573 assert_eq!(
1574 thinking_events.len(),
1575 4,
1576 "expected 4 thinking events, got {:?}",
1577 thinking_events
1578 );
1579 assert!(
1580 matches!(&thinking_events[0], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about")
1581 );
1582 assert!(
1583 matches!(&thinking_events[1], LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer")
1584 );
1585 assert!(
1586 matches!(&thinking_events[2], LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"),
1587 "expected separator between summary parts"
1588 );
1589 assert!(
1590 matches!(&thinking_events[3], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part")
1591 );
1592
1593 assert!(mapped.iter().any(
1594 |e| matches!(e, LanguageModelCompletionEvent::Text(t) if t == "The answer is 42")
1595 ));
1596 }
1597
1598 #[test]
1599 fn responses_stream_maps_reasoning_from_done_only() {
1600 let events = vec![
1601 ResponsesStreamEvent::OutputItemAdded {
1602 output_index: 0,
1603 sequence_number: None,
1604 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1605 id: Some("rs_789".into()),
1606 summary: vec![],
1607 }),
1608 },
1609 ResponsesStreamEvent::OutputItemDone {
1610 output_index: 0,
1611 sequence_number: None,
1612 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1613 id: Some("rs_789".into()),
1614 summary: vec![ReasoningSummaryPart::SummaryText {
1615 text: "Summary without deltas".into(),
1616 }],
1617 }),
1618 },
1619 ResponsesStreamEvent::Completed {
1620 response: ResponseSummary::default(),
1621 },
1622 ];
1623
1624 let mapped = map_response_events(events);
1625 assert!(
1626 !mapped
1627 .iter()
1628 .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
1629 "OutputItemDone reasoning should not produce Thinking events"
1630 );
1631 }
1632
1633 #[test]
1634 fn into_open_ai_interleaved_reasoning() {
1635 let tool_use_id = LanguageModelToolUseId::from("call-1");
1636 let tool_input = json!({"query": "foo"});
1637 let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1638 let tool_use = LanguageModelToolUse {
1639 id: tool_use_id.clone(),
1640 name: Arc::from("search"),
1641 raw_input: tool_arguments.clone(),
1642 input: tool_input,
1643 is_input_complete: true,
1644 thought_signature: None,
1645 };
1646 let tool_result = LanguageModelToolResult {
1647 tool_use_id: tool_use_id,
1648 tool_name: Arc::from("search"),
1649 is_error: false,
1650 content: vec![LanguageModelToolResultContent::Text(Arc::from("result"))],
1651 output: None,
1652 };
1653 let request = LanguageModelRequest {
1654 thread_id: None,
1655 prompt_id: None,
1656 intent: None,
1657 messages: vec![
1658 LanguageModelRequestMessage {
1659 role: Role::User,
1660 content: vec![MessageContent::Text("search for something".into())],
1661 cache: false,
1662 reasoning_details: None,
1663 },
1664 LanguageModelRequestMessage {
1665 role: Role::Assistant,
1666 content: vec![
1667 MessageContent::Thinking {
1668 text: "I should search".into(),
1669 signature: None,
1670 },
1671 MessageContent::Text("Searching now.".into()),
1672 MessageContent::ToolUse(tool_use),
1673 ],
1674 cache: false,
1675 reasoning_details: None,
1676 },
1677 LanguageModelRequestMessage {
1678 role: Role::Assistant,
1679 content: vec![MessageContent::ToolResult(tool_result)],
1680 cache: false,
1681 reasoning_details: None,
1682 },
1683 ],
1684 tools: vec![],
1685 tool_choice: None,
1686 stop: vec![],
1687 temperature: None,
1688 thinking_allowed: true,
1689 thinking_effort: None,
1690 speed: None,
1691 };
1692
1693 let result = into_open_ai(request.clone(), "model", false, false, None, None, true);
1694 assert_eq!(
1695 serde_json::to_value(&result).unwrap()["messages"],
1696 json!([
1697 {"role": "user", "content": "search for something"},
1698 {
1699 "role": "assistant",
1700 "content": "Searching now.",
1701 "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}],
1702 "reasoning_content": "I should search"
1703 },
1704 {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1705 ])
1706 );
1707
1708 let result = into_open_ai(request, "model", false, false, None, None, false);
1709 assert_eq!(
1710 serde_json::to_value(&result).unwrap()["messages"],
1711 json!([
1712 {"role": "user", "content": "search for something"},
1713 {
1714 "role": "assistant",
1715 "content": [
1716 {"type": "text", "text": "I should search"},
1717 {"type": "text", "text": "Searching now."}
1718 ],
1719 "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}]
1720 },
1721 {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1722 ])
1723 );
1724 }
1725}