mattrguerra/cortexmainPrivate on GitHub, published here in full

provider.rs

Language Rust6.8 KB181 lines

crates/cortex-core/tests/provider.rs

  1. //! The provider client against the stand-in server: every path a real stream
  2. //! can take, including cancelling mid-reply.
  3. use std::time::Duration;
  4. use cortex_core::provider::{ChatRequest, Client, OutMessage, ProviderError, StreamItem, StreamOptions};
  5. use cortex_core::types::Usage;
  6. use tokio_util::sync::CancellationToken;
  7. fn req(model: &str, text: &str) -> ChatRequest {
  8. ChatRequest {
  9. model: model.into(),
  10. messages: vec![OutMessage::text("system", "be brief"), OutMessage::text("user", text)],
  11. stream: true,
  12. stream_options: Some(StreamOptions { include_usage: true }),
  13. temperature: None,
  14. max_tokens: None,
  15. }
  16. }
  17. async fn run(client: &Client, r: &ChatRequest) -> (cortex_core::provider::StreamOutcome, Vec<StreamItem>) {
  18. let mut items = Vec::new();
  19. let out = client.stream_chat(r, &CancellationToken::new(), |i| items.push(i)).await;
  20. (out, items)
  21. }
  22. #[tokio::test]
  23. async fn streams_text_and_final_usage() {
  24. let s = standin::spawn().await;
  25. let c = Client::new(&s.base_url(), Some("good-key".into()));
  26. let (out, items) = run(&c, &req("standin/echo", "hello there friend")).await;
  27. assert_eq!(out.error, None);
  28. assert_eq!(out.content, "You said: hello there friend");
  29. assert_eq!(out.finish_reason.as_deref(), Some("stop"));
  30. // 2 + 3 words, plus 4 per message
  31. assert_eq!(out.usage, Some(Usage { prompt_tokens: 13, completion_tokens: 5, cached_tokens: 0 }));
  32. let deltas = items.iter().filter(|i| matches!(i, StreamItem::Content(_))).count();
  33. assert_eq!(deltas, 5);
  34. assert!(out.ttft_ms.is_some());
  35. let rec = &s.requests()[0];
  36. assert_eq!(rec.authorization.as_deref(), Some("Bearer good-key"));
  37. assert_eq!(rec.body["stream_options"]["include_usage"], true);
  38. assert_eq!(rec.body["stream"], true);
  39. }
  40. #[tokio::test]
  41. async fn no_key_sends_no_auth_header() {
  42. let s = standin::spawn().await;
  43. let c = Client::new(&s.base_url(), Some(String::new()));
  44. run(&c, &req("standin/echo", "hi")).await;
  45. assert_eq!(s.requests()[0].authorization, None);
  46. }
  47. #[tokio::test]
  48. async fn usage_variants() {
  49. let s = standin::spawn().await;
  50. let c = Client::new(&s.base_url(), None);
  51. let (out, _) = run(&c, &req("standin/inline-usage", "a b")).await;
  52. assert_eq!(out.usage.unwrap().completion_tokens, 4);
  53. let (out, _) = run(&c, &req("standin/no-usage", "a b")).await;
  54. assert_eq!(out.usage, None);
  55. assert_eq!(out.content, "You said: a b");
  56. let (out, _) = run(&c, &req("standin/cached", "one two three")).await;
  57. assert_eq!(out.usage.unwrap().cached_tokens, 13);
  58. assert_eq!(out.provider_cost_usd, Some(0.0000123));
  59. }
  60. #[tokio::test]
  61. async fn reasoning_is_separate_from_content() {
  62. let s = standin::spawn().await;
  63. let c = Client::new(&s.base_url(), None);
  64. let (out, items) = run(&c, &req("standin/reasoning", "why")).await;
  65. assert_eq!(out.reasoning, "Checking the question.");
  66. assert_eq!(out.content, "You said: why");
  67. assert!(matches!(items[0], StreamItem::Reasoning(_)));
  68. }
  69. #[tokio::test]
  70. async fn images_go_as_content_parts() {
  71. let s = standin::spawn().await;
  72. let c = Client::new(&s.base_url(), None);
  73. let mut r = req("standin/vision", "");
  74. r.messages[1] = OutMessage::with_images(
  75. "user",
  76. "what is in these",
  77. vec!["data:image/png;base64,AAAA".into(), "data:image/jpeg;base64,BBBB".into()],
  78. );
  79. let (out, _) = run(&c, &r).await;
  80. assert_eq!(out.content, "I see 2 images.");
  81. let body = &s.requests()[0].body;
  82. assert_eq!(body["messages"][1]["content"][2]["type"], "image_url");
  83. }
  84. #[tokio::test]
  85. async fn http_errors_map_to_plain_errors() {
  86. let s = standin::spawn().await;
  87. let bad = Client::new(&s.base_url(), Some("bad-key".into()));
  88. assert_eq!(run(&bad, &req("standin/echo", "x")).await.0.error, Some(ProviderError::InvalidKey));
  89. // Saving a key checks it this way before storing it.
  90. assert_eq!(bad.list_models().await.unwrap_err(), ProviderError::InvalidKey);
  91. let c = Client::new(&s.base_url(), None);
  92. assert_eq!(run(&c, &req("standin/402", "x")).await.0.error, Some(ProviderError::InsufficientBalance));
  93. assert_eq!(
  94. run(&c, &req("standin/429", "x")).await.0.error,
  95. Some(ProviderError::RateLimited { retry_after: Some(7) })
  96. );
  97. assert_eq!(
  98. run(&c, &req("standin/500", "x")).await.0.error,
  99. Some(ProviderError::Upstream { status: 500, message: "upstream exploded".into() })
  100. );
  101. }
  102. #[tokio::test]
  103. async fn midstream_error_keeps_the_partial_reply() {
  104. let s = standin::spawn().await;
  105. let c = Client::new(&s.base_url(), None);
  106. let (out, _) = run(&c, &req("standin/midstream-error", "one two three four")).await;
  107. assert_eq!(out.content, "You said: one"); // three words, then the error
  108. assert_eq!(out.error, Some(ProviderError::Stream("model overloaded".into())));
  109. assert_eq!(out.usage, None);
  110. }
  111. #[tokio::test]
  112. async fn unreachable_server() {
  113. // Bind then drop a listener to get a port with nothing on it.
  114. let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
  115. let c = Client::new(&format!("http://127.0.0.1:{port}/v1"), None);
  116. let (out, _) = run(&c, &req("standin/echo", "x")).await;
  117. assert_eq!(out.error.unwrap().kind(), "unreachable");
  118. }
  119. #[tokio::test]
  120. async fn cancel_drops_the_connection_and_keeps_what_arrived() {
  121. let s = standin::spawn().await;
  122. let c = Client::new(&s.base_url(), None);
  123. let cancel = CancellationToken::new();
  124. let mut deltas = 0;
  125. let trigger = cancel.clone();
  126. let out = c
  127. .stream_chat(&req("standin/slow", "go"), &cancel, |i| {
  128. if matches!(i, StreamItem::Content(_)) {
  129. deltas += 1;
  130. if deltas == 3 {
  131. trigger.cancel();
  132. }
  133. }
  134. })
  135. .await;
  136. assert!(out.cancelled);
  137. assert_eq!(out.error, None);
  138. assert!(out.content.starts_with("word1 word2"), "{}", out.content);
  139. assert!(out.content.len() < 60);
  140. // The server notices the dropped connection on its next write.
  141. let mut seen = 0;
  142. for _ in 0..40 {
  143. seen = s.disconnects();
  144. if seen > 0 {
  145. break;
  146. }
  147. tokio::time::sleep(Duration::from_millis(50)).await;
  148. }
  149. assert_eq!(seen, 1, "server never saw the client go away");
  150. }
  151. #[tokio::test]
  152. async fn cancel_before_the_response_arrives() {
  153. let s = standin::spawn().await;
  154. let c = Client::new(&s.base_url(), None);
  155. let cancel = CancellationToken::new();
  156. cancel.cancel();
  157. let out = c.stream_chat(&req("standin/echo", "x"), &cancel, |_| {}).await;
  158. assert!(out.cancelled && out.content.is_empty());
  159. }
  160. #[tokio::test]
  161. async fn lists_models() {
  162. let s = standin::spawn().await;
  163. let c = Client::new(&s.base_url(), None);
  164. let v = c.list_models().await.unwrap();
  165. assert_eq!(v["data"][0]["id"], "standin/echo");
  166. }