1use bytes::Bytes;
26use futures::StreamExt;
27use futures::future::BoxFuture;
28
29use crate::completion::{CompletionRequest, CompletionResponse};
30use crate::error::ProviderError;
31use crate::{
32 completion::FinishReason,
33 error::ErrorReport,
34 http_client,
35 message::AssistantContent,
36 streaming::{Item, StreamEvent},
37};
38
39#[derive(Debug, thiserror::Error)]
41pub enum ConformanceError {
42 #[error(transparent)]
44 Completion(#[from] ProviderError),
45 #[error("{scenario} conformance failed for {provider}: {details}")]
47 Contract {
48 scenario: &'static str,
50 provider: &'static str,
52 details: String,
54 },
55}
56
57impl ConformanceError {
58 fn contract(
59 scenario: &'static str,
60 provider: &'static str,
61 details: impl Into<String>,
62 ) -> Self {
63 Self::Contract {
64 scenario,
65 provider,
66 details: details.into(),
67 }
68 }
69}
70
71#[derive(Debug)]
73pub struct ScenarioReport {
74 pub name: &'static str,
76 pub provider: &'static str,
78 pub observations: Vec<String>,
80}
81
82#[derive(Debug)]
91pub enum ScenarioOutcome {
92 Ran(ScenarioReport),
94 Skipped {
96 name: &'static str,
98 provider: &'static str,
100 reason: &'static str,
102 },
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub struct SuiteCapabilities {
114 pub partial_tool_args: bool,
117 pub zero_usage_terminal: bool,
120 pub bare_terminal: bool,
122 pub malformed_frame: bool,
124 pub unknown_event_frame: bool,
126 pub defective_known_frame: bool,
129 pub delta_less_prelude: bool,
132 pub refusal: bool,
134 pub interleaved_reasoning: bool,
138}
139
140impl SuiteCapabilities {
141 pub fn from_names(names: &[&str]) -> Result<Self, String> {
146 let mut caps = Self::default();
147 for name in names {
148 match *name {
149 "partial_tool_args" => caps.partial_tool_args = true,
150 "zero_usage_terminal" => caps.zero_usage_terminal = true,
151 "bare_terminal" => caps.bare_terminal = true,
152 "malformed_frame" => caps.malformed_frame = true,
153 "unknown_event_frame" => caps.unknown_event_frame = true,
154 "defective_known_frame" => caps.defective_known_frame = true,
155 "delta_less_prelude" => caps.delta_less_prelude = true,
156 "refusal" => caps.refusal = true,
157 "interleaved_reasoning" => caps.interleaved_reasoning = true,
158 other => {
159 return Err(format!(
160 "unknown capability name in suite manifest: {other}"
161 ));
162 }
163 }
164 }
165 Ok(caps)
166 }
167}
168
169pub const CANONICAL_SCENARIOS: &[&str] = &[
173 "truncation_preserves_content_without_terminal",
174 "transport_error_after_tool_call_yields_err_then_end",
175 "malformed_frame_ends_the_reply",
176 "unknown_event_is_skipped",
177 "defective_known_event_ends_the_reply",
178 "delta_less_choice_prelude_is_a_noop",
179 "refusal_frames_deliver_text_without_error",
180 "bare_terminal_after_only_unparseable_frames_fabricates_nothing",
181 "usage_variants_are_reported_or_absent",
182 "interleaved_constant_id_reasoning_preserves_order",
183];
184
185pub const WIRE_FAMILIES: &[&str] = &[
190 "openai_chat",
191 "openai_responses",
192 "openai_responses_websocket",
193 "chatgpt",
194 "anthropic",
195 "gemini_rest",
196 "gemini_interactions",
197 "gemini_grpc",
198 "xai",
199 "copilot",
200 "bedrock",
201 "candle",
202];
203
204pub fn xfail_reason<'a>(xfail: &[&'a str], scenario: &str) -> Option<&'a str> {
207 xfail.iter().find_map(|entry| {
208 let (name, reason) = entry.split_once(':')?;
209 (name.trim() == scenario).then(|| reason.trim())
210 })
211}
212
213pub fn invalid_xfail_entries(xfail: &[&str]) -> Vec<String> {
215 xfail
216 .iter()
217 .filter(|entry| match entry.split_once(':') {
218 Some((name, reason)) => {
219 !CANONICAL_SCENARIOS.contains(&name.trim()) || reason.trim().is_empty()
220 }
221 None => true,
222 })
223 .map(std::string::ToString::to_string)
224 .collect()
225}
226
227pub fn check_gated_outcome(
235 scenario: &'static str,
236 capability: bool,
237 xfail: &[&str],
238 outcome: Result<ScenarioOutcome, ConformanceError>,
239) -> Result<(), String> {
240 match (xfail_reason(xfail, scenario), outcome) {
241 (Some(reason), Err(error)) => {
242 eprintln!("xfail {scenario}: {reason} ({error})");
243 Ok(())
244 }
245 (Some(reason), Ok(_)) => Err(format!(
246 "{scenario} passed but is listed as xfail ({reason}); remove the xfail entry"
247 )),
248 (None, Err(error)) => Err(format!("{scenario} failed: {error}")),
249 (None, Ok(ScenarioOutcome::Ran(_))) => {
250 if capability {
251 Ok(())
252 } else {
253 Err(format!(
254 "{scenario} ran but the suite disclaims the capability; set the flag to true"
255 ))
256 }
257 }
258 (None, Ok(ScenarioOutcome::Skipped { reason, .. })) => {
259 if capability {
260 Err(format!(
261 "{scenario} skipped ({reason}) but the suite declares the capability; \
262 a declared capability's scenario must run"
263 ))
264 } else {
265 eprintln!("skipped {scenario}: {reason}");
266 Ok(())
267 }
268 }
269 }
270}
271
272pub fn check_ungated_outcome(
274 scenario: &'static str,
275 xfail: &[&str],
276 result: Result<ScenarioReport, ConformanceError>,
277) -> Result<(), String> {
278 check_gated_outcome(scenario, true, xfail, result.map(ScenarioOutcome::Ran))
280}
281
282#[derive(Clone)]
289pub enum WireInput {
290 Bytes(Bytes),
292 Event(std::sync::Arc<dyn std::any::Any + Send + Sync>),
294}
295
296impl WireInput {
297 pub fn as_bytes(&self) -> Option<&Bytes> {
299 match self {
300 Self::Bytes(bytes) => Some(bytes),
301 Self::Event(_) => None,
302 }
303 }
304
305 pub fn downcast_event<T: 'static>(&self) -> Option<&T> {
307 match self {
308 Self::Bytes(_) => None,
309 Self::Event(event) => event.downcast_ref(),
310 }
311 }
312}
313
314impl From<Bytes> for WireInput {
315 fn from(bytes: Bytes) -> Self {
316 Self::Bytes(bytes)
317 }
318}
319
320impl std::fmt::Debug for WireInput {
321 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 match self {
323 Self::Bytes(bytes) => formatter.debug_tuple("Bytes").field(bytes).finish(),
324 Self::Event(_) => formatter.write_str("Event(..)"),
325 }
326 }
327}
328
329pub fn event_frame<T: Send + Sync + 'static>(event: T) -> WireInput {
331 WireInput::Event(std::sync::Arc::new(event))
332}
333
334pub type WireChunks = Vec<http_client::Result<WireInput>>;
337
338pub fn ok_chunks(frames: impl IntoIterator<Item = impl Into<WireInput>>) -> WireChunks {
340 frames.into_iter().map(|frame| Ok(frame.into())).collect()
341}
342
343pub fn transport_error_chunk() -> http_client::Result<WireInput> {
346 Err(http_client::Error::instance(std::io::Error::new(
347 std::io::ErrorKind::ConnectionReset,
348 "connection reset",
349 )))
350}
351
352pub fn assert_valid_event_stream(
377 items: &[Result<Item<StreamEvent>, ErrorReport>],
378 choice: &[AssistantContent],
379) {
380 use crate::message::AssistantContent;
381
382 if let Some(error_index) = items.iter().position(Result::is_err) {
384 assert_eq!(
385 error_index + 1,
386 items.len(),
387 "law 1 (terminal error): an item followed the stream's error"
388 );
389 }
390 let events: Vec<&StreamEvent> = items
391 .iter()
392 .filter_map(|item| match item {
393 Ok(Item::Event(event)) => Some(event),
394 _ => None,
395 })
396 .collect();
397
398 let streamed_text: String = events
400 .iter()
401 .filter_map(|event| match event {
402 StreamEvent::Text { text, .. } => Some(text.as_str()),
403 _ => None,
404 })
405 .collect();
406 let aggregated_text: String = choice
407 .iter()
408 .filter_map(|content| match content {
409 AssistantContent::Text(text) => Some(text.text.as_str()),
410 _ => None,
411 })
412 .collect();
413 assert_eq!(
414 aggregated_text, streamed_text,
415 "law 2 (text conservation): aggregated text differs from the streamed fragments"
416 );
417
418 let yielded_calls = events
420 .iter()
421 .filter(|event| {
422 matches!(
423 event,
424 StreamEvent::End {
425 content: AssistantContent::ToolCall(_),
426 ..
427 }
428 )
429 })
430 .count();
431 let aggregated_calls = choice
432 .iter()
433 .filter(|content| matches!(content, AssistantContent::ToolCall(_)))
434 .count();
435 assert_eq!(
436 aggregated_calls, yielded_calls,
437 "law 3 (completed-call conservation): {yielded_calls} calls yielded, \
438 {aggregated_calls} aggregated"
439 );
440
441 let serialized = serde_json::to_value(
443 items
444 .iter()
445 .filter_map(|item| item.as_ref().ok())
446 .collect::<Vec<_>>(),
447 )
448 .unwrap_or_default();
449 let read_back = crate::streaming::StreamRecord::parse_prefix(serialized);
450 assert!(
451 read_back.is_ok(),
452 "law 4 (sequence): the stream's events do not read back: {read_back:?}"
453 );
454
455 let yielded_reasoning = events.iter().any(|event| {
457 matches!(
458 event,
459 StreamEvent::End {
460 content: AssistantContent::Reasoning(_),
461 ..
462 }
463 )
464 });
465 let aggregated_reasoning = choice
466 .iter()
467 .any(|content| matches!(content, AssistantContent::Reasoning(_)));
468 assert!(
469 yielded_reasoning || !aggregated_reasoning,
470 "law 5 (reasoning provenance): aggregated reasoning with no reasoning yielded"
471 );
472}
473
474#[derive(Debug)]
477pub struct DrainedStream {
478 pub items: Vec<Result<Item<StreamEvent>, ErrorReport>>,
480 pub choice: Vec<AssistantContent>,
483 pub response: Option<CompletionResponse>,
485}
486
487impl DrainedStream {
488 fn events(&self) -> impl Iterator<Item = &StreamEvent> {
489 self.items.iter().filter_map(|item| match item {
490 Ok(Item::Event(event)) => Some(event),
491 _ => None,
492 })
493 }
494
495 pub fn texts(&self) -> Vec<&str> {
497 self.events()
498 .filter_map(|event| match event {
499 StreamEvent::Text { text, .. } => Some(text.as_str()),
500 _ => None,
501 })
502 .collect()
503 }
504
505 pub fn tool_call_names(&self) -> Vec<&str> {
507 self.events()
508 .filter_map(|event| match event {
509 StreamEvent::End {
510 content: AssistantContent::ToolCall(tool_call),
511 ..
512 } => Some(tool_call.function.name.as_str()),
513 _ => None,
514 })
515 .collect()
516 }
517
518 pub fn unknown_values(&self) -> Vec<&serde_json::Value> {
520 self.items
521 .iter()
522 .filter_map(|item| match item {
523 Ok(Item::Unknown(value)) => Some(value.value()),
524 _ => None,
525 })
526 .collect()
527 }
528
529 pub fn error_count(&self) -> usize {
531 self.items.iter().filter(|item| item.is_err()).count()
532 }
533
534 fn has_terminal(&self) -> bool {
536 self.response.is_some()
537 }
538
539 fn completed_cleanly(&self) -> bool {
542 self.error_count() == 0 && self.response.is_some()
543 }
544
545 fn first_error_index(&self) -> Option<usize> {
547 self.items.iter().position(std::result::Result::is_err)
548 }
549
550 pub fn choice_texts(&self) -> Vec<&str> {
552 self.choice
553 .iter()
554 .filter_map(|content| match content {
555 AssistantContent::Text(text) => Some(text.text.as_str()),
556 _ => None,
557 })
558 .collect()
559 }
560
561 pub fn choice_reasoning(&self) -> Vec<&crate::message::Reasoning> {
563 self.choice
564 .iter()
565 .filter_map(|content| match content {
566 AssistantContent::Reasoning(reasoning) => Some(reasoning),
567 _ => None,
568 })
569 .collect()
570 }
571}
572
573type DriveFn = Box<
574 dyn Fn(WireChunks) -> BoxFuture<'static, Result<DrainedStream, ProviderError>> + Send + Sync,
575>;
576
577pub struct WireDriver {
583 pub provider: &'static str,
585 drive: DriveFn,
586}
587
588impl WireDriver {
589 pub fn new(
591 provider: &'static str,
592 drive: impl Fn(WireChunks) -> BoxFuture<'static, Result<DrainedStream, ProviderError>>
593 + Send
594 + Sync
595 + 'static,
596 ) -> Self {
597 Self {
598 provider,
599 drive: Box::new(drive),
600 }
601 }
602
603 pub async fn drive(&self, chunks: WireChunks) -> Result<DrainedStream, ProviderError> {
605 (self.drive)(chunks).await
606 }
607}
608
609pub struct RefusalFixture {
611 pub frames: Vec<WireInput>,
613 pub expected_text: &'static str,
615}
616
617pub struct InterleavedReasoningFixture {
622 pub frames: Vec<WireInput>,
624 pub first_reasoning: &'static str,
626 pub tool_name: &'static str,
628 pub second_reasoning: &'static str,
630}
631
632type BufferedDriveFn = Box<
633 dyn Fn(String) -> BoxFuture<'static, Result<Vec<AssistantContent>, ProviderError>>
634 + Send
635 + Sync,
636>;
637
638pub struct BufferedBodyDriver {
641 pub provider: &'static str,
643 drive: BufferedDriveFn,
644}
645
646impl BufferedBodyDriver {
647 pub fn new(
649 provider: &'static str,
650 drive: impl Fn(String) -> BoxFuture<'static, Result<Vec<AssistantContent>, ProviderError>>
651 + Send
652 + Sync
653 + 'static,
654 ) -> Self {
655 Self {
656 provider,
657 drive: Box::new(drive),
658 }
659 }
660
661 pub async fn drive(&self, body: String) -> Result<Vec<AssistantContent>, ProviderError> {
663 (self.drive)(body).await
664 }
665}
666
667pub struct ProviderWireFixture {
672 pub driver: WireDriver,
674 pub text_frames: Vec<WireInput>,
676 pub expected_texts: Vec<&'static str>,
678 pub tool_call_frames: Vec<WireInput>,
681 pub expected_tool_name: &'static str,
683 pub partial_tool_call_frames: Option<Vec<WireInput>>,
686 pub terminal_frames: Vec<WireInput>,
688 pub expected_usage_total: u64,
690 pub expected_finish_reason: Option<FinishReason>,
692 pub zero_usage_terminal_frames: Option<Vec<WireInput>>,
694 pub bare_terminal_frames: Option<Vec<WireInput>>,
697 pub malformed_frame: Option<WireInput>,
701 pub unknown_event_frame: Option<WireInput>,
703 pub defective_known_frame: Option<WireInput>,
705 pub delta_less_prelude_frame: Option<WireInput>,
707 pub refusal: Option<RefusalFixture>,
709 pub interleaved_reasoning: Option<InterleavedReasoningFixture>,
713}
714
715impl ProviderWireFixture {
716 pub fn capabilities(&self) -> SuiteCapabilities {
724 SuiteCapabilities {
725 partial_tool_args: self.partial_tool_call_frames.is_some(),
726 zero_usage_terminal: self.zero_usage_terminal_frames.is_some(),
727 bare_terminal: self.bare_terminal_frames.is_some(),
728 malformed_frame: self.malformed_frame.is_some(),
729 unknown_event_frame: self.unknown_event_frame.is_some(),
730 defective_known_frame: self.defective_known_frame.is_some(),
731 delta_less_prelude: self.delta_less_prelude_frame.is_some(),
732 refusal: self.refusal.is_some(),
733 interleaved_reasoning: self.interleaved_reasoning.is_some(),
734 }
735 }
736}
737
738fn concat_frames(parts: &[&[WireInput]]) -> Vec<WireInput> {
739 parts
740 .iter()
741 .flat_map(|frames| frames.iter().cloned())
742 .collect()
743}
744
745struct Checks {
752 name: &'static str,
753 provider: &'static str,
754 observations: Vec<String>,
755}
756
757impl Checks {
758 fn new(name: &'static str, provider: &'static str) -> Self {
759 Self {
760 name,
761 provider,
762 observations: Vec::new(),
763 }
764 }
765
766 fn fail(&self, details: impl Into<String>) -> ConformanceError {
768 ConformanceError::contract(self.name, self.provider, details)
769 }
770
771 fn require<D: Into<String>>(
774 &self,
775 held: bool,
776 details: impl FnOnce() -> D,
777 ) -> Result<(), ConformanceError> {
778 if held {
779 return Ok(());
780 }
781 Err(self.fail(details()))
782 }
783
784 fn note(&mut self, observation: impl Into<String>) {
786 self.observations.push(observation.into());
787 }
788
789 fn skip(&self, reason: &'static str) -> ScenarioOutcome {
793 ScenarioOutcome::Skipped {
794 name: self.name,
795 provider: self.provider,
796 reason,
797 }
798 }
799
800 fn report(self) -> ScenarioReport {
801 ScenarioReport {
802 name: self.name,
803 provider: self.provider,
804 observations: self.observations,
805 }
806 }
807
808 fn ran(self) -> ScenarioOutcome {
809 ScenarioOutcome::Ran(self.report())
810 }
811}
812
813pub async fn truncation_preserves_content_without_terminal(
821 fixture: &ProviderWireFixture,
822) -> Result<ScenarioReport, ConformanceError> {
823 let mut checks = Checks::new(
824 "truncation_preserves_content_without_terminal",
825 fixture.driver.provider,
826 );
827
828 let drained = fixture.driver.drive(Vec::new()).await?;
830 checks.require(
831 !drained.has_terminal(),
832 || "an empty stream must not synthesize a terminal record",
833 )?;
834 checks.note("EOF before content: no terminal");
835
836 let drained = fixture
838 .driver
839 .drive(ok_chunks(fixture.text_frames.clone()))
840 .await?;
841 checks.require(drained.texts() == fixture.expected_texts, || {
842 format!(
843 "text delivered before truncation must be preserved: expected {:?}, observed {:?}",
844 fixture.expected_texts,
845 drained.texts()
846 )
847 })?;
848 checks.require(
849 !drained.has_terminal(),
850 || "EOF after text deltas must not synthesize a terminal record",
851 )?;
852 checks.note("EOF mid-text: content preserved, no terminal");
853
854 if let Some(partial) = &fixture.partial_tool_call_frames {
856 let drained = fixture.driver.drive(ok_chunks(partial.clone())).await?;
857 checks.require(
858 !drained.has_terminal(),
859 || "EOF mid-tool-arguments must not synthesize a terminal record",
860 )?;
861 checks.note("EOF mid-tool-args: no terminal");
862 }
863
864 let drained = fixture
868 .driver
869 .drive(ok_chunks(fixture.tool_call_frames.clone()))
870 .await?;
871 checks.require(
872 drained
873 .tool_call_names()
874 .iter()
875 .all(|name| *name == fixture.expected_tool_name),
876 || {
877 format!(
878 "only the delivered call may surface: observed {:?}",
879 drained.tool_call_names()
880 )
881 },
882 )?;
883 checks.require(
884 !drained.has_terminal(),
885 || "EOF after a tool call must not synthesize a terminal record",
886 )?;
887 checks.note("EOF after a tool call: no terminal");
888
889 Ok(checks.report())
890}
891
892pub async fn transport_error_after_tool_call_yields_err_then_end(
896 fixture: &ProviderWireFixture,
897) -> Result<ScenarioReport, ConformanceError> {
898 let mut checks = Checks::new(
899 "transport_error_after_tool_call_yields_err_then_end",
900 fixture.driver.provider,
901 );
902
903 let mut chunks = ok_chunks(fixture.tool_call_frames.clone());
904 chunks.push(transport_error_chunk());
905 let drained = fixture.driver.drive(chunks).await?;
906
907 checks.require(
908 drained
909 .tool_call_names()
910 .iter()
911 .all(|name| *name == fixture.expected_tool_name),
912 || {
913 format!(
914 "only the delivered call may precede the transport error: observed {:?}",
915 drained.tool_call_names()
916 )
917 },
918 )?;
919 let error_index = drained
920 .first_error_index()
921 .ok_or_else(|| checks.fail("the transport failure must reach the consumer"))?;
922 checks.require(
923 error_index + 1 == drained.items.len(),
924 || "nothing may follow the terminal transport error",
925 )?;
926 checks.require(
927 !drained.has_terminal(),
928 || "a transport failure must not be papered over with a terminal record",
929 )?;
930
931 checks.note("Err, then end; no terminal");
932 Ok(checks.report())
933}
934
935pub async fn malformed_frame_ends_the_reply(
939 fixture: &ProviderWireFixture,
940) -> Result<ScenarioOutcome, ConformanceError> {
941 let mut checks = Checks::new("malformed_frame_ends_the_reply", fixture.driver.provider);
942 let Some(malformed) = &fixture.malformed_frame else {
943 return Ok(checks.skip("wire family cannot spell a frame-level decode failure"));
944 };
945
946 let frames = concat_frames(&[
947 &fixture.text_frames,
948 std::slice::from_ref(malformed),
949 &fixture.terminal_frames,
950 ]);
951 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
952
953 checks.require(drained.error_count() == 1, || {
954 format!(
955 "the malformed frame must surface as exactly one Err item, observed {}",
956 drained.error_count()
957 )
958 })?;
959 checks.require(
960 drained.first_error_index() == Some(drained.items.len() - 1),
961 || "the malformed frame's error must be the stream's last item",
962 )?;
963 checks.require(
964 drained.texts() == fixture.expected_texts,
965 || "content before the malformed frame must be preserved",
966 )?;
967 checks.require(
968 !drained.has_terminal(),
969 || "a reply cut by a corrupt frame has no response",
970 )?;
971
972 checks.note("Err surfaced last; content kept; no response");
973 Ok(checks.ran())
974}
975
976pub async fn unknown_event_is_skipped(
982 fixture: &ProviderWireFixture,
983) -> Result<ScenarioOutcome, ConformanceError> {
984 let mut checks = Checks::new("unknown_event_is_skipped", fixture.driver.provider);
985 let Some(unknown) = &fixture.unknown_event_frame else {
986 return Ok(checks.skip("wire family cannot spell an unknown event type"));
987 };
988
989 let frames = concat_frames(&[
990 &fixture.text_frames,
991 std::slice::from_ref(unknown),
992 &fixture.terminal_frames,
993 ]);
994 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
995
996 checks.require(
997 drained.error_count() == 0,
998 || "an unknown event type must be skipped, not surfaced as an error",
999 )?;
1000 checks.require(
1001 drained.texts() == fixture.expected_texts && drained.response.is_some(),
1002 || "the stream must deliver its content and complete around the skipped event",
1003 )?;
1004 checks.require(drained.unknown_values().len() == 1, || {
1007 format!(
1008 "exactly one Unknown passthrough item must surface for the unknown frame, \
1009 observed {}",
1010 drained.unknown_values().len()
1011 )
1012 })?;
1013
1014 let control_frames = concat_frames(&[&fixture.text_frames, &fixture.terminal_frames]);
1017 let control = fixture.driver.drive(ok_chunks(control_frames)).await?;
1018 checks.require(
1019 drained.choice == control.choice,
1020 || "the unknown frame must not perturb the aggregated assistant choice",
1021 )?;
1022
1023 checks.note(
1024 "unknown event skipped semantically, surfaced on the raw channel, \
1025 choice unchanged, stream completed",
1026 );
1027 Ok(checks.ran())
1028}
1029
1030pub async fn defective_known_event_ends_the_reply(
1036 fixture: &ProviderWireFixture,
1037) -> Result<ScenarioOutcome, ConformanceError> {
1038 let mut checks = Checks::new(
1039 "defective_known_event_ends_the_reply",
1040 fixture.driver.provider,
1041 );
1042 let Some(defective) = &fixture.defective_known_frame else {
1043 return Ok(
1044 checks.skip("wire family cannot spell a known event with a schema-defective payload")
1045 );
1046 };
1047
1048 let frames = concat_frames(&[
1049 &fixture.text_frames,
1050 std::slice::from_ref(defective),
1051 &fixture.terminal_frames,
1052 ]);
1053 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1054
1055 checks.require(drained.error_count() == 1, || {
1056 format!(
1057 "a known event with a schema defect must surface exactly one Err item, observed {}",
1058 drained.error_count()
1059 )
1060 })?;
1061 checks.require(
1062 !drained.has_terminal(),
1063 || "a reply cut by a defective event has no response",
1064 )?;
1065
1066 checks.note("defective known event ended the reply with its Err");
1067 Ok(checks.ran())
1068}
1069
1070pub async fn delta_less_choice_prelude_is_a_noop(
1076 fixture: &ProviderWireFixture,
1077) -> Result<ScenarioOutcome, ConformanceError> {
1078 let mut checks = Checks::new(
1079 "delta_less_choice_prelude_is_a_noop",
1080 fixture.driver.provider,
1081 );
1082 let Some(prelude) = &fixture.delta_less_prelude_frame else {
1083 return Ok(checks.skip("wire family has no delta-less prelude shape"));
1084 };
1085
1086 let frames = concat_frames(&[
1087 std::slice::from_ref(prelude),
1088 &fixture.text_frames,
1089 &fixture.terminal_frames,
1090 ]);
1091 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1092
1093 checks.require(
1094 drained.error_count() == 0,
1095 || "the delta-less prelude must not surface an error",
1096 )?;
1097 checks.require(
1098 drained.texts() == fixture.expected_texts && drained.response.is_some(),
1099 || "the prelude must not perturb content delivery or the terminal",
1100 )?;
1101
1102 checks.note("delta-less prelude ignored; stream unaffected");
1103 Ok(checks.ran())
1104}
1105
1106pub async fn refusal_frames_deliver_text_without_error(
1111 fixture: &ProviderWireFixture,
1112) -> Result<ScenarioOutcome, ConformanceError> {
1113 let mut checks = Checks::new(
1114 "refusal_frames_deliver_text_without_error",
1115 fixture.driver.provider,
1116 );
1117 let Some(refusal) = &fixture.refusal else {
1118 return Ok(checks.skip("wire family has no refusal channel"));
1119 };
1120
1121 let frames = concat_frames(&[&refusal.frames, &fixture.terminal_frames]);
1122 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1123
1124 checks.require(
1125 drained.error_count() == 0,
1126 || "refusal content must not surface as an error",
1127 )?;
1128 let delivered = drained.texts().concat();
1129 checks.require(delivered == refusal.expected_text, || {
1130 format!(
1131 "refusal text must be delivered: expected {:?}, observed {delivered:?}",
1132 refusal.expected_text
1133 )
1134 })?;
1135 checks.require(
1136 drained.response.is_some(),
1137 || "a refused turn still ends with the provider's genuine terminal",
1138 )?;
1139
1140 checks.note("refusal text delivered without error");
1141 Ok(checks.ran())
1142}
1143
1144pub async fn terminal_body_content_merges_per_kind(
1153 driver: &BufferedBodyDriver,
1154 cases: Vec<(&'static str, String)>,
1155 expected_text: &str,
1156) -> Result<ScenarioReport, ConformanceError> {
1157 let mut checks = Checks::new("terminal_body_content_merges_per_kind", driver.provider);
1158
1159 for (label, body) in cases {
1160 let choice = driver.drive(body).await?;
1161 let choice_text: String = choice
1162 .iter()
1163 .filter_map(|content| match content {
1164 AssistantContent::Text(text) => Some(text.text.as_str()),
1165 _ => None,
1166 })
1167 .collect();
1168 let occurrences = choice_text.matches(expected_text).count();
1169 checks.require(occurrences == 1, || {
1170 format!(
1171 "{label}: terminal-body text must appear exactly once in the choice, observed {occurrences} in {choice_text:?}"
1172 )
1173 })?;
1174 checks.note(format!("{label}: text merged exactly once"));
1175 }
1176
1177 Ok(checks.report())
1178}
1179
1180pub async fn bare_terminal_after_only_unparseable_frames_fabricates_nothing(
1187 fixture: &ProviderWireFixture,
1188) -> Result<ScenarioOutcome, ConformanceError> {
1189 let mut checks = Checks::new(
1190 "bare_terminal_after_only_unparseable_frames_fabricates_nothing",
1191 fixture.driver.provider,
1192 );
1193 let Some(bare_terminal) = &fixture.bare_terminal_frames else {
1194 return Ok(checks.skip("wire family has no data-less terminal signal"));
1195 };
1196 let Some(malformed) = &fixture.malformed_frame else {
1197 return Ok(checks.skip("wire family cannot spell a frame-level decode failure"));
1198 };
1199
1200 let frames = concat_frames(&[std::slice::from_ref(malformed), bare_terminal]);
1201 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1202
1203 checks.require(
1204 drained.error_count() != 0,
1205 || "the unparseable frame must surface as an Err item",
1206 )?;
1207 checks.require(
1208 !drained.has_terminal(),
1209 || "a bare terminal with no decoded frame must not fabricate a terminal record",
1210 )?;
1211
1212 checks.note("no fabricated terminal after only-unparseable frames");
1213 Ok(checks.ran())
1214}
1215
1216pub async fn usage_variants_are_reported_or_absent(
1223 fixture: &ProviderWireFixture,
1224) -> Result<ScenarioReport, ConformanceError> {
1225 let mut checks = Checks::new(
1226 "usage_variants_are_reported_or_absent",
1227 fixture.driver.provider,
1228 );
1229
1230 let frames = concat_frames(&[&fixture.text_frames, &fixture.terminal_frames]);
1231 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1232 let response = drained
1233 .response
1234 .as_ref()
1235 .ok_or_else(|| checks.fail("the genuine terminal must produce a record"))?;
1236 checks.require(
1237 response.usage.total_tokens == Some(fixture.expected_usage_total),
1238 || {
1239 format!(
1240 "terminal usage must be preserved: expected total {}, observed {:?}",
1241 fixture.expected_usage_total, response.usage.total_tokens
1242 )
1243 },
1244 )?;
1245 checks.require(
1246 response.finish_reason() == fixture.expected_finish_reason,
1247 || {
1248 format!(
1249 "terminal finish reason must be normalized: expected {:?}, observed {:?}",
1250 fixture.expected_finish_reason,
1251 response.finish_reason()
1252 )
1253 },
1254 )?;
1255 checks.note(format!(
1256 "usage total {} and finish reason {:?} preserved",
1257 fixture.expected_usage_total, fixture.expected_finish_reason
1258 ));
1259
1260 if let Some(zero_usage) = &fixture.zero_usage_terminal_frames {
1261 let frames = concat_frames(&[&fixture.text_frames, zero_usage]);
1262 let drained = fixture.driver.drive(ok_chunks(frames)).await?;
1263 let response = drained.response.as_ref().ok_or_else(|| {
1264 checks.fail("a usage-less genuine terminal must still complete the stream")
1265 })?;
1266 checks.require(!response.usage.is_reported(), || {
1267 format!(
1268 "missing usage metrics must leave every counter unreported, not invented: {:?}",
1269 response.usage
1270 )
1271 })?;
1272 checks.note("usage-less terminal completed with no counter reported");
1273 }
1274
1275 Ok(checks.report())
1276}
1277
1278pub async fn reasoning_summary_deltas_are_superseded_without_duplication(
1287 driver: &WireDriver,
1288 frames: Vec<WireInput>,
1289 summary_text: &str,
1290) -> Result<ScenarioReport, ConformanceError> {
1291 let mut checks = Checks::new(
1292 "reasoning_summary_deltas_are_superseded_without_duplication",
1293 driver.provider,
1294 );
1295
1296 let drained = driver.drive(ok_chunks(frames)).await?;
1297 checks.require(
1298 drained.completed_cleanly(),
1299 || "the reasoning stream must complete without errors",
1300 )?;
1301 let reasoning = drained.choice_reasoning();
1302 let occurrences: usize = reasoning
1303 .iter()
1304 .map(|item| item.text.matches(summary_text).count())
1305 .sum();
1306 checks.require(occurrences == 1, || {
1307 format!(
1308 "the summary must appear exactly once in the aggregated choice, observed {occurrences} across {reasoning:?}"
1309 )
1310 })?;
1311 checks.require(reasoning.len() == 1, || {
1312 format!(
1313 "deltas and their full block must collapse to one reasoning item, observed {}",
1314 reasoning.len()
1315 )
1316 })?;
1317
1318 checks.note("summary aggregated exactly once");
1319 Ok(checks.report())
1320}
1321
1322pub async fn multi_part_same_id_reasoning_keeps_every_part(
1326 driver: &WireDriver,
1327 frames: Vec<WireInput>,
1328 expected_parts: &[&str],
1329) -> Result<ScenarioReport, ConformanceError> {
1330 let mut checks = Checks::new(
1331 "multi_part_same_id_reasoning_keeps_every_part",
1332 driver.provider,
1333 );
1334
1335 let drained = driver.drive(ok_chunks(frames)).await?;
1336 checks.require(
1337 drained.completed_cleanly(),
1338 || "the reasoning stream must complete without errors",
1339 )?;
1340 let items: Vec<String> = drained
1341 .choice_reasoning()
1342 .iter()
1343 .filter_map(|item| item.native.as_ref())
1344 .map(|native| native.item.to_string())
1345 .collect();
1346 let in_order = |item: &String| {
1347 let mut rest = item.as_str();
1348 expected_parts.iter().all(|part| match rest.find(part) {
1349 Some(at) => {
1350 rest = &rest[at + part.len()..];
1351 true
1352 }
1353 None => false,
1354 })
1355 };
1356 let observed = items;
1357 checks.require(observed.len() == 1 && observed.iter().all(in_order), || {
1358 format!(
1359 "every same-id reasoning part must survive in order: expected {expected_parts:?}, observed {observed:?}"
1360 )
1361 })?;
1362
1363 checks.note(format!(
1364 "all {} reasoning parts survived",
1365 expected_parts.len()
1366 ));
1367 Ok(checks.report())
1368}
1369
1370pub async fn interleaved_reasoning_aggregates_to_one_item(
1378 driver: &WireDriver,
1379 frames: Vec<WireInput>,
1380 expected_text: &str,
1381) -> Result<ScenarioReport, ConformanceError> {
1382 let mut checks = Checks::new(
1383 "interleaved_reasoning_aggregates_to_one_item",
1384 driver.provider,
1385 );
1386
1387 let drained = driver.drive(ok_chunks(frames)).await?;
1388 checks.require(
1389 drained.completed_cleanly(),
1390 || "the interleaved stream must complete without errors",
1391 )?;
1392 let reasoning = drained.choice_reasoning();
1393 checks.require(reasoning.len() == 1, || {
1394 format!(
1395 "interleaved deltas and their completed block must collapse to one reasoning item, observed {}",
1396 reasoning.len()
1397 )
1398 })?;
1399 let carries_text = reasoning.iter().any(|item| item.text == expected_text);
1400 checks.require(carries_text, || {
1401 format!("the reasoning item must carry the completed block's text {expected_text:?}")
1402 })?;
1403
1404 checks.note("exactly one reasoning item with the completed content");
1405 Ok(checks.report())
1406}
1407
1408pub async fn interleaved_constant_id_reasoning_preserves_order(
1417 fixture: &ProviderWireFixture,
1418) -> Result<ScenarioOutcome, ConformanceError> {
1419 let mut checks = Checks::new(
1420 "interleaved_constant_id_reasoning_preserves_order",
1421 fixture.driver.provider,
1422 );
1423 let Some(interleaved) = &fixture.interleaved_reasoning else {
1424 return Ok(checks.skip("wire fixture supplies no interleaved reasoning frames"));
1425 };
1426
1427 let drained = fixture
1428 .driver
1429 .drive(ok_chunks(interleaved.frames.clone()))
1430 .await?;
1431 checks.require(
1432 drained.completed_cleanly(),
1433 || "the interleaved stream must complete without errors",
1434 )?;
1435 assert_reasoning_tool_reasoning(
1436 &checks,
1437 &drained,
1438 interleaved.first_reasoning,
1439 interleaved.tool_name,
1440 interleaved.second_reasoning,
1441 )?;
1442
1443 checks.note("boundary kept: reasoning, tool call, reasoning in order");
1444 Ok(checks.ran())
1445}
1446
1447pub async fn interleaved_signed_full_reasoning_does_not_erase_prior_thought(
1456 driver: &WireDriver,
1457 frames: Vec<WireInput>,
1458 first: &str,
1459 tool_name: &str,
1460 second: &str,
1461) -> Result<ScenarioReport, ConformanceError> {
1462 let mut checks = Checks::new(
1463 "interleaved_signed_full_reasoning_does_not_erase_prior_thought",
1464 driver.provider,
1465 );
1466
1467 let drained = driver.drive(ok_chunks(frames)).await?;
1468 checks.require(
1469 drained.completed_cleanly(),
1470 || "the interleaved stream must complete without errors",
1471 )?;
1472 assert_reasoning_tool_reasoning(&checks, &drained, first, tool_name, second)?;
1473 let signed = drained
1474 .choice_reasoning()
1475 .last()
1476 .is_some_and(|reasoning| reasoning.native.is_some());
1477 checks.require(signed, || "the post-boundary block must keep its signature")?;
1478
1479 checks.note("pre-boundary thought survived; signed block completed the post-boundary part");
1480 Ok(checks.report())
1481}
1482
1483fn assert_reasoning_tool_reasoning(
1486 checks: &Checks,
1487 drained: &DrainedStream,
1488 first: &str,
1489 tool_name: &str,
1490 second: &str,
1491) -> Result<(), ConformanceError> {
1492 let shape: Vec<String> = drained
1493 .choice
1494 .iter()
1495 .map(|content| match content {
1496 AssistantContent::Reasoning(reasoning) => format!("reasoning:{}", reasoning.text),
1497 AssistantContent::ToolCall(tool_call) => {
1498 format!("tool:{}", tool_call.function.name)
1499 }
1500 AssistantContent::Text(text) => format!("text:{}", text.text),
1501 AssistantContent::Image(_) => "image".to_string(),
1502 AssistantContent::Opaque(opaque) => format!("opaque:{}", opaque.kind().unwrap_or("")),
1503 })
1504 .collect();
1505 let expected = vec![
1506 format!("reasoning:{first}"),
1507 format!("tool:{tool_name}"),
1508 format!("reasoning:{second}"),
1509 ];
1510 checks.require(shape == expected, || {
1511 format!("the boundary must survive aggregation: expected {expected:?}, observed {shape:?}")
1512 })
1513}
1514
1515pub mod fixtures {
1517 use super::*;
1518 use crate::driver::{Model, Transport};
1519 use crate::operation::Completion;
1520 use crate::test_utils::SequencedStreamingHttpClient;
1521 use crate::wire::Wire;
1522 use serde_json::json;
1523
1524 pub async fn drain(mut stream: crate::streaming::CompletionStream) -> DrainedStream {
1528 let mut items = Vec::new();
1529 while let Some(item) = stream.next().await {
1530 items.push(item.map_err(|error| ErrorReport::from(&error)));
1531 }
1532 let partial = stream.partial().choice;
1533 let response = stream.finish().await.ok();
1534 let drained = DrainedStream {
1535 items,
1536 choice: response
1537 .as_ref()
1538 .map_or(partial, |response| response.choice.clone()),
1539 response,
1540 };
1541 super::assert_valid_event_stream(&drained.items, &drained.choice);
1545 drained
1546 }
1547
1548 pub async fn drain_observed<W, T>(
1553 model: &Model<W, T>,
1554 request: crate::completion::CompletionRequest,
1555 ) -> Result<DrainedStream, ProviderError>
1556 where
1557 W: Wire<Op = Completion>,
1558 T: Transport<W>,
1559 {
1560 let log = std::sync::Arc::new(crate::observe::ObservationLog::default());
1561 let context = crate::observe::AdapterContext::new(
1562 log.clone(),
1563 crate::observe::Subject::default(),
1564 "conformance",
1565 );
1566 let drained = match model.stream_observed(request, context) {
1569 Ok(stream) => Ok(drain(stream).await),
1570 Err(error) => Err(error),
1571 };
1572 let events: Vec<_> = log
1573 .trace()
1574 .observations
1575 .iter()
1576 .filter_map(|o| match &o.action {
1577 crate::observe::Action::Adapter { observation } => Some(observation.event.clone()),
1578 _ => None,
1579 })
1580 .collect();
1581 assert!(
1582 matches!(
1583 events.first(),
1584 Some(crate::observe::AdapterEvent::Started { .. })
1585 ),
1586 "the wire must attach the observation context it was handed: {events:?}"
1587 );
1588 assert!(
1589 matches!(
1590 events.last(),
1591 Some(crate::observe::AdapterEvent::Finished { .. })
1592 ),
1593 "the attempt must close: {events:?}"
1594 );
1595 drained
1596 }
1597
1598 fn byte_chunks(chunks: WireChunks) -> Result<Vec<http_client::Result<Bytes>>, ProviderError> {
1602 chunks
1603 .into_iter()
1604 .map(|chunk| match chunk {
1605 Ok(WireInput::Bytes(bytes)) => Ok(Ok(bytes)),
1606 Ok(WireInput::Event(_)) => Err(ProviderError::Provider(
1607 "typed-event frame fed to a byte-transport driver".to_string(),
1608 )),
1609 Err(error) => Ok(Err(error)),
1610 })
1611 .collect()
1612 }
1613
1614 fn byte_driver<W>(
1622 provider: &'static str,
1623 bind: fn(SequencedStreamingHttpClient) -> Model<W, SequencedStreamingHttpClient>,
1624 ) -> WireDriver
1625 where
1626 W: Wire<Op = Completion, Payload = crate::wire::Encoded, Frame = crate::wire::WireFrame>,
1627 {
1628 WireDriver::new(provider, move |chunks| {
1629 Box::pin(async move {
1630 let model = bind(SequencedStreamingHttpClient::new(byte_chunks(chunks)?));
1631 let request = CompletionRequest::new("hello");
1632 drain_observed(&model, request).await
1633 })
1634 })
1635 }
1636
1637 fn sse(frame: &serde_json::Value) -> WireInput {
1638 WireInput::Bytes(Bytes::from(format!("data: {frame}\n\n")))
1639 }
1640
1641 fn sse_raw(data: &str) -> WireInput {
1642 WireInput::Bytes(Bytes::from(format!("data: {data}\n\n")))
1643 }
1644
1645 fn frame_text(frame: &WireInput) -> String {
1648 frame
1649 .as_bytes()
1650 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
1651 .unwrap_or_default()
1652 }
1653
1654 pub mod openai_chat {
1656 use super::*;
1657
1658 fn driver() -> WireDriver {
1659 byte_driver("openai", |transport| {
1660 crate::driver::Model::new(
1661 crate::providers::openai::wire::OpenAIConfig::with_key(
1662 &crate::providers::openai::wire::OPENAI,
1663 "test-key",
1664 )
1665 .chat("gpt-4o"),
1666 transport,
1667 )
1668 })
1669 }
1670
1671 pub fn fixture() -> ProviderWireFixture {
1673 ProviderWireFixture {
1674 driver: driver(),
1675 text_frames: vec![sse(&json!({
1676 "id": "chatcmpl-1",
1677 "model": "gpt-4o-2024-08-06",
1678 "choices": [{"index": 0, "delta": {"content": "hi"}, "finish_reason": null}],
1679 "usage": null,
1680 }))],
1681 expected_texts: vec!["hi"],
1682 tool_call_frames: vec![
1683 sse(&json!({
1684 "choices": [{"index": 0, "delta": {"tool_calls": [{
1685 "index": 0,
1686 "id": "call_1",
1687 "type": "function",
1688 "function": {"name": "get_weather", "arguments": ""},
1689 }]}, "finish_reason": null}],
1690 })),
1691 sse(&json!({
1692 "choices": [{"index": 0, "delta": {"tool_calls": [{
1693 "index": 0,
1694 "function": {"arguments": "{\"city\":\"Tokyo\"}"},
1695 }]}, "finish_reason": null}],
1696 })),
1697 ],
1701 expected_tool_name: "get_weather",
1702 partial_tool_call_frames: Some(vec![sse(&json!({
1703 "choices": [{"index": 0, "delta": {"tool_calls": [{
1704 "index": 0,
1705 "id": "call_1",
1706 "type": "function",
1707 "function": {"name": "get_weather", "arguments": "{\"cit"},
1708 }]}, "finish_reason": null}],
1709 }))]),
1710 terminal_frames: vec![
1711 sse(&json!({
1712 "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
1713 "usage": null,
1714 })),
1715 sse(&json!({
1716 "choices": [],
1717 "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
1718 })),
1719 sse_raw("[DONE]"),
1720 ],
1721 expected_usage_total: 15,
1722 expected_finish_reason: Some(FinishReason::Stop),
1723 zero_usage_terminal_frames: Some(vec![
1724 sse(&json!({
1725 "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
1726 "usage": null,
1727 })),
1728 sse_raw("[DONE]"),
1729 ]),
1730 bare_terminal_frames: Some(vec![sse_raw("[DONE]")]),
1731 malformed_frame: Some(sse_raw("{not json")),
1732 unknown_event_frame: None,
1733 defective_known_frame: None,
1739 delta_less_prelude_frame: Some(sse_raw(
1742 r#"{"id":"","object":"","choices":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"}}}]}"#,
1743 )),
1744 refusal: None,
1745 interleaved_reasoning: None,
1757 }
1758 }
1759 }
1760
1761 pub mod openai_responses {
1763 use super::*;
1764
1765 pub fn driver() -> WireDriver {
1767 byte_driver("openai", |transport| {
1768 crate::driver::Model::new(
1769 crate::providers::openai::OpenAIConfig::new("test-key").responses("gpt-5.4"),
1770 transport,
1771 )
1772 })
1773 }
1774
1775 fn completed_response(
1776 usage: Option<&serde_json::Value>,
1777 output: &serde_json::Value,
1778 ) -> serde_json::Value {
1779 json!({
1780 "id": "resp_1",
1781 "object": "response",
1782 "created_at": 0,
1783 "status": "completed",
1784 "model": "gpt-5.4",
1785 "output": output,
1786 "tools": [],
1787 "usage": usage,
1788 })
1789 }
1790
1791 fn terminal(usage: Option<&serde_json::Value>, output: &serde_json::Value) -> WireInput {
1792 sse(&json!({
1793 "type": "response.completed",
1794 "sequence_number": 99,
1795 "response": completed_response(usage, output),
1796 }))
1797 }
1798
1799 fn usage_json() -> serde_json::Value {
1800 json!({
1801 "input_tokens": 10,
1802 "output_tokens": 5,
1803 "output_tokens_details": {"reasoning_tokens": 0},
1804 "total_tokens": 15,
1805 })
1806 }
1807
1808 fn text_delta(text: &str) -> WireInput {
1809 sse(&json!({
1810 "type": "response.output_text.delta",
1811 "content_index": 0,
1812 "delta": text,
1813 "item_id": "msg_1",
1814 "output_index": 0,
1815 "sequence_number": 1,
1816 }))
1817 }
1818
1819 fn tool_call_done() -> WireInput {
1820 sse(&json!({
1821 "type": "response.output_item.done",
1822 "output_index": 0,
1823 "sequence_number": 2,
1824 "item": {
1825 "type": "function_call",
1826 "id": "fc_1",
1827 "arguments": "{\"city\":\"Tokyo\"}",
1828 "call_id": "call_1",
1829 "name": "get_weather",
1830 "status": "completed",
1831 },
1832 }))
1833 }
1834
1835 pub fn incomplete_mid_tool_call_frames() -> Vec<WireInput> {
1842 vec![
1843 sse(&json!({
1844 "type": "response.output_item.added",
1845 "output_index": 0,
1846 "sequence_number": 1,
1847 "item": {
1848 "type": "function_call",
1849 "id": "fc_1",
1850 "arguments": "",
1851 "call_id": "call_1",
1852 "name": "add",
1853 "status": "in_progress",
1854 },
1855 })),
1856 sse(&json!({
1857 "type": "response.function_call_arguments.delta",
1858 "item_id": "fc_1",
1859 "output_index": 0,
1860 "sequence_number": 2,
1861 "delta": "{\"x",
1862 })),
1863 sse(&json!({
1864 "type": "response.function_call_arguments.delta",
1865 "item_id": "fc_1",
1866 "output_index": 0,
1867 "sequence_number": 3,
1868 "delta": "\":48151",
1869 })),
1870 sse(&json!({
1871 "type": "response.function_call_arguments.done",
1872 "item_id": "fc_1",
1873 "output_index": 0,
1874 "sequence_number": 4,
1875 "arguments": "{\"x\":48151",
1876 })),
1877 sse(&json!({
1878 "type": "response.output_item.done",
1879 "output_index": 0,
1880 "sequence_number": 5,
1881 "item": {
1882 "type": "function_call",
1883 "id": "fc_1",
1884 "arguments": "{\"x\":48151",
1885 "call_id": "call_1",
1886 "name": "add",
1887 "status": "incomplete",
1888 },
1889 })),
1890 sse(&json!({
1891 "type": "response.incomplete",
1892 "sequence_number": 6,
1893 "response": {
1894 "id": "resp_1",
1895 "object": "response",
1896 "created_at": 0,
1897 "status": "incomplete",
1898 "incomplete_details": {"reason": "max_output_tokens"},
1899 "model": "gpt-5.4",
1900 "output": [{
1901 "type": "function_call",
1902 "id": "fc_1",
1903 "arguments": "{\"x\":48151",
1904 "call_id": "call_1",
1905 "name": "add",
1906 "status": "incomplete",
1907 }],
1908 "tools": [],
1909 "usage": usage_json(),
1910 },
1911 })),
1912 ]
1913 }
1914
1915 fn reasoning_done_item(
1916 id: &str,
1917 summary: &serde_json::Value,
1918 content: &serde_json::Value,
1919 encrypted: Option<&str>,
1920 ) -> WireInput {
1921 let mut item = json!({
1922 "type": "reasoning",
1923 "id": id,
1924 "summary": summary,
1925 "content": content,
1926 "status": "completed",
1927 });
1928 if let (Some(encrypted), Some(object)) = (encrypted, item.as_object_mut()) {
1929 object.insert("encrypted_content".to_string(), json!(encrypted));
1930 }
1931 sse(&json!({
1932 "type": "response.output_item.done",
1933 "output_index": 0,
1934 "sequence_number": 3,
1935 "item": item,
1936 }))
1937 }
1938
1939 pub fn fixture() -> ProviderWireFixture {
1941 ProviderWireFixture {
1942 driver: driver(),
1943 text_frames: vec![text_delta("hi")],
1944 expected_texts: vec!["hi"],
1945 tool_call_frames: vec![tool_call_done()],
1946 expected_tool_name: "get_weather",
1947 partial_tool_call_frames: Some(vec![
1948 sse(&json!({
1949 "type": "response.output_item.added",
1950 "output_index": 0,
1951 "sequence_number": 1,
1952 "item": {
1953 "type": "function_call",
1954 "id": "fc_1",
1955 "arguments": "",
1956 "call_id": "call_1",
1957 "name": "get_weather",
1958 "status": "in_progress",
1959 },
1960 })),
1961 sse(&json!({
1962 "type": "response.function_call_arguments.delta",
1963 "item_id": "fc_1",
1964 "output_index": 0,
1965 "sequence_number": 2,
1966 "delta": "{\"cit",
1967 })),
1968 ]),
1969 terminal_frames: vec![terminal(Some(&usage_json()), &json!([]))],
1970 expected_usage_total: 15,
1971 expected_finish_reason: Some(FinishReason::Stop),
1972 zero_usage_terminal_frames: Some(vec![terminal(None, &json!([]))]),
1973 bare_terminal_frames: None,
1974 malformed_frame: Some(sse_raw("{not json")),
1975 unknown_event_frame: Some(sse(&json!({
1976 "type": "response.web_search_call.searching",
1977 "output_index": 0,
1978 "sequence_number": 4,
1979 "item_id": "ws_1",
1980 }))),
1981 defective_known_frame: None,
1984 delta_less_prelude_frame: None,
1985 refusal: Some(RefusalFixture {
1986 frames: vec![sse(&json!({
1987 "type": "response.refusal.delta",
1988 "content_index": 0,
1989 "delta": "I cannot help with that.",
1990 "item_id": "msg_1",
1991 "output_index": 0,
1992 "sequence_number": 1,
1993 }))],
1994 expected_text: "I cannot help with that.",
1995 }),
1996 interleaved_reasoning: None,
1997 }
1998 }
1999
2000 pub fn buffered_driver() -> BufferedBodyDriver {
2010 BufferedBodyDriver::new("chatgpt", |body| {
2011 Box::pin(async move {
2012 let model = crate::driver::Model::new(
2013 crate::providers::openai::OpenAIConfig::with_key(
2014 &crate::providers::chatgpt::DIALECT,
2015 "test-token",
2016 )
2017 .with_account_id("account-id")
2018 .responses("gpt-5.4"),
2019 crate::test_utils::RecordingHttpClient::new(body),
2020 );
2021 let request = CompletionRequest::new("hello");
2022 let response = model.call(request).await?;
2023 Ok(response.choice)
2024 })
2025 })
2026 }
2027
2028 fn message_output(text: &str) -> serde_json::Value {
2029 json!([{
2030 "type": "message",
2031 "id": "msg_1",
2032 "role": "assistant",
2033 "status": "completed",
2034 "content": [{"type": "output_text", "text": text, "annotations": []}],
2035 }])
2036 }
2037
2038 pub fn terminal_body_only_sse_body(text: &str) -> String {
2040 frame_text(&terminal(Some(&usage_json()), &message_output(text)))
2041 }
2042
2043 pub fn terminal_body_and_delta_sse_body(text: &str) -> String {
2045 let frames = [
2046 text_delta(text),
2047 terminal(Some(&usage_json()), &message_output(text)),
2048 ];
2049 frames.iter().map(frame_text).collect()
2050 }
2051
2052 pub fn delta_only_sse_body(text: &str) -> String {
2055 let frames = [text_delta(text), terminal(Some(&usage_json()), &json!([]))];
2056 frames.iter().map(frame_text).collect()
2057 }
2058
2059 pub fn envelope_less_reasoning_supersede_sse_body() -> (String, &'static str) {
2066 let delta = json!({
2067 "type": "response.reasoning_summary_text.delta",
2068 "delta": "step 1",
2069 });
2070 let frames = [
2071 sse(&delta),
2072 reasoning_done_item(
2073 "rs_1",
2074 &json!([{"type": "summary_text", "text": "step 1"}]),
2075 &json!([]),
2076 None,
2077 ),
2078 terminal(Some(&usage_json()), &json!([])),
2079 ];
2080 (frames.iter().map(frame_text).collect(), "step 1")
2081 }
2082
2083 pub fn reasoning_summary_supersede_frames() -> (Vec<WireInput>, &'static str) {
2087 let frames = vec![
2088 sse(&json!({
2089 "type": "response.reasoning_summary_text.delta",
2090 "item_id": "rs_1",
2091 "output_index": 0,
2092 "summary_index": 0,
2093 "sequence_number": 1,
2094 "delta": "step 1",
2095 })),
2096 reasoning_done_item(
2097 "rs_1",
2098 &json!([{"type": "summary_text", "text": "step 1"}]),
2099 &json!([]),
2100 None,
2101 ),
2102 terminal(Some(&usage_json()), &json!([])),
2103 ];
2104 (frames, "step 1")
2105 }
2106
2107 pub fn multi_part_reasoning_frames() -> (Vec<WireInput>, Vec<&'static str>) {
2110 let frames = vec![
2111 reasoning_done_item(
2112 "rs_1",
2113 &json!([
2114 {"type": "summary_text", "text": "s1"},
2115 {"type": "summary_text", "text": "s2"},
2116 ]),
2117 &json!([{"type": "reasoning_text", "text": "visible"}]),
2118 Some("enc_blob"),
2119 ),
2120 terminal(Some(&usage_json()), &json!([])),
2121 ];
2122 (frames, vec!["s1", "s2", "visible", "enc_blob"])
2123 }
2124
2125 pub fn interleaved_reasoning_frames() -> (Vec<WireInput>, &'static str) {
2128 let frames = vec![
2129 sse(&json!({
2130 "type": "response.reasoning_text.delta",
2131 "item_id": "rs_2",
2132 "output_index": 0,
2133 "content_index": 0,
2134 "sequence_number": 1,
2135 "delta": "full ",
2136 })),
2137 sse(&json!({
2139 "type": "response.output_item.done",
2140 "output_index": 1,
2141 "sequence_number": 2,
2142 "item": {
2143 "type": "function_call",
2144 "id": "fc_1",
2145 "arguments": "{\"city\":\"Tokyo\"}",
2146 "call_id": "call_1",
2147 "name": "get_weather",
2148 "status": "completed",
2149 },
2150 })),
2151 reasoning_done_item(
2152 "rs_2",
2153 &json!([]),
2154 &json!([{"type": "reasoning_text", "text": "full reasoning"}]),
2155 None,
2156 ),
2157 terminal(Some(&usage_json()), &json!([])),
2158 ];
2159 (frames, "full reasoning")
2160 }
2161 }
2162
2163 pub mod gemini_rest {
2165 use super::*;
2166
2167 fn driver() -> WireDriver {
2168 byte_driver("gemini", |transport| {
2169 crate::driver::Model::new(
2170 crate::providers::gemini::GeminiConfig::new("test-key").completion(
2171 crate::providers::gemini::completion::GEMINI_2_5_PRO_PREVIEW_06_05,
2172 ),
2173 transport,
2174 )
2175 })
2176 }
2177
2178 pub fn fixture() -> ProviderWireFixture {
2180 ProviderWireFixture {
2181 driver: driver(),
2182 text_frames: vec![sse(&json!({
2183 "candidates": [{"content": {"parts": [{"text": "hi"}], "role": "model"}}],
2184 "responseId": "resp-1",
2185 "modelVersion": "gemini-2.5-pro",
2186 }))],
2187 expected_texts: vec!["hi"],
2188 tool_call_frames: vec![sse(&json!({
2189 "candidates": [{"content": {"parts": [{
2190 "functionCall": {"name": "get_weather", "args": {"city": "Tokyo"}},
2191 }], "role": "model"}}],
2192 "responseId": "resp-1",
2193 "modelVersion": "gemini-2.5-pro",
2194 }))],
2195 expected_tool_name: "get_weather",
2196 partial_tool_call_frames: None,
2198 terminal_frames: vec![sse(&json!({
2199 "candidates": [{
2200 "content": {"parts": [], "role": "model"},
2201 "finishReason": "STOP",
2202 }],
2203 "usageMetadata": {
2204 "promptTokenCount": 5,
2205 "candidatesTokenCount": 2,
2206 "totalTokenCount": 7,
2207 },
2208 "responseId": "resp-1",
2209 "modelVersion": "gemini-2.5-pro",
2210 }))],
2211 expected_usage_total: 7,
2212 expected_finish_reason: Some(FinishReason::Stop),
2213 zero_usage_terminal_frames: Some(vec![sse(&json!({
2214 "candidates": [{
2215 "content": {"parts": [], "role": "model"},
2216 "finishReason": "STOP",
2217 }],
2218 "responseId": "resp-1",
2219 "modelVersion": "gemini-2.5-pro",
2220 }))]),
2221 bare_terminal_frames: None,
2222 malformed_frame: Some(sse_raw("{not json")),
2223 unknown_event_frame: Some(sse_raw(r#"{"noise":true}"#)),
2227 defective_known_frame: Some(sse_raw(r#"{"candidates": 42}"#)),
2228 delta_less_prelude_frame: None,
2229 refusal: None,
2230 interleaved_reasoning: Some(interleaved_thought_fixture()),
2231 }
2232 }
2233
2234 fn chunk(parts: &serde_json::Value) -> WireInput {
2235 sse(&json!({
2236 "candidates": [{"content": {"parts": parts, "role": "model"}}],
2237 "responseId": "resp-1",
2238 "modelVersion": "gemini-2.5-pro",
2239 }))
2240 }
2241
2242 fn terminal_frame() -> WireInput {
2243 sse(&json!({
2244 "candidates": [{
2245 "content": {"parts": [], "role": "model"},
2246 "finishReason": "STOP",
2247 }],
2248 "usageMetadata": {
2249 "promptTokenCount": 5,
2250 "candidatesTokenCount": 2,
2251 "totalTokenCount": 7,
2252 },
2253 "responseId": "resp-1",
2254 "modelVersion": "gemini-2.5-pro",
2255 }))
2256 }
2257
2258 fn interleaved_thought_fixture() -> InterleavedReasoningFixture {
2261 InterleavedReasoningFixture {
2262 frames: vec![
2263 chunk(&json!([{"text": "before tool", "thought": true}])),
2264 chunk(&json!([{
2265 "functionCall": {"name": "get_weather", "args": {"city": "Tokyo"}},
2266 }])),
2267 chunk(&json!([{"text": "after tool", "thought": true}])),
2268 terminal_frame(),
2269 ],
2270 first_reasoning: "before tool",
2271 tool_name: "get_weather",
2272 second_reasoning: "after tool",
2273 }
2274 }
2275
2276 pub fn interleaved_signed_thought_frames()
2279 -> (Vec<WireInput>, &'static str, &'static str, &'static str) {
2280 let frames = vec![
2281 chunk(&json!([{"text": "before tool", "thought": true}])),
2282 chunk(&json!([{
2283 "functionCall": {"name": "get_weather", "args": {"city": "Tokyo"}},
2284 }])),
2285 chunk(&json!([{
2286 "text": "signed conclusion",
2287 "thought": true,
2288 "thoughtSignature": "sig-1",
2289 }])),
2290 terminal_frame(),
2291 ];
2292 (frames, "before tool", "get_weather", "signed conclusion")
2293 }
2294 }
2295
2296 pub mod interactions {
2298 use super::*;
2299
2300 fn driver() -> WireDriver {
2301 byte_driver("gemini", |transport| {
2302 crate::driver::Model::new(
2303 crate::providers::gemini::GeminiConfig::new("test-key")
2304 .interactions("gemini-2.5-pro"),
2305 transport,
2306 )
2307 })
2308 }
2309
2310 fn completed(usage: Option<serde_json::Value>) -> WireInput {
2311 let mut interaction = json!({
2312 "id": "int-1",
2313 "model": "gemini-2.5-pro",
2314 "status": "completed",
2315 });
2316 if let (Some(usage), Some(object)) = (usage, interaction.as_object_mut()) {
2317 object.insert("usage".to_string(), usage);
2318 }
2319 sse(&json!({
2320 "event_type": "interaction.completed",
2321 "interaction": interaction,
2322 }))
2323 }
2324
2325 pub fn fixture() -> ProviderWireFixture {
2327 ProviderWireFixture {
2328 driver: driver(),
2329 text_frames: vec![sse(&json!({
2330 "event_type": "step.delta",
2331 "index": 0,
2332 "delta": {"type": "text", "text": "hi"},
2333 }))],
2334 expected_texts: vec!["hi"],
2335 tool_call_frames: vec![sse(&json!({
2336 "event_type": "step.delta",
2337 "index": 0,
2338 "delta": {
2339 "type": "function_call",
2340 "name": "get_weather",
2341 "arguments": {"city": "Tokyo"},
2342 "id": "call-1",
2343 },
2344 }))],
2345 expected_tool_name: "get_weather",
2346 partial_tool_call_frames: None,
2349 terminal_frames: vec![completed(Some(json!({
2350 "total_input_tokens": 5,
2351 "total_output_tokens": 2,
2352 "total_tokens": 7,
2353 })))],
2354 expected_usage_total: 7,
2355 expected_finish_reason: Some(FinishReason::Stop),
2356 zero_usage_terminal_frames: Some(vec![completed(None)]),
2357 bare_terminal_frames: None,
2358 malformed_frame: Some(sse_raw("{not json")),
2359 unknown_event_frame: Some(sse(&json!({
2360 "event_type": "future.event",
2361 "index": 0,
2362 }))),
2363 defective_known_frame: Some(sse_raw(
2366 r#"{"event_type":"step.delta","index":0,"delta":42}"#,
2367 )),
2368 delta_less_prelude_frame: None,
2369 refusal: None,
2370 interleaved_reasoning: Some(interleaved_thought_fixture()),
2371 }
2372 }
2373
2374 fn interleaved_thought_fixture() -> InterleavedReasoningFixture {
2377 let frames = vec![
2378 sse(&json!({
2379 "event_type": "step.delta",
2380 "index": 0,
2381 "delta": {
2382 "type": "thought_summary",
2383 "content": {"type": "text", "text": "before tool"},
2384 },
2385 })),
2386 sse(&json!({
2387 "event_type": "step.delta",
2388 "index": 1,
2389 "delta": {
2390 "type": "function_call",
2391 "name": "get_weather",
2392 "arguments": {"city": "Tokyo"},
2393 "id": "call-1",
2394 },
2395 })),
2396 sse(&json!({
2397 "event_type": "step.delta",
2398 "index": 2,
2399 "delta": {
2400 "type": "thought_summary",
2401 "content": {"type": "text", "text": "after tool"},
2402 },
2403 })),
2404 completed(Some(json!({
2405 "total_input_tokens": 5,
2406 "total_output_tokens": 2,
2407 "total_tokens": 7,
2408 }))),
2409 ];
2410 InterleavedReasoningFixture {
2411 frames,
2412 first_reasoning: "before tool",
2413 tool_name: "get_weather",
2414 second_reasoning: "after tool",
2415 }
2416 }
2417 }
2418
2419 pub mod anthropic {
2421 use super::*;
2422
2423 fn driver() -> WireDriver {
2424 byte_driver("anthropic", |transport| {
2425 crate::driver::Model::new(
2426 crate::providers::anthropic::wire::AnthropicConfig::new("test-key")
2427 .completion(crate::providers::anthropic::completion::CLAUDE_SONNET_4_6),
2428 transport,
2429 )
2430 })
2431 }
2432
2433 fn message_start() -> WireInput {
2434 sse(&json!({
2435 "type": "message_start",
2436 "message": {
2437 "id": "msg_1",
2438 "role": "assistant",
2439 "content": [],
2440 "model": "claude-sonnet-4-6",
2441 "stop_reason": null,
2442 "stop_sequence": null,
2443 "usage": {"input_tokens": 5, "output_tokens": 0},
2444 },
2445 }))
2446 }
2447
2448 pub fn fixture() -> ProviderWireFixture {
2450 ProviderWireFixture {
2451 driver: driver(),
2452 text_frames: vec![
2453 message_start(),
2454 sse(&json!({
2455 "type": "content_block_start",
2456 "index": 0,
2457 "content_block": {"type": "text", "text": ""},
2458 })),
2459 sse(&json!({
2460 "type": "content_block_delta",
2461 "index": 0,
2462 "delta": {"type": "text_delta", "text": "hi"},
2463 })),
2464 ],
2465 expected_texts: vec!["hi"],
2466 tool_call_frames: vec![
2467 sse(&json!({
2468 "type": "content_block_start",
2469 "index": 0,
2470 "content_block": {
2471 "type": "tool_use",
2472 "id": "toolu_1",
2473 "name": "get_weather",
2474 "input": {},
2475 },
2476 })),
2477 sse(&json!({
2478 "type": "content_block_delta",
2479 "index": 0,
2480 "delta": {"type": "input_json_delta", "partial_json": "{\"city\":\"Tokyo\"}"},
2481 })),
2482 sse(&json!({"type": "content_block_stop", "index": 0})),
2485 ],
2486 expected_tool_name: "get_weather",
2487 partial_tool_call_frames: Some(vec![
2488 sse(&json!({
2489 "type": "content_block_start",
2490 "index": 0,
2491 "content_block": {
2492 "type": "tool_use",
2493 "id": "toolu_1",
2494 "name": "get_weather",
2495 "input": {},
2496 },
2497 })),
2498 sse(&json!({
2499 "type": "content_block_delta",
2500 "index": 0,
2501 "delta": {"type": "input_json_delta", "partial_json": "{\"cit"},
2502 })),
2503 ]),
2504 terminal_frames: vec![sse(&json!({
2505 "type": "message_delta",
2506 "delta": {"stop_reason": "end_turn", "stop_sequence": null},
2507 "usage": {"output_tokens": 4},
2508 }))],
2509 expected_usage_total: 9,
2511 expected_finish_reason: Some(FinishReason::Stop),
2512 zero_usage_terminal_frames: None,
2515 bare_terminal_frames: Some(vec![sse(&json!({"type": "message_stop"}))]),
2518 malformed_frame: Some(sse_raw("{not json")),
2519 unknown_event_frame: Some(sse(&json!({
2520 "type": "content_block_heartbeat",
2521 "index": 0,
2522 }))),
2523 defective_known_frame: Some(sse_raw(
2526 r#"{"type":"content_block_delta","index":0,"delta":42}"#,
2527 )),
2528 delta_less_prelude_frame: None,
2529 refusal: None,
2530 interleaved_reasoning: None,
2531 }
2532 }
2533 }
2534}