1use bytes::Bytes;
4use futures::{Stream, StreamExt};
5use reqwest::Client;
6use serde::Deserialize;
7use serde_json::Value as JsonValue;
8use serde_json::json;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use super::openai_responses_shared::parse_streaming_json;
14use super::shared_client;
15use super::sse::split_complete_lines;
16use crate::{
17 Api, AssistantMessage, ContentBlock, Context, Message, MessageContent, Model, Provider,
18 ProviderEvent, StopReason, StreamOptions, StreamResult, TextContent, ThinkingContent, ToolCall,
19 ToolResultMessage, Usage, error::ProviderError,
20};
21
22fn is_zai(model: &Model) -> bool {
24 model.provider.eq_ignore_ascii_case("zai") || model.base_url.contains("api.z.ai")
25}
26
27#[derive(Clone)]
29pub struct OpenAiProvider {
30 client: &'static Client,
31 api_key: Option<String>,
32 base_url: Option<String>,
33 extra_headers: Vec<(String, String)>,
35}
36
37impl OpenAiProvider {
38 pub fn new() -> Self {
43 Self {
44 client: shared_client(),
45 api_key: None,
46 base_url: None,
47 extra_headers: Vec::new(),
48 }
49 }
50
51 pub fn with_api_key(api_key: impl Into<String>) -> Self {
53 Self {
54 client: shared_client(),
55 api_key: Some(api_key.into()),
56 base_url: None,
57 extra_headers: Vec::new(),
58 }
59 }
60
61 pub fn with_base_url(base_url: &str) -> Self {
65 Self {
66 client: shared_client(),
67 api_key: None,
68 base_url: Some(base_url.to_string()),
69 extra_headers: Vec::new(),
70 }
71 }
72
73 pub fn with_base_url_and_key(base_url: &str, api_key: Option<String>) -> Self {
77 Self {
78 client: shared_client(),
79 api_key,
80 base_url: Some(base_url.to_string()),
81 extra_headers: Vec::new(),
82 }
83 }
84
85 pub fn with_config(
89 base_url: &str,
90 api_key: Option<String>,
91 extra_headers: Vec<(String, String)>,
92 ) -> Self {
93 Self {
94 client: shared_client(),
95 api_key,
96 base_url: Some(base_url.to_string()),
97 extra_headers,
98 }
99 }
100}
101
102impl Default for OpenAiProvider {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl Provider for OpenAiProvider {
109 fn stream<'a>(
110 &'a self,
111 model: &'a Model,
112 context: &'a Context,
113 options: Option<StreamOptions>,
114 ) -> Pin<Box<dyn Future<Output = StreamResult> + Send + 'a>> {
115 Box::pin(async move {
116 let options = options.unwrap_or_default();
117
118 let effective_base_url = self.base_url.as_deref().unwrap_or(&model.base_url);
120 let url = format!("{}/chat/completions", effective_base_url);
121
122 let api_key = options
124 .api_key
125 .as_ref()
126 .or(self.api_key.as_ref())
127 .ok_or_else(|| ProviderError::MissingApiKey)?;
128
129 let normalized = normalize_messages(&context.messages, &model.provider, &model.id);
131 let messages = build_messages_from_normalized(&context.system_prompt, &normalized)?;
132
133 let mut body = serde_json::json!({
135 "model": model.id,
136 "messages": messages,
137 "stream": true,
138 "stream_options": { "include_usage": true },
139 });
140
141 if let Some(temp) = options.temperature {
143 body["temperature"] = serde_json::json!(temp);
144 }
145
146 if let Some(max) = options.max_tokens {
147 body["max_completion_tokens"] = serde_json::json!(max);
150 body["max_tokens"] = serde_json::json!(max);
151 }
152
153 if !context.tools.is_empty() {
155 body["tools"] = build_tools(&context.tools)?;
156 }
157
158 if let Some(choice) = build_tool_choice(options.tool_choice.as_ref()) {
160 body["tool_choice"] = choice;
161 }
162
163 if model.reasoning {
168 let openai_opts = options
169 .provider_options
170 .as_ref()
171 .and_then(|po| po.openai.as_ref());
172
173 let effort = openai_opts
174 .and_then(|o| o.reasoning_effort.clone())
175 .or_else(|| {
176 options
177 .thinking_level
178 .as_ref()
179 .and_then(|l| l.as_str().map(String::from))
180 });
181
182 if let Some(effort_str) = effort {
183 body["reasoning_effort"] = serde_json::json!(effort_str);
184 }
185 }
186
187 if is_zai(model) {
191 if model.reasoning {
192 body["enable_thinking"] = serde_json::json!(true);
193 }
194 if !context.tools.is_empty() {
195 body["tool_stream"] = serde_json::json!(true);
196 }
197 }
198
199 tracing::info!(
200 "Sending request to {} model={} body_len={} enable_thinking={} tool_stream={}",
201 url,
202 model.id,
203 body.to_string().len(),
204 body.get("enable_thinking").is_some(),
205 body.get("tool_stream").is_some()
206 );
207 tracing::debug!("Request body: {}", body.to_string());
208
209 let mut headers = reqwest::header::HeaderMap::new();
211 headers.insert(
212 reqwest::header::AUTHORIZATION,
213 format!("Bearer {}", api_key).parse().map_err(|e| {
214 ProviderError::InvalidResponse(format!("invalid bearer header: {e}"))
215 })?,
216 );
217 headers.insert(
218 reqwest::header::CONTENT_TYPE,
219 "application/json".parse().map_err(|e| {
220 ProviderError::InvalidResponse(format!("invalid header value: {e}"))
221 })?,
222 );
223
224 for (k, v) in &self.extra_headers {
226 if let (Ok(name), Ok(value)) = (
227 k.parse::<reqwest::header::HeaderName>(),
228 v.parse::<reqwest::header::HeaderValue>(),
229 ) {
230 headers.insert(name, value);
231 }
232 }
233
234 for (k, v) in &options.headers {
236 if let (Ok(name), Ok(value)) = (
237 k.parse::<reqwest::header::HeaderName>(),
238 v.parse::<reqwest::header::HeaderValue>(),
239 ) {
240 headers.insert(name, value);
241 }
242 }
243
244 let response = self
246 .client
247 .post(&url)
248 .headers(headers)
249 .json(&body)
250 .send()
251 .await
252 .map_err(ProviderError::RequestFailed)?;
253
254 if !response.status().is_success() {
255 let status = response.status();
256 let body: String = response.text().await.unwrap_or_default();
257 return Err(ProviderError::HttpError(
258 crate::error::HttpErrorDetail::new(status.as_u16(), body),
259 ));
260 }
261
262 let provider_name = model.provider.clone();
264 let model_id = model.id.clone();
265
266 let start_event = ProviderEvent::Start {
268 partial: Arc::new(AssistantMessage::new(
269 Api::OpenAiCompletions,
270 &provider_name,
271 &model_id,
272 )),
273 };
274
275 let stream = response
286 .bytes_stream()
287 .scan(
288 (
289 Vec::new(),
290 std::collections::HashMap::<usize, (String, String, String)>::new(),
291 std::collections::HashMap::<String, usize>::new(), false,
293 AssistantMessage::new(Api::OpenAiCompletions, &provider_name, &model_id),
294 ),
295 move |(
296 pending_bytes,
297 pending_tc,
298 tc_id_to_index,
299 thinking_started,
300 accumulated_output,
301 ),
302 chunk: Result<Bytes, reqwest::Error>| {
303 let events = match chunk {
304 Ok(bytes) => {
305 let mut combined =
307 Vec::with_capacity(pending_bytes.len() + bytes.len());
308 combined.extend_from_slice(pending_bytes);
309 combined.extend_from_slice(&bytes);
310
311 let (text, trailing) = split_complete_lines(&combined);
315 *pending_bytes = trailing;
316
317 tracing::debug!(
318 "parse_sse_events input: {} bytes, {} lines",
319 text.len(),
320 text.lines().count()
321 );
322 let raw_events = parse_sse_events(
323 &text,
324 &provider_name,
325 &model_id,
326 accumulated_output,
327 );
328 tracing::debug!("parse_sse_events output: {} events", raw_events.len());
329
330 let mut processed = Vec::new();
332 for event in raw_events {
333 match &event {
334 ProviderEvent::ThinkingDelta { content_index, .. } => {
335 if !*thinking_started {
337 *thinking_started = true;
338 processed.push(ProviderEvent::ThinkingStart {
339 content_index: *content_index,
340 partial: Arc::new(AssistantMessage::new(
341 Api::OpenAiCompletions,
342 &provider_name,
343 &model_id,
344 )),
345 });
346 }
347 processed.push(event);
348 }
349 ProviderEvent::ToolCallStart {
350 content_index,
351 tool_call_id,
352 tool_name,
353 ..
354 } => {
355 let entry =
356 pending_tc.entry(*content_index).or_insert_with(|| {
357 (String::new(), String::new(), String::new())
358 });
359 if let Some(id) = tool_call_id
360 && !id.is_empty()
361 {
362 entry.0 = id.clone();
363 tc_id_to_index.insert(id.clone(), *content_index);
364 }
365 if let Some(name) = tool_name
366 && !name.is_empty()
367 {
368 entry.1 = name.clone();
369 }
370 processed.push(event);
371 }
372 ProviderEvent::ToolCallDelta {
373 content_index,
374 delta,
375 ..
376 } => {
377 let idx = if pending_tc.contains_key(content_index) {
379 *content_index
380 } else {
381 tc_id_to_index
383 .values()
384 .copied()
385 .find(|i| *i == *content_index)
386 .unwrap_or(*content_index)
387 };
388 let entry = pending_tc.entry(idx).or_insert_with(|| {
389 (String::new(), String::new(), String::new())
390 });
391 tracing::debug!(
392 "[TC-DELTA] idx={}, delta_len={}, accumulated_len={}",
393 idx,
394 delta.len(),
395 entry.2.len() + delta.len()
396 );
397 entry.2.push_str(delta);
398 processed.push(event);
399 }
400 ProviderEvent::ToolCallEnd { .. } => {
401 processed.push(event);
403 }
404 ProviderEvent::Done { reason, .. } => {
405 if matches!(reason, StopReason::ToolUse) {
407 let mut indices: Vec<usize> =
408 pending_tc.keys().copied().collect();
409 indices.sort();
410 for idx in indices {
411 let (id, name, arguments) = &pending_tc[&idx];
412 tracing::debug!(
413 "[TC-END] idx={}, id={}, name={}, args_len={}",
414 idx,
415 id.len(),
416 name.len(),
417 arguments.len()
418 );
419 let args_value = parse_streaming_json(arguments);
420 processed.push(ProviderEvent::ToolCallEnd {
421 content_index: idx,
422 tool_call: crate::ToolCall {
423 content_type:
424 crate::messages::ToolCallType::ToolCall,
425 id: id.clone(),
426 name: name.clone(),
427 arguments: args_value,
428 thought_signature: None,
429 },
430 partial: Arc::new(AssistantMessage::new(
431 Api::OpenAiCompletions,
432 &provider_name,
433 &model_id,
434 )),
435 });
436 }
437 }
438 pending_tc.clear();
442 tc_id_to_index.clear();
443 processed.push(event);
444 }
445 _ => {
446 processed.push(event);
447 }
448 }
449 }
450 processed
451 }
452 Err(e) => {
453 vec![ProviderEvent::Error {
454 reason: StopReason::Error,
455 error: create_error_message(
456 &e.to_string(),
457 &provider_name,
458 &model_id,
459 ),
460 }]
461 }
462 };
463 async move { Some(futures::stream::iter(events)) }
465 },
466 )
467 .flatten();
468
469 let stream_with_start = futures::stream::once(async move { start_event }).chain(stream);
471 Ok(Box::pin(stream_with_start) as Pin<Box<dyn Stream<Item = ProviderEvent> + Send>>)
472 })
473 }
474}
475
476fn build_messages_from_normalized(
482 system_prompt: &Option<String>,
483 messages: &[Message],
484) -> Result<Vec<JsonValue>, ProviderError> {
485 let mut result = Vec::new();
486
487 if let Some(prompt) = system_prompt {
489 result.push(serde_json::json!({
490 "role": "system",
491 "content": prompt,
492 }));
493 }
494
495 for msg in messages {
497 match msg {
498 Message::User(u) => {
499 let content: String = match &u.content {
500 MessageContent::Text(s) => s.clone(),
501 MessageContent::Blocks(blocks) => blocks_to_content(blocks)?.to_string(),
502 };
503 result.push(serde_json::json!({
504 "role": "user",
505 "content": content,
506 }));
507 }
508 Message::Assistant(a) => {
509 let mut text_parts = Vec::new();
511 let mut tool_calls = Vec::new();
512 for block in &a.content {
513 match block {
514 ContentBlock::Text(t) => {
515 text_parts.push(t.text.clone());
516 }
517 ContentBlock::Thinking(_) => {
518 }
520 ContentBlock::ToolCall(tc) => {
521 tool_calls.push(serde_json::json!({
522 "id": tc.id,
523 "type": "function",
524 "function": {
525 "name": tc.name,
526 "arguments": tc.arguments.to_string(),
527 },
528 }));
529 }
530 ContentBlock::Image(_) | ContentBlock::Unknown(_) => {}
531 }
532 }
533
534 let mut msg_obj = serde_json::json!({
535 "role": "assistant",
536 "content": text_parts.join(""),
537 });
538 if !tool_calls.is_empty() {
539 msg_obj["tool_calls"] = serde_json::json!(tool_calls);
540 }
541 result.push(msg_obj);
542 }
543 Message::ToolResult(t) => {
544 let result_text: String = t
545 .content
546 .iter()
547 .filter_map(|b| b.as_text())
548 .collect::<Vec<_>>()
549 .join("");
550 result.push(serde_json::json!({
551 "role": "tool",
552 "tool_call_id": t.tool_call_id,
553 "content": result_text,
554 }));
555 }
556 }
557 }
558
559 Ok(result)
560}
561
562fn blocks_to_content(blocks: &[ContentBlock]) -> Result<JsonValue, ProviderError> {
564 if blocks.len() == 1
565 && let Some(text) = blocks[0].as_text()
566 {
567 return Ok(JsonValue::String(text.to_string()));
568 }
569
570 let items: Result<Vec<_>, _> = blocks
571 .iter()
572 .map(|block| match block {
573 ContentBlock::Text(t) => Ok(serde_json::json!({
574 "type": "text",
575 "text": t.text,
576 })),
577 ContentBlock::ToolCall(tc) => Ok(serde_json::json!({
578 "type": "function",
579 "id": tc.id,
580 "function": {
581 "name": tc.name,
582 "arguments": tc.arguments.to_string(),
583 },
584 })),
585 ContentBlock::Thinking(th) => Ok(serde_json::json!({
586 "type": "thinking",
587 "thinking": th.thinking,
588 })),
589 ContentBlock::Image(img) => Ok(serde_json::json!({
590 "type": "image_url",
591 "image_url": {
592 "url": format!("data:{};base64,{}", img.mime_type, img.data),
593 },
594 })),
595 ContentBlock::Unknown(_) => Err(ProviderError::InvalidResponse(
596 "Unknown content block type".into(),
597 )),
598 })
599 .collect();
600
601 Ok(serde_json::json!(items?))
602}
603
604fn build_tool_choice(tool_choice: Option<&crate::tools::ToolChoice>) -> Option<JsonValue> {
606 match tool_choice {
607 None | Some(crate::tools::ToolChoice::Auto) => None,
608 Some(crate::tools::ToolChoice::Named(name)) => {
609 Some(json!({"type": "function", "function": {"name": name}}))
610 }
611 }
612}
613
614fn build_tools(tools: &[crate::Tool]) -> Result<JsonValue, ProviderError> {
616 let items: Vec<_> = tools
617 .iter()
618 .map(|tool| {
619 serde_json::json!({
620 "type": "function",
621 "function": {
622 "name": tool.name,
623 "description": tool.description,
624 "parameters": tool.parameters,
625 },
626 })
627 })
628 .collect();
629
630 Ok(serde_json::json!(items))
631}
632
633fn parse_sse_events(
644 text: &str,
645 _provider: &str,
646 _model_id: &str,
647 output: &mut AssistantMessage,
648) -> Vec<ProviderEvent> {
649 let mut events = Vec::with_capacity(text.len() / 80);
657
658 let mut accumulated_usage = Usage::default();
659
660 for line in text.split('\n') {
661 let line = line.trim_end_matches('\r');
662 if line.is_empty() {
663 continue;
664 }
665
666 if !line.starts_with("data: ") {
668 continue;
669 }
670
671 let data = &line[6..]; if data == "[DONE]" {
675 break;
676 }
677
678 if data.is_empty() {
679 continue;
680 }
681
682 let chunk = match serde_json::from_str::<SSEChunk>(data) {
683 Ok(c) => c,
684 Err(_) => continue,
685 };
686
687 if let Some(chunk_usage) = &chunk.usage {
693 accumulated_usage.input = chunk_usage.prompt_tokens.max(accumulated_usage.input);
694 accumulated_usage.output = chunk_usage.completion_tokens.max(accumulated_usage.output);
695 accumulated_usage.cache_read = chunk_usage
696 .prompt_tokens_details
697 .as_ref()
698 .map(|d| d.cached_tokens)
699 .unwrap_or(0)
700 .max(accumulated_usage.cache_read);
701 accumulated_usage.total_tokens =
702 chunk_usage.total_tokens.max(accumulated_usage.total_tokens);
703 }
704
705 for choice in &chunk.choices {
706 if let Some(delta) = &choice.delta {
707 if let Some(content) = &delta.content {
708 let last_text_idx = output
710 .content
711 .iter()
712 .rposition(|b| matches!(b, ContentBlock::Text(_)));
713 if let Some(idx) = last_text_idx
714 && let ContentBlock::Text(t) = &mut output.content[idx]
715 {
716 t.text.push_str(content);
717 } else {
718 output
719 .content
720 .push(ContentBlock::Text(TextContent::new(content.clone())));
721 }
722 events.push(ProviderEvent::TextDelta {
723 content_index: choice.index,
724 delta: content.clone(),
725 partial: Arc::new(output.clone()),
726 });
727 }
728
729 if let Some(ref reasoning) = delta.reasoning_content
731 && !reasoning.is_empty()
732 {
733 let last_think_idx = output
735 .content
736 .iter()
737 .rposition(|b| matches!(b, ContentBlock::Thinking(_)));
738 if let Some(idx) = last_think_idx
739 && let ContentBlock::Thinking(t) = &mut output.content[idx]
740 {
741 t.thinking.push_str(reasoning);
742 } else {
743 output
744 .content
745 .push(ContentBlock::Thinking(ThinkingContent::new(
746 reasoning.clone(),
747 )));
748 }
749 events.push(ProviderEvent::ThinkingDelta {
750 content_index: choice.index,
751 delta: reasoning.clone(),
752 partial: Arc::new(output.clone()),
753 });
754 }
755
756 if let Some(tool_calls) = &delta.tool_calls {
757 for tc in tool_calls {
758 let tc_index = tc.index.unwrap_or(choice.index);
759
760 if tc.id.is_some()
762 || tc.function.as_ref().and_then(|f| f.name.as_ref()).is_some()
763 {
764 events.push(ProviderEvent::ToolCallStart {
765 content_index: tc_index,
766 tool_call_id: tc.id.clone(),
767 tool_name: tc.function.as_ref().and_then(|f| f.name.clone()),
768 partial: Arc::new(output.clone()),
769 });
770 }
771
772 if let Some(func) = &tc.function {
774 events.push(ProviderEvent::ToolCallDelta {
775 content_index: tc_index,
776 delta: func.arguments.clone().unwrap_or_default(),
777 partial: Arc::new(output.clone()),
778 });
779 }
780 }
781 }
782 }
783
784 if choice.finish_reason.is_some() {
785 let reason = match choice.finish_reason.as_deref() {
786 Some("stop") | Some("end") => StopReason::Stop,
787 Some("length") => StopReason::Length,
788 Some("tool_calls") | Some("function_call") => StopReason::ToolUse,
789 Some("content_filter") => StopReason::Error,
790 Some(unknown) => {
791 tracing::warn!("Unknown finish_reason: '{}', treating as Error", unknown);
792 StopReason::Error
793 }
794 None => StopReason::Stop,
795 };
796 tracing::info!("finish_reason={:?} → {:?}", choice.finish_reason, reason);
797
798 let mut done_msg = output.clone();
799 done_msg.stop_reason = reason;
800 done_msg.usage = accumulated_usage.clone();
801 events.push(ProviderEvent::Done {
802 reason,
803 message: done_msg,
804 });
805 }
806 }
807 }
808
809 events
810}
811
812fn create_error_message(msg: &str, provider: &str, model_id: &str) -> AssistantMessage {
814 let mut message = AssistantMessage::new(Api::OpenAiCompletions, provider, model_id);
815 message.stop_reason = StopReason::Error;
816 message.error_message = Some(msg.to_string());
817 message
818}
819
820#[derive(Debug, Deserialize)]
822struct SSEChunk {
824 _id: Option<String>,
825 #[serde(rename = "model")]
826 _model: Option<String>,
827 choices: Vec<Choice>,
828 usage: Option<UsageInfo>,
829}
830
831#[derive(Debug, Deserialize)]
832struct Choice {
834 index: usize,
835 delta: Option<Delta>,
836 finish_reason: Option<String>,
837}
838
839#[derive(Debug, Deserialize)]
840struct Delta {
841 content: Option<String>,
842 reasoning_content: Option<String>,
843 tool_calls: Option<Vec<ToolCallDelta>>,
844}
845
846#[derive(Debug, Deserialize)]
847struct ToolCallDelta {
849 index: Option<usize>,
850 id: Option<String>,
851 #[serde(rename = "type")]
852 _type_: Option<String>,
853 function: Option<FunctionDelta>,
854}
855
856#[derive(Debug, Deserialize)]
857struct FunctionDelta {
859 name: Option<String>,
860 arguments: Option<String>,
861}
862
863#[derive(Debug, Deserialize, Clone)]
864struct UsageInfo {
865 prompt_tokens: usize,
866 completion_tokens: usize,
867 total_tokens: usize,
868 #[serde(rename = "prompt_tokens_details")]
869 prompt_tokens_details: Option<PromptTokensDetails>,
870}
871
872#[derive(Debug, Deserialize, Clone)]
873struct PromptTokensDetails {
874 #[serde(rename = "cached_tokens")]
875 cached_tokens: usize,
876}
877
878pub fn normalize_messages(messages: &[Message], provider: &str, model_id: &str) -> Vec<Message> {
898 let provider_lower = provider.to_lowercase();
899 let model_lower = model_id.to_lowercase();
900
901 let is_anthropic = provider_lower == "anthropic"
902 || provider_lower.contains("vertex-anthropic")
903 || provider_lower.contains("google-vertex") && model_lower.contains("claude");
904
905 let is_mistral = provider_lower == "mistral"
906 || model_lower.contains("mistral")
907 || model_lower.contains("devstral");
908
909 let is_deepseek = model_lower.contains("deepseek");
910
911 let is_claude = model_lower.contains("claude");
912
913 let needs_tool_id_scrub = is_mistral || is_claude;
914
915 let messages: Vec<Message> = messages.to_vec();
916
917 let messages = if is_deepseek {
919 messages
920 .iter()
921 .map(|msg| match msg {
922 Message::Assistant(a) => {
923 let has_reasoning = a
924 .content
925 .iter()
926 .any(|b| matches!(b, ContentBlock::Thinking(_)));
927 if has_reasoning {
928 Message::Assistant(a.clone())
929 } else {
930 let mut new_content = a.content.clone();
931 new_content
932 .push(ContentBlock::Thinking(ThinkingContent::new(String::new())));
933 let mut new_msg = AssistantMessage::new(a.api, &a.provider, &a.model);
934 new_msg.content = new_content;
935 new_msg.usage = a.usage.clone();
936 new_msg.stop_reason = a.stop_reason;
937 new_msg.response_id = a.response_id.clone();
938 new_msg.timestamp = a.timestamp;
939 Message::Assistant(new_msg)
940 }
941 }
942 _ => msg.clone(),
943 })
944 .collect()
945 } else {
946 messages
947 };
948
949 let messages = if is_anthropic {
951 messages
952 .iter()
953 .filter_map(|msg| match msg {
954 Message::User(u) => {
955 let filtered = filter_empty_content(&u.content);
956 filtered.as_ref()?;
957 #[allow(clippy::expect_used)]
961 Some(Message::User(crate::UserMessage {
962 role: u.role,
963 content: filtered.expect("checked above"),
964 timestamp: u.timestamp,
965 visible: true,
966 }))
967 }
968 Message::Assistant(a) => {
969 let filtered: Vec<ContentBlock> = a
970 .content
971 .iter()
972 .filter(|b| !is_empty_content_block(b))
973 .cloned()
974 .collect();
975 if filtered.is_empty() {
976 return None; }
978 let mut new_msg = AssistantMessage::new(a.api, &a.provider, &a.model);
979 new_msg.content = filtered;
980 new_msg.usage = a.usage.clone();
981 new_msg.stop_reason = a.stop_reason;
982 new_msg.response_id = a.response_id.clone();
983 new_msg.timestamp = a.timestamp;
984 Some(Message::Assistant(new_msg))
985 }
986 Message::ToolResult(t) => Some(Message::ToolResult(t.clone())),
987 })
988 .collect()
989 } else {
990 messages
991 };
992
993 let messages = if is_anthropic {
995 messages
996 .iter()
997 .flat_map(|msg| match msg {
998 Message::Assistant(a) => {
999 let parts = &a.content;
1000 let has_tool = parts.iter().any(|b| matches!(b, ContentBlock::ToolCall(_)));
1001 let has_non_tool = parts
1002 .iter()
1003 .any(|b| !matches!(b, ContentBlock::ToolCall(_)));
1004
1005 if has_tool && has_non_tool {
1006 let non_tools: Vec<ContentBlock> = parts
1008 .iter()
1009 .filter(|b| !matches!(b, ContentBlock::ToolCall(_)))
1010 .cloned()
1011 .collect();
1012 let tools: Vec<ContentBlock> = parts
1013 .iter()
1014 .filter(|b| matches!(b, ContentBlock::ToolCall(_)))
1015 .cloned()
1016 .collect();
1017
1018 let mut msg1 = AssistantMessage::new(a.api, &a.provider, &a.model);
1019 msg1.content = non_tools;
1020 msg1.usage = a.usage.clone();
1021 msg1.stop_reason = a.stop_reason;
1022 msg1.response_id = a.response_id.clone();
1023 msg1.timestamp = a.timestamp;
1024
1025 let mut msg2 = AssistantMessage::new(a.api, &a.provider, &a.model);
1026 msg2.content = tools;
1027 msg2.usage = a.usage.clone();
1028 msg2.stop_reason = a.stop_reason;
1029 msg2.response_id = a.response_id.clone();
1030 msg2.timestamp = a.timestamp;
1031
1032 vec![Message::Assistant(msg1), Message::Assistant(msg2)]
1033 } else {
1034 vec![Message::Assistant(a.clone())]
1035 }
1036 }
1037 _ => vec![msg.clone()],
1038 })
1039 .collect()
1040 } else {
1041 messages
1042 };
1043
1044 if needs_tool_id_scrub {
1047 messages
1048 .iter()
1049 .map(|msg| match msg {
1050 Message::Assistant(a) => {
1051 let new_content: Vec<ContentBlock> = a
1052 .content
1053 .iter()
1054 .map(|block| match block {
1055 ContentBlock::ToolCall(tc) => ContentBlock::ToolCall(ToolCall::new(
1056 scrub_tool_id(&tc.id, is_mistral, is_anthropic),
1057 tc.name.clone(),
1058 tc.arguments.clone(),
1059 )),
1060 _ => block.clone(),
1061 })
1062 .collect();
1063 let mut new_msg = AssistantMessage::new(a.api, &a.provider, &a.model);
1064 new_msg.content = new_content;
1065 new_msg.usage = a.usage.clone();
1066 new_msg.stop_reason = a.stop_reason;
1067 new_msg.response_id = a.response_id.clone();
1068 new_msg.timestamp = a.timestamp;
1069 Message::Assistant(new_msg)
1070 }
1071 Message::ToolResult(t) => Message::ToolResult(ToolResultMessage::new(
1072 scrub_tool_id(&t.tool_call_id, is_mistral, is_anthropic),
1073 &t.tool_name,
1074 t.content.clone(),
1075 )),
1076 _ => msg.clone(),
1077 })
1078 .collect()
1079 } else {
1080 messages
1081 }
1082}
1083
1084fn is_empty_content_block(block: &ContentBlock) -> bool {
1086 match block {
1087 ContentBlock::Text(t) => t.text.trim().is_empty(),
1088 ContentBlock::Thinking(t) => t.thinking.trim().is_empty(),
1089 _ => false,
1090 }
1091}
1092
1093fn filter_empty_content(content: &MessageContent) -> Option<MessageContent> {
1096 match content {
1097 MessageContent::Text(s) => {
1098 if s.trim().is_empty() {
1099 None
1100 } else {
1101 Some(MessageContent::Text(s.clone()))
1102 }
1103 }
1104 MessageContent::Blocks(blocks) => {
1105 let filtered: Vec<ContentBlock> = blocks
1106 .iter()
1107 .filter(|b| !is_empty_content_block(b))
1108 .cloned()
1109 .collect();
1110 if filtered.is_empty() {
1111 None
1112 } else {
1113 Some(MessageContent::Blocks(filtered))
1114 }
1115 }
1116 }
1117}
1118
1119fn scrub_tool_id(id: &str, is_mistral: bool, is_anthropic: bool) -> String {
1123 if is_mistral {
1124 let alphanumeric: String = id.chars().filter(|c| c.is_alphanumeric()).take(9).collect();
1125 if alphanumeric.len() < 9 {
1126 format!("{}{}", alphanumeric, "0".repeat(9 - alphanumeric.len()))
1127 } else {
1128 alphanumeric
1129 }
1130 } else if is_anthropic {
1131 id.chars()
1134 .map(|c| {
1135 if c.is_alphanumeric() || c == '_' || c == '-' {
1136 c
1137 } else {
1138 '_'
1139 }
1140 })
1141 .take(64)
1142 .collect()
1143 } else {
1144 id.chars()
1145 .filter(|c| c.is_alphanumeric() || *c == '_')
1146 .collect()
1147 }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152 use super::*;
1153
1154 #[test]
1155 fn build_tool_choice_maps_named_to_function_shape() {
1156 assert!(build_tool_choice(None).is_none());
1157 assert!(build_tool_choice(Some(&crate::tools::ToolChoice::Auto)).is_none());
1158 assert_eq!(
1159 build_tool_choice(Some(&crate::tools::ToolChoice::Named("todo".into()))),
1160 Some(serde_json::json!({"type": "function", "function": {"name": "todo"}}))
1161 );
1162 }
1163
1164 const PROVIDER: &str = "openai";
1165 const MODEL: &str = "gpt-4o";
1166
1167 fn parse_sse(sse: &str) -> Vec<ProviderEvent> {
1168 let mut output = AssistantMessage::new(Api::OpenAiCompletions, PROVIDER, MODEL);
1169 parse_sse_events(sse, PROVIDER, MODEL, &mut output)
1170 }
1171
1172 #[test]
1175 fn parse_single_text_event() {
1176 let sse = "data: {\"id\":\"chatcmpl-1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"}}]}\n\n";
1177 let events = parse_sse(sse);
1178 assert_eq!(events.len(), 1);
1179 match &events[0] {
1180 ProviderEvent::TextDelta {
1181 delta,
1182 content_index,
1183 ..
1184 } => {
1185 assert_eq!(delta, "Hello");
1186 assert_eq!(*content_index, 0);
1187 }
1188 other => panic!("expected TextDelta, got {other:?}"),
1189 }
1190 }
1191
1192 #[test]
1193 fn parse_multiple_text_events() {
1194 let sse = concat!(
1195 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hel\"}}]}\n",
1196 "\n",
1197 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"lo!\"}}]}\n",
1198 "\n"
1199 );
1200 let events = parse_sse(sse);
1201 assert_eq!(events.len(), 2);
1202 let texts: Vec<&str> = events
1203 .iter()
1204 .filter_map(|e| match e {
1205 ProviderEvent::TextDelta { delta, .. } => Some(delta.as_str()),
1206 _ => None,
1207 })
1208 .collect();
1209 assert_eq!(texts, vec!["Hel", "lo!"]);
1210 }
1211
1212 #[test]
1213 fn parse_done_terminator() {
1214 let sse = concat!(
1215 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"X\"}}]}\n",
1216 "\n",
1217 "data: [DONE]\n",
1218 "\n",
1219 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"NEVER\"}}]}\n"
1220 );
1221 let events = parse_sse(sse);
1222 assert_eq!(events.len(), 1);
1224 match &events[0] {
1225 ProviderEvent::TextDelta { delta, .. } => assert_eq!(delta, "X"),
1226 other => panic!("expected TextDelta, got {other:?}"),
1227 }
1228 }
1229
1230 #[test]
1233 fn parse_finish_reason_stop() {
1234 let sse = "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"stop\"}]}\n\n";
1235 let events = parse_sse(sse);
1236 assert_eq!(events.len(), 1);
1237 match &events[0] {
1238 ProviderEvent::Done { reason, .. } => assert!(matches!(reason, StopReason::Stop)),
1239 other => panic!("expected Done, got {other:?}"),
1240 }
1241 }
1242
1243 #[test]
1244 fn parse_finish_reason_length() {
1245 let sse = "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"length\"}]}\n\n";
1246 let events = parse_sse(sse);
1247 match &events[0] {
1248 ProviderEvent::Done { reason, .. } => assert!(matches!(reason, StopReason::Length)),
1249 other => panic!("expected Done with Length, got {other:?}"),
1250 }
1251 }
1252
1253 #[test]
1254 fn parse_finish_reason_tool_calls() {
1255 let sse = "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"tool_calls\"}]}\n\n";
1256 let events = parse_sse(sse);
1257 match &events[0] {
1258 ProviderEvent::Done { reason, .. } => assert!(matches!(reason, StopReason::ToolUse)),
1259 other => panic!("expected Done with ToolUse, got {other:?}"),
1260 }
1261 }
1262
1263 #[test]
1266 fn parse_tool_call_deltas() {
1267 let sse = concat!(
1268 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]}}]}\n",
1269 "\n",
1270 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"city\\\":\\\"SF\\\"}\"}}]}}]}\n",
1271 "\n"
1272 );
1273 let events = parse_sse(sse);
1274 assert_eq!(events.len(), 3);
1277 let starts: Vec<&str> = events
1278 .iter()
1279 .filter_map(|e| match e {
1280 ProviderEvent::ToolCallStart { tool_name, .. } => tool_name.as_deref(),
1281 _ => None,
1282 })
1283 .collect();
1284 assert_eq!(starts, vec!["get_weather"]);
1285 let deltas: Vec<&str> = events
1286 .iter()
1287 .filter_map(|e| match e {
1288 ProviderEvent::ToolCallDelta { delta, .. } => Some(delta.as_str()),
1289 _ => None,
1290 })
1291 .collect();
1292 assert_eq!(deltas, vec!["", "{\"city\":\"SF\"}"]);
1293 }
1294
1295 #[test]
1296 fn parse_tool_call_with_no_arguments_field() {
1297 let sse = "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"name\":\"run\"}}]}}]}\n\n";
1299 let events = parse_sse(sse);
1300 assert_eq!(events.len(), 2);
1301 match &events[0] {
1302 ProviderEvent::ToolCallStart { tool_name, .. } => {
1303 assert_eq!(tool_name.as_deref(), Some("run"));
1304 }
1305 other => panic!("expected ToolCallStart, got {other:?}"),
1306 }
1307 match &events[1] {
1308 ProviderEvent::ToolCallDelta { delta, .. } => assert_eq!(delta, ""),
1309 other => panic!("expected ToolCallDelta, got {other:?}"),
1310 }
1311 }
1312
1313 #[test]
1316 fn parse_usage_in_chunk() {
1317 let sse = concat!(
1320 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":8,\"total_tokens\":18,\"prompt_tokens_details\":{\"cached_tokens\":3}}}\n",
1321 "\n",
1322 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"stop\"}]}\n"
1323 );
1324 let events = parse_sse(sse);
1325 assert_eq!(events.len(), 2);
1327 match &events[1] {
1328 ProviderEvent::Done { message, .. } => {
1329 assert_eq!(message.usage.input, 10);
1330 assert_eq!(message.usage.output, 8);
1331 assert_eq!(message.usage.total_tokens, 18);
1332 assert_eq!(message.usage.cache_read, 3);
1333 }
1334 other => panic!("expected Done, got {other:?}"),
1335 }
1336 }
1337
1338 #[test]
1339 fn parse_usage_without_cache_details() {
1340 let sse = concat!(
1342 "data: {\"id\":\"c\",\"choices\":[],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":2,\"total_tokens\":7}}\n",
1343 "\n",
1344 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"stop\"}]}\n"
1345 );
1346 let events = parse_sse(sse);
1347 match &events[0] {
1348 ProviderEvent::Done { message, .. } => {
1349 assert_eq!(message.usage.input, 5);
1350 assert_eq!(message.usage.output, 2);
1351 assert_eq!(message.usage.cache_read, 0);
1352 }
1353 other => panic!("expected Done, got {other:?}"),
1354 }
1355 }
1356
1357 #[test]
1360 fn parse_empty_input() {
1361 let events = parse_sse("");
1362 assert!(events.is_empty());
1363 }
1364
1365 #[test]
1366 fn parse_only_empty_lines() {
1367 let events = parse_sse("\n\n\n");
1368 assert!(events.is_empty());
1369 }
1370
1371 #[test]
1372 fn parse_malformed_json_after_data() {
1373 let sse = "data: {not json at all}\ndata: also bad\ndata: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"}}]}\n";
1374 let events = parse_sse(sse);
1375 assert_eq!(events.len(), 1);
1377 match &events[0] {
1378 ProviderEvent::TextDelta { delta, .. } => assert_eq!(delta, "ok"),
1379 other => panic!("expected TextDelta, got {other:?}"),
1380 }
1381 }
1382
1383 #[test]
1384 fn parse_empty_data_line() {
1385 let sse = "data: \ndata: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"X\"}}]}\n";
1386 let events = parse_sse(sse);
1387 assert_eq!(events.len(), 1);
1388 }
1389
1390 #[test]
1391 fn parse_non_data_lines_ignored() {
1392 let sse = "event: ping\nid: 42\nretry: 5000\ndata: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Y\"}}]}\n";
1393 let events = parse_sse(sse);
1394 assert_eq!(events.len(), 1);
1395 }
1396
1397 #[test]
1398 fn parse_carriage_return_line_endings() {
1399 let sse = "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"CR\"}}]}\r\n\r\n";
1400 let events = parse_sse(sse);
1401 assert_eq!(events.len(), 1);
1402 match &events[0] {
1403 ProviderEvent::TextDelta { delta, .. } => assert_eq!(delta, "CR"),
1404 other => panic!("expected TextDelta, got {other:?}"),
1405 }
1406 }
1407
1408 #[test]
1411 fn parse_full_stream_with_text_tool_and_done() {
1412 let sse = concat!(
1413 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Let me\"}}]}\n",
1414 "\n",
1415 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" check\"}}]}\n",
1416 "\n",
1417 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"search\",\"arguments\":\"{\\\"q\\\":\\\"rust\\\"}\"}}]}}]}\n",
1418 "\n",
1419 "data: {\"id\":\"c\",\"choices\":[{\"index\":0,\"delta\":null,\"finish_reason\":\"tool_calls\"}]}\n",
1420 "\n",
1421 "data: [DONE]\n"
1422 );
1423 let events = parse_sse(sse);
1424 assert_eq!(events.len(), 5); let mut text_count = 0;
1427 let mut tc_start_count = 0;
1428 let mut tc_delta_count = 0;
1429 let mut done_count = 0;
1430 for e in &events {
1431 match e {
1432 ProviderEvent::TextDelta { .. } => text_count += 1,
1433 ProviderEvent::ToolCallStart { .. } => tc_start_count += 1,
1434 ProviderEvent::ToolCallDelta { .. } => tc_delta_count += 1,
1435 ProviderEvent::Done { reason, .. } => {
1436 done_count += 1;
1437 assert!(matches!(reason, StopReason::ToolUse));
1438 }
1439 other => panic!("unexpected event: {other:?}"),
1440 }
1441 }
1442 assert_eq!(text_count, 2);
1443 assert_eq!(tc_start_count, 1);
1444 assert_eq!(tc_delta_count, 1);
1445 assert_eq!(done_count, 1);
1446 }
1447}