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 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
821pub fn collect_tiktoken_messages(
822 request: LanguageModelRequest,
823) -> Vec<tiktoken_rs::ChatCompletionRequestMessage> {
824 request
825 .messages
826 .into_iter()
827 .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
828 role: match message.role {
829 Role::User => "user".into(),
830 Role::Assistant => "assistant".into(),
831 Role::System => "system".into(),
832 },
833 content: Some(message.string_contents()),
834 name: None,
835 function_call: None,
836 })
837 .collect::<Vec<_>>()
838}
839
840/// Count tokens for an OpenAI model. This is synchronous; callers should spawn
841/// it on a background thread if needed.
842pub fn count_open_ai_tokens(request: LanguageModelRequest, model: Model) -> Result<u64> {
843 let messages = collect_tiktoken_messages(request);
844 match model {
845 Model::Custom { max_tokens, .. } => {
846 let model = if max_tokens >= 100_000 {
847 // If the max tokens is 100k or more, it likely uses the o200k_base tokenizer
848 "gpt-4o"
849 } else {
850 // Otherwise fallback to gpt-4, since only cl100k_base and o200k_base are
851 // supported with this tiktoken method
852 "gpt-4"
853 };
854 tiktoken_rs::num_tokens_from_messages(model, &messages)
855 }
856 // Currently supported by tiktoken_rs
857 // Sometimes tiktoken-rs is behind on model support. If that is the case, make a new branch
858 // arm with an override. We enumerate all supported models here so that we can check if new
859 // models are supported yet or not.
860 Model::ThreePointFiveTurbo
861 | Model::Four
862 | Model::FourTurbo
863 | Model::FourOmniMini
864 | Model::FourPointOneNano
865 | Model::O1
866 | Model::O3
867 | Model::O3Mini
868 | Model::Five
869 | Model::FiveCodex
870 | Model::FiveMini
871 | Model::FiveNano => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
872 // 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
873 Model::FivePointOne
874 | Model::FivePointTwo
875 | Model::FivePointTwoCodex
876 | Model::FivePointThreeCodex
877 | Model::FivePointFour
878 | Model::FivePointFourPro => tiktoken_rs::num_tokens_from_messages("gpt-5", &messages),
879 }
880 .map(|tokens| tokens as u64)
881}
882
883#[cfg(test)]
884mod tests {
885 use crate::responses::{
886 ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
887 ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
888 StreamEvent as ResponsesStreamEvent,
889 };
890 use futures::{StreamExt, executor::block_on};
891 use language_model_core::{
892 LanguageModelImage, LanguageModelRequestMessage, LanguageModelRequestTool,
893 LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
894 LanguageModelToolUseId, SharedString,
895 };
896 use pretty_assertions::assert_eq;
897 use serde_json::json;
898
899 use super::*;
900
901 fn map_response_events(events: Vec<ResponsesStreamEvent>) -> Vec<LanguageModelCompletionEvent> {
902 block_on(async {
903 OpenAiResponseEventMapper::new()
904 .map_stream(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
905 .collect::<Vec<_>>()
906 .await
907 .into_iter()
908 .map(Result::unwrap)
909 .collect()
910 })
911 }
912
913 fn response_item_message(id: &str) -> ResponseOutputItem {
914 ResponseOutputItem::Message(ResponseOutputMessage {
915 id: Some(id.to_string()),
916 role: Some("assistant".to_string()),
917 status: Some("in_progress".to_string()),
918 content: vec![],
919 })
920 }
921
922 fn response_item_function_call(id: &str, args: Option<&str>) -> ResponseOutputItem {
923 ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
924 id: Some(id.to_string()),
925 status: Some("in_progress".to_string()),
926 name: Some("get_weather".to_string()),
927 call_id: Some("call_123".to_string()),
928 arguments: args.map(|s| s.to_string()).unwrap_or_default(),
929 })
930 }
931
932 #[test]
933 fn tiktoken_rs_support() {
934 let request = LanguageModelRequest {
935 thread_id: None,
936 prompt_id: None,
937 intent: None,
938 messages: vec![LanguageModelRequestMessage {
939 role: Role::User,
940 content: vec![MessageContent::Text("message".into())],
941 cache: false,
942 reasoning_details: None,
943 }],
944 tools: vec![],
945 tool_choice: None,
946 stop: vec![],
947 temperature: None,
948 thinking_allowed: true,
949 thinking_effort: None,
950 speed: None,
951 };
952
953 // Validate that all models are supported by tiktoken-rs
954 for model in <Model as strum::IntoEnumIterator>::iter() {
955 let count = count_open_ai_tokens(request.clone(), model).unwrap();
956 assert!(count > 0);
957 }
958 }
959
960 #[test]
961 fn responses_stream_maps_text_and_usage() {
962 let events = vec![
963 ResponsesStreamEvent::OutputItemAdded {
964 output_index: 0,
965 sequence_number: None,
966 item: response_item_message("msg_123"),
967 },
968 ResponsesStreamEvent::OutputTextDelta {
969 item_id: "msg_123".into(),
970 output_index: 0,
971 content_index: Some(0),
972 delta: "Hello".into(),
973 },
974 ResponsesStreamEvent::Completed {
975 response: ResponseSummary {
976 usage: Some(ResponseUsage {
977 input_tokens: Some(5),
978 output_tokens: Some(3),
979 total_tokens: Some(8),
980 }),
981 ..Default::default()
982 },
983 },
984 ];
985
986 let mapped = map_response_events(events);
987 assert!(matches!(
988 mapped[0],
989 LanguageModelCompletionEvent::StartMessage { ref message_id } if message_id == "msg_123"
990 ));
991 assert!(matches!(
992 mapped[1],
993 LanguageModelCompletionEvent::Text(ref text) if text == "Hello"
994 ));
995 assert!(matches!(
996 mapped[2],
997 LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
998 input_tokens: 5,
999 output_tokens: 3,
1000 ..
1001 })
1002 ));
1003 assert!(matches!(
1004 mapped[3],
1005 LanguageModelCompletionEvent::Stop(StopReason::EndTurn)
1006 ));
1007 }
1008
1009 #[test]
1010 fn into_open_ai_response_builds_complete_payload() {
1011 let tool_call_id = LanguageModelToolUseId::from("call-42");
1012 let tool_input = json!({ "city": "Boston" });
1013 let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1014 let tool_use = LanguageModelToolUse {
1015 id: tool_call_id.clone(),
1016 name: Arc::from("get_weather"),
1017 raw_input: tool_arguments.clone(),
1018 input: tool_input,
1019 is_input_complete: true,
1020 thought_signature: None,
1021 };
1022 let tool_result = LanguageModelToolResult {
1023 tool_use_id: tool_call_id,
1024 tool_name: Arc::from("get_weather"),
1025 is_error: false,
1026 content: LanguageModelToolResultContent::Text(Arc::from("Sunny")),
1027 output: Some(json!({ "forecast": "Sunny" })),
1028 };
1029 let user_image = LanguageModelImage {
1030 source: SharedString::from("aGVsbG8="),
1031 size: None,
1032 };
1033 let expected_image_url = user_image.to_base64_url();
1034
1035 let request = LanguageModelRequest {
1036 thread_id: Some("thread-123".into()),
1037 prompt_id: None,
1038 intent: None,
1039 messages: vec![
1040 LanguageModelRequestMessage {
1041 role: Role::System,
1042 content: vec![MessageContent::Text("System context".into())],
1043 cache: false,
1044 reasoning_details: None,
1045 },
1046 LanguageModelRequestMessage {
1047 role: Role::User,
1048 content: vec![
1049 MessageContent::Text("Please check the weather.".into()),
1050 MessageContent::Image(user_image),
1051 ],
1052 cache: false,
1053 reasoning_details: None,
1054 },
1055 LanguageModelRequestMessage {
1056 role: Role::Assistant,
1057 content: vec![
1058 MessageContent::Text("Looking that up.".into()),
1059 MessageContent::ToolUse(tool_use),
1060 ],
1061 cache: false,
1062 reasoning_details: None,
1063 },
1064 LanguageModelRequestMessage {
1065 role: Role::Assistant,
1066 content: vec![MessageContent::ToolResult(tool_result)],
1067 cache: false,
1068 reasoning_details: None,
1069 },
1070 ],
1071 tools: vec![LanguageModelRequestTool {
1072 name: "get_weather".into(),
1073 description: "Fetches the weather".into(),
1074 input_schema: json!({ "type": "object" }),
1075 use_input_streaming: false,
1076 }],
1077 tool_choice: Some(LanguageModelToolChoice::Any),
1078 stop: vec!["<STOP>".into()],
1079 temperature: None,
1080 thinking_allowed: false,
1081 thinking_effort: None,
1082 speed: None,
1083 };
1084
1085 let response = into_open_ai_response(
1086 request,
1087 "custom-model",
1088 true,
1089 true,
1090 Some(2048),
1091 Some(ReasoningEffort::Low),
1092 );
1093
1094 let serialized = serde_json::to_value(&response).unwrap();
1095 let expected = json!({
1096 "model": "custom-model",
1097 "input": [
1098 {
1099 "type": "message",
1100 "role": "system",
1101 "content": [
1102 { "type": "input_text", "text": "System context" }
1103 ]
1104 },
1105 {
1106 "type": "message",
1107 "role": "user",
1108 "content": [
1109 { "type": "input_text", "text": "Please check the weather." },
1110 { "type": "input_image", "image_url": expected_image_url }
1111 ]
1112 },
1113 {
1114 "type": "message",
1115 "role": "assistant",
1116 "content": [
1117 { "type": "output_text", "text": "Looking that up.", "annotations": [] }
1118 ]
1119 },
1120 {
1121 "type": "function_call",
1122 "call_id": "call-42",
1123 "name": "get_weather",
1124 "arguments": tool_arguments
1125 },
1126 {
1127 "type": "function_call_output",
1128 "call_id": "call-42",
1129 "output": "Sunny"
1130 }
1131 ],
1132 "stream": true,
1133 "max_output_tokens": 2048,
1134 "parallel_tool_calls": true,
1135 "tool_choice": "required",
1136 "tools": [
1137 {
1138 "type": "function",
1139 "name": "get_weather",
1140 "description": "Fetches the weather",
1141 "parameters": { "type": "object" }
1142 }
1143 ],
1144 "prompt_cache_key": "thread-123",
1145 "reasoning": { "effort": "low", "summary": "auto" }
1146 });
1147
1148 assert_eq!(serialized, expected);
1149 }
1150
1151 #[test]
1152 fn responses_stream_maps_tool_calls() {
1153 let events = vec![
1154 ResponsesStreamEvent::OutputItemAdded {
1155 output_index: 0,
1156 sequence_number: None,
1157 item: response_item_function_call("item_fn", Some("{\"city\":\"Bos")),
1158 },
1159 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1160 item_id: "item_fn".into(),
1161 output_index: 0,
1162 delta: "ton\"}".into(),
1163 sequence_number: None,
1164 },
1165 ResponsesStreamEvent::FunctionCallArgumentsDone {
1166 item_id: "item_fn".into(),
1167 output_index: 0,
1168 arguments: "{\"city\":\"Boston\"}".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 {
1181 is_input_complete: false,
1182 ..
1183 })
1184 ));
1185 assert!(matches!(
1186 mapped[1],
1187 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1188 ref id,
1189 ref name,
1190 ref raw_input,
1191 is_input_complete: true,
1192 ..
1193 }) if id.to_string() == "call_123"
1194 && name.as_ref() == "get_weather"
1195 && raw_input == "{\"city\":\"Boston\"}"
1196 ));
1197 assert!(matches!(
1198 mapped[2],
1199 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1200 ));
1201 }
1202
1203 #[test]
1204 fn responses_stream_uses_max_tokens_stop_reason() {
1205 let events = vec![ResponsesStreamEvent::Incomplete {
1206 response: ResponseSummary {
1207 status_details: Some(ResponseStatusDetails {
1208 reason: Some("max_output_tokens".into()),
1209 r#type: Some("incomplete".into()),
1210 error: None,
1211 }),
1212 usage: Some(ResponseUsage {
1213 input_tokens: Some(10),
1214 output_tokens: Some(20),
1215 total_tokens: Some(30),
1216 }),
1217 ..Default::default()
1218 },
1219 }];
1220
1221 let mapped = map_response_events(events);
1222 assert!(matches!(
1223 mapped[0],
1224 LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
1225 input_tokens: 10,
1226 output_tokens: 20,
1227 ..
1228 })
1229 ));
1230 assert!(matches!(
1231 mapped[1],
1232 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1233 ));
1234 }
1235
1236 #[test]
1237 fn responses_stream_handles_multiple_tool_calls() {
1238 let events = vec![
1239 ResponsesStreamEvent::OutputItemAdded {
1240 output_index: 0,
1241 sequence_number: None,
1242 item: response_item_function_call("item_fn1", Some("{\"city\":\"NYC\"}")),
1243 },
1244 ResponsesStreamEvent::FunctionCallArgumentsDone {
1245 item_id: "item_fn1".into(),
1246 output_index: 0,
1247 arguments: "{\"city\":\"NYC\"}".into(),
1248 sequence_number: None,
1249 },
1250 ResponsesStreamEvent::OutputItemAdded {
1251 output_index: 1,
1252 sequence_number: None,
1253 item: response_item_function_call("item_fn2", Some("{\"city\":\"LA\"}")),
1254 },
1255 ResponsesStreamEvent::FunctionCallArgumentsDone {
1256 item_id: "item_fn2".into(),
1257 output_index: 1,
1258 arguments: "{\"city\":\"LA\"}".into(),
1259 sequence_number: None,
1260 },
1261 ResponsesStreamEvent::Completed {
1262 response: ResponseSummary::default(),
1263 },
1264 ];
1265
1266 let mapped = map_response_events(events);
1267 assert_eq!(mapped.len(), 3);
1268 assert!(matches!(
1269 mapped[0],
1270 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1271 if raw_input == "{\"city\":\"NYC\"}"
1272 ));
1273 assert!(matches!(
1274 mapped[1],
1275 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. })
1276 if raw_input == "{\"city\":\"LA\"}"
1277 ));
1278 assert!(matches!(
1279 mapped[2],
1280 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1281 ));
1282 }
1283
1284 #[test]
1285 fn responses_stream_handles_mixed_text_and_tool_calls() {
1286 let events = vec![
1287 ResponsesStreamEvent::OutputItemAdded {
1288 output_index: 0,
1289 sequence_number: None,
1290 item: response_item_message("msg_123"),
1291 },
1292 ResponsesStreamEvent::OutputTextDelta {
1293 item_id: "msg_123".into(),
1294 output_index: 0,
1295 content_index: Some(0),
1296 delta: "Let me check that".into(),
1297 },
1298 ResponsesStreamEvent::OutputItemAdded {
1299 output_index: 1,
1300 sequence_number: None,
1301 item: response_item_function_call("item_fn", Some("{\"query\":\"test\"}")),
1302 },
1303 ResponsesStreamEvent::FunctionCallArgumentsDone {
1304 item_id: "item_fn".into(),
1305 output_index: 1,
1306 arguments: "{\"query\":\"test\"}".into(),
1307 sequence_number: None,
1308 },
1309 ResponsesStreamEvent::Completed {
1310 response: ResponseSummary::default(),
1311 },
1312 ];
1313
1314 let mapped = map_response_events(events);
1315 assert!(matches!(
1316 mapped[0],
1317 LanguageModelCompletionEvent::StartMessage { .. }
1318 ));
1319 assert!(
1320 matches!(mapped[1], LanguageModelCompletionEvent::Text(ref text) if text == "Let me check that")
1321 );
1322 assert!(
1323 matches!(mapped[2], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"query\":\"test\"}")
1324 );
1325 assert!(matches!(
1326 mapped[3],
1327 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1328 ));
1329 }
1330
1331 #[test]
1332 fn responses_stream_handles_json_parse_error() {
1333 let events = vec![
1334 ResponsesStreamEvent::OutputItemAdded {
1335 output_index: 0,
1336 sequence_number: None,
1337 item: response_item_function_call("item_fn", Some("{invalid json")),
1338 },
1339 ResponsesStreamEvent::FunctionCallArgumentsDone {
1340 item_id: "item_fn".into(),
1341 output_index: 0,
1342 arguments: "{invalid json".into(),
1343 sequence_number: None,
1344 },
1345 ResponsesStreamEvent::Completed {
1346 response: ResponseSummary::default(),
1347 },
1348 ];
1349
1350 let mapped = map_response_events(events);
1351 assert!(matches!(
1352 mapped[0],
1353 LanguageModelCompletionEvent::ToolUseJsonParseError { ref raw_input, .. }
1354 if raw_input.as_ref() == "{invalid json"
1355 ));
1356 }
1357
1358 #[test]
1359 fn responses_stream_handles_incomplete_function_call() {
1360 let events = vec![
1361 ResponsesStreamEvent::OutputItemAdded {
1362 output_index: 0,
1363 sequence_number: None,
1364 item: response_item_function_call("item_fn", Some("{\"city\":")),
1365 },
1366 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1367 item_id: "item_fn".into(),
1368 output_index: 0,
1369 delta: "\"Boston\"".into(),
1370 sequence_number: None,
1371 },
1372 ResponsesStreamEvent::Incomplete {
1373 response: ResponseSummary {
1374 status_details: Some(ResponseStatusDetails {
1375 reason: Some("max_output_tokens".into()),
1376 r#type: Some("incomplete".into()),
1377 error: None,
1378 }),
1379 output: vec![response_item_function_call(
1380 "item_fn",
1381 Some("{\"city\":\"Boston\"}"),
1382 )],
1383 ..Default::default()
1384 },
1385 },
1386 ];
1387
1388 let mapped = map_response_events(events);
1389 assert_eq!(mapped.len(), 3);
1390 assert!(matches!(
1391 mapped[0],
1392 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1393 is_input_complete: false,
1394 ..
1395 })
1396 ));
1397 assert!(
1398 matches!(mapped[1], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, is_input_complete: true, .. }) if raw_input == "{\"city\":\"Boston\"}")
1399 );
1400 assert!(matches!(
1401 mapped[2],
1402 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1403 ));
1404 }
1405
1406 #[test]
1407 fn responses_stream_incomplete_does_not_duplicate_tool_calls() {
1408 let events = vec![
1409 ResponsesStreamEvent::OutputItemAdded {
1410 output_index: 0,
1411 sequence_number: None,
1412 item: response_item_function_call("item_fn", Some("{\"city\":\"Boston\"}")),
1413 },
1414 ResponsesStreamEvent::FunctionCallArgumentsDone {
1415 item_id: "item_fn".into(),
1416 output_index: 0,
1417 arguments: "{\"city\":\"Boston\"}".into(),
1418 sequence_number: None,
1419 },
1420 ResponsesStreamEvent::Incomplete {
1421 response: ResponseSummary {
1422 status_details: Some(ResponseStatusDetails {
1423 reason: Some("max_output_tokens".into()),
1424 r#type: Some("incomplete".into()),
1425 error: None,
1426 }),
1427 output: vec![response_item_function_call(
1428 "item_fn",
1429 Some("{\"city\":\"Boston\"}"),
1430 )],
1431 ..Default::default()
1432 },
1433 },
1434 ];
1435
1436 let mapped = map_response_events(events);
1437 assert_eq!(mapped.len(), 2);
1438 assert!(
1439 matches!(mapped[0], LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { ref raw_input, .. }) if raw_input == "{\"city\":\"Boston\"}")
1440 );
1441 assert!(matches!(
1442 mapped[1],
1443 LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)
1444 ));
1445 }
1446
1447 #[test]
1448 fn responses_stream_handles_empty_tool_arguments() {
1449 let events = vec![
1450 ResponsesStreamEvent::OutputItemAdded {
1451 output_index: 0,
1452 sequence_number: None,
1453 item: response_item_function_call("item_fn", Some("")),
1454 },
1455 ResponsesStreamEvent::FunctionCallArgumentsDone {
1456 item_id: "item_fn".into(),
1457 output_index: 0,
1458 arguments: "".into(),
1459 sequence_number: None,
1460 },
1461 ResponsesStreamEvent::Completed {
1462 response: ResponseSummary::default(),
1463 },
1464 ];
1465
1466 let mapped = map_response_events(events);
1467 assert_eq!(mapped.len(), 2);
1468 assert!(matches!(
1469 &mapped[0],
1470 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1471 id, name, raw_input, input, ..
1472 }) if id.to_string() == "call_123"
1473 && name.as_ref() == "get_weather"
1474 && raw_input == ""
1475 && input.is_object()
1476 && input.as_object().unwrap().is_empty()
1477 ));
1478 assert!(matches!(
1479 mapped[1],
1480 LanguageModelCompletionEvent::Stop(StopReason::ToolUse)
1481 ));
1482 }
1483
1484 #[test]
1485 fn responses_stream_emits_partial_tool_use_events() {
1486 let events = vec![
1487 ResponsesStreamEvent::OutputItemAdded {
1488 output_index: 0,
1489 sequence_number: None,
1490 item: ResponseOutputItem::FunctionCall(
1491 crate::responses::ResponseFunctionToolCall {
1492 id: Some("item_fn".to_string()),
1493 status: Some("in_progress".to_string()),
1494 name: Some("get_weather".to_string()),
1495 call_id: Some("call_abc".to_string()),
1496 arguments: String::new(),
1497 },
1498 ),
1499 },
1500 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1501 item_id: "item_fn".into(),
1502 output_index: 0,
1503 delta: "{\"city\":\"Bos".into(),
1504 sequence_number: None,
1505 },
1506 ResponsesStreamEvent::FunctionCallArgumentsDelta {
1507 item_id: "item_fn".into(),
1508 output_index: 0,
1509 delta: "ton\"}".into(),
1510 sequence_number: None,
1511 },
1512 ResponsesStreamEvent::FunctionCallArgumentsDone {
1513 item_id: "item_fn".into(),
1514 output_index: 0,
1515 arguments: "{\"city\":\"Boston\"}".into(),
1516 sequence_number: None,
1517 },
1518 ResponsesStreamEvent::Completed {
1519 response: ResponseSummary::default(),
1520 },
1521 ];
1522
1523 let mapped = map_response_events(events);
1524 assert!(mapped.len() >= 3);
1525
1526 let complete_tool_use = mapped.iter().find(|e| {
1527 matches!(
1528 e,
1529 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1530 is_input_complete: true,
1531 ..
1532 })
1533 )
1534 });
1535 assert!(
1536 complete_tool_use.is_some(),
1537 "should have a complete tool use event"
1538 );
1539
1540 let tool_uses: Vec<_> = mapped
1541 .iter()
1542 .filter(|e| matches!(e, LanguageModelCompletionEvent::ToolUse(_)))
1543 .collect();
1544 assert!(
1545 tool_uses.len() >= 2,
1546 "should have at least one partial and one complete event"
1547 );
1548 assert!(matches!(
1549 tool_uses.last().unwrap(),
1550 LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
1551 is_input_complete: true,
1552 ..
1553 })
1554 ));
1555 }
1556
1557 #[test]
1558 fn responses_stream_maps_reasoning_summary_deltas() {
1559 let events = vec![
1560 ResponsesStreamEvent::OutputItemAdded {
1561 output_index: 0,
1562 sequence_number: None,
1563 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1564 id: Some("rs_123".into()),
1565 summary: vec![],
1566 }),
1567 },
1568 ResponsesStreamEvent::ReasoningSummaryPartAdded {
1569 item_id: "rs_123".into(),
1570 output_index: 0,
1571 summary_index: 0,
1572 },
1573 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1574 item_id: "rs_123".into(),
1575 output_index: 0,
1576 delta: "Thinking about".into(),
1577 },
1578 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1579 item_id: "rs_123".into(),
1580 output_index: 0,
1581 delta: " the answer".into(),
1582 },
1583 ResponsesStreamEvent::ReasoningSummaryTextDone {
1584 item_id: "rs_123".into(),
1585 output_index: 0,
1586 text: "Thinking about the answer".into(),
1587 },
1588 ResponsesStreamEvent::ReasoningSummaryPartDone {
1589 item_id: "rs_123".into(),
1590 output_index: 0,
1591 summary_index: 0,
1592 },
1593 ResponsesStreamEvent::ReasoningSummaryPartAdded {
1594 item_id: "rs_123".into(),
1595 output_index: 0,
1596 summary_index: 1,
1597 },
1598 ResponsesStreamEvent::ReasoningSummaryTextDelta {
1599 item_id: "rs_123".into(),
1600 output_index: 0,
1601 delta: "Second part".into(),
1602 },
1603 ResponsesStreamEvent::ReasoningSummaryTextDone {
1604 item_id: "rs_123".into(),
1605 output_index: 0,
1606 text: "Second part".into(),
1607 },
1608 ResponsesStreamEvent::ReasoningSummaryPartDone {
1609 item_id: "rs_123".into(),
1610 output_index: 0,
1611 summary_index: 1,
1612 },
1613 ResponsesStreamEvent::OutputItemDone {
1614 output_index: 0,
1615 sequence_number: None,
1616 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1617 id: Some("rs_123".into()),
1618 summary: vec![
1619 ReasoningSummaryPart::SummaryText {
1620 text: "Thinking about the answer".into(),
1621 },
1622 ReasoningSummaryPart::SummaryText {
1623 text: "Second part".into(),
1624 },
1625 ],
1626 }),
1627 },
1628 ResponsesStreamEvent::OutputItemAdded {
1629 output_index: 1,
1630 sequence_number: None,
1631 item: response_item_message("msg_456"),
1632 },
1633 ResponsesStreamEvent::OutputTextDelta {
1634 item_id: "msg_456".into(),
1635 output_index: 1,
1636 content_index: Some(0),
1637 delta: "The answer is 42".into(),
1638 },
1639 ResponsesStreamEvent::Completed {
1640 response: ResponseSummary::default(),
1641 },
1642 ];
1643
1644 let mapped = map_response_events(events);
1645
1646 let thinking_events: Vec<_> = mapped
1647 .iter()
1648 .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
1649 .collect();
1650 assert_eq!(
1651 thinking_events.len(),
1652 4,
1653 "expected 4 thinking events, got {:?}",
1654 thinking_events
1655 );
1656 assert!(
1657 matches!(&thinking_events[0], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about")
1658 );
1659 assert!(
1660 matches!(&thinking_events[1], LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer")
1661 );
1662 assert!(
1663 matches!(&thinking_events[2], LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"),
1664 "expected separator between summary parts"
1665 );
1666 assert!(
1667 matches!(&thinking_events[3], LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part")
1668 );
1669
1670 assert!(mapped.iter().any(
1671 |e| matches!(e, LanguageModelCompletionEvent::Text(t) if t == "The answer is 42")
1672 ));
1673 }
1674
1675 #[test]
1676 fn responses_stream_maps_reasoning_from_done_only() {
1677 let events = vec![
1678 ResponsesStreamEvent::OutputItemAdded {
1679 output_index: 0,
1680 sequence_number: None,
1681 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1682 id: Some("rs_789".into()),
1683 summary: vec![],
1684 }),
1685 },
1686 ResponsesStreamEvent::OutputItemDone {
1687 output_index: 0,
1688 sequence_number: None,
1689 item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
1690 id: Some("rs_789".into()),
1691 summary: vec![ReasoningSummaryPart::SummaryText {
1692 text: "Summary without deltas".into(),
1693 }],
1694 }),
1695 },
1696 ResponsesStreamEvent::Completed {
1697 response: ResponseSummary::default(),
1698 },
1699 ];
1700
1701 let mapped = map_response_events(events);
1702 assert!(
1703 !mapped
1704 .iter()
1705 .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
1706 "OutputItemDone reasoning should not produce Thinking events"
1707 );
1708 }
1709
1710 #[test]
1711 fn into_open_ai_interleaved_reasoning() {
1712 let tool_use_id = LanguageModelToolUseId::from("call-1");
1713 let tool_input = json!({"query": "foo"});
1714 let tool_arguments = serde_json::to_string(&tool_input).unwrap();
1715 let tool_use = LanguageModelToolUse {
1716 id: tool_use_id.clone(),
1717 name: Arc::from("search"),
1718 raw_input: tool_arguments.clone(),
1719 input: tool_input,
1720 is_input_complete: true,
1721 thought_signature: None,
1722 };
1723 let tool_result = LanguageModelToolResult {
1724 tool_use_id: tool_use_id,
1725 tool_name: Arc::from("search"),
1726 is_error: false,
1727 content: LanguageModelToolResultContent::Text(Arc::from("result")),
1728 output: None,
1729 };
1730 let request = LanguageModelRequest {
1731 thread_id: None,
1732 prompt_id: None,
1733 intent: None,
1734 messages: vec![
1735 LanguageModelRequestMessage {
1736 role: Role::User,
1737 content: vec![MessageContent::Text("search for something".into())],
1738 cache: false,
1739 reasoning_details: None,
1740 },
1741 LanguageModelRequestMessage {
1742 role: Role::Assistant,
1743 content: vec![
1744 MessageContent::Thinking {
1745 text: "I should search".into(),
1746 signature: None,
1747 },
1748 MessageContent::Text("Searching now.".into()),
1749 MessageContent::ToolUse(tool_use),
1750 ],
1751 cache: false,
1752 reasoning_details: None,
1753 },
1754 LanguageModelRequestMessage {
1755 role: Role::Assistant,
1756 content: vec![MessageContent::ToolResult(tool_result)],
1757 cache: false,
1758 reasoning_details: None,
1759 },
1760 ],
1761 tools: vec![],
1762 tool_choice: None,
1763 stop: vec![],
1764 temperature: None,
1765 thinking_allowed: true,
1766 thinking_effort: None,
1767 speed: None,
1768 };
1769
1770 let result = into_open_ai(request.clone(), "model", false, false, None, None, true);
1771 assert_eq!(
1772 serde_json::to_value(&result).unwrap()["messages"],
1773 json!([
1774 {"role": "user", "content": "search for something"},
1775 {
1776 "role": "assistant",
1777 "content": "Searching now.",
1778 "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}],
1779 "reasoning_content": "I should search"
1780 },
1781 {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1782 ])
1783 );
1784
1785 let result = into_open_ai(request, "model", false, false, None, None, false);
1786 assert_eq!(
1787 serde_json::to_value(&result).unwrap()["messages"],
1788 json!([
1789 {"role": "user", "content": "search for something"},
1790 {
1791 "role": "assistant",
1792 "content": [
1793 {"type": "text", "text": "I should search"},
1794 {"type": "text", "text": "Searching now."}
1795 ],
1796 "tool_calls": [{"id": "call-1", "type": "function", "function": {"name": "search", "arguments": tool_arguments}}]
1797 },
1798 {"role": "tool", "content": "result", "tool_call_id": "call-1"}
1799 ])
1800 );
1801 }
1802}