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