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