mattrguerra/cortexmainPrivate on GitHub, published here in full

lib.rs

Language Rust10.8 KB260 lines

crates/standin/src/lib.rs

  1. //! A stand-in for DeepInfra (or any OpenAI-compatible server).
  2. //!
  3. //! The model id picks the behavior, so one server covers every path:
  4. //!
  5. //! | model | behavior |
  6. //! |---------------------------|-------------------------------------------------------------|
  7. //! | `standin/echo` | "You said: ..." word by word; usage in a final empty chunk |
  8. //! | `standin/cached` | like echo, with 40 cached prompt tokens and a cache price |
  9. //! | `standin/inline-usage` | like echo, usage on the last content chunk |
  10. //! | `standin/slow` | 200 words, one every 50 ms (for cancelling) |
  11. //! | `standin/no-usage` | like echo, no usage at all (as most local servers) |
  12. //! | `standin/reasoning` | reasoning deltas, then the answer |
  13. //! | `standin/vision` | "I see N image(s)." |
  14. //! | `standin/midstream-error` | three words, then an error event |
  15. //! | `standin/402` `/429` `/500` | that HTTP status (429 with Retry-After: 7) |
  16. //!
  17. //! The key `bad-key` gets a 401 in DeepInfra's shape. Every request is recorded,
  18. //! and a stream the client abandons is counted in `disconnects`.
  19. use std::convert::Infallible;
  20. use std::net::SocketAddr;
  21. use std::sync::atomic::{AtomicUsize, Ordering};
  22. use std::sync::{Arc, Mutex};
  23. use std::time::Duration;
  24. use axum::body::Body;
  25. use axum::extract::State;
  26. use axum::http::{HeaderMap, StatusCode};
  27. use axum::response::{IntoResponse, Response};
  28. use axum::routing::{get, post};
  29. use axum::{Json, Router};
  30. use bytes::Bytes;
  31. use serde_json::{json, Value};
  32. use tokio::sync::mpsc;
  33. use tokio_stream::wrappers::ReceiverStream;
  34. #[derive(Debug, Clone)]
  35. pub struct Recorded {
  36. pub authorization: Option<String>,
  37. pub body: Value,
  38. }
  39. #[derive(Default)]
  40. pub struct Shared {
  41. pub requests: Mutex<Vec<Recorded>>,
  42. pub disconnects: AtomicUsize,
  43. }
  44. pub struct Standin {
  45. pub addr: SocketAddr,
  46. pub shared: Arc<Shared>,
  47. }
  48. impl Standin {
  49. /// The base URL a client should use, e.g. `http://127.0.0.1:PORT/v1`.
  50. pub fn base_url(&self) -> String {
  51. format!("http://{}/v1", self.addr)
  52. }
  53. pub fn requests(&self) -> Vec<Recorded> {
  54. self.shared.requests.lock().unwrap().clone()
  55. }
  56. pub fn disconnects(&self) -> usize {
  57. self.shared.disconnects.load(Ordering::SeqCst)
  58. }
  59. }
  60. pub fn models() -> Value {
  61. let m = |id: &str, i: f64, o: f64, cache: Option<f64>, tags: &[&str]| {
  62. let mut pricing = json!({"input_tokens": i, "output_tokens": o});
  63. if let Some(c) = cache {
  64. pricing["cache_read_tokens"] = json!(c);
  65. }
  66. json!({"id": id, "object": "model", "owned_by": "standin",
  67. "metadata": {"context_length": 32768, "max_tokens": 4096, "pricing": pricing, "tags": tags}})
  68. };
  69. json!({"object": "list", "data": [
  70. m("standin/echo", 0.5, 1.5, None, &["chat"]),
  71. m("standin/cached", 0.5, 1.5, Some(0.1), &["chat", "prompt_cache"]),
  72. m("standin/inline-usage", 0.5, 1.5, None, &["chat"]),
  73. m("standin/slow", 0.5, 1.5, None, &["chat"]),
  74. m("standin/no-usage", 0.5, 1.5, None, &["chat"]),
  75. m("standin/reasoning", 0.5, 1.5, None, &["chat", "reasoning"]),
  76. m("standin/vision", 0.5, 1.5, None, &["chat", "vision"]),
  77. m("standin/midstream-error", 0.5, 1.5, None, &["chat"]),
  78. m("standin/402", 0.5, 1.5, None, &["chat"]),
  79. m("standin/429", 0.5, 1.5, None, &["chat"]),
  80. m("standin/500", 0.5, 1.5, None, &["chat"]),
  81. json!({"id": "standin/embed", "object": "model", "metadata": {"pricing": {"input_tokens": 0.01}, "tags": ["embed"]}}),
  82. json!({"id": "standin/image", "object": "model", "metadata": {"pricing": {"per_image_unit": 0.005}, "tags": ["image-gen"]}}),
  83. ]})
  84. }
  85. pub fn router(shared: Arc<Shared>) -> Router {
  86. Router::new().route("/v1/models", get(list_models)).route("/v1/chat/completions", post(chat)).with_state(shared)
  87. }
  88. pub async fn spawn() -> Standin {
  89. spawn_on("127.0.0.1:0").await
  90. }
  91. pub async fn spawn_on(bind: &str) -> Standin {
  92. let shared = Arc::new(Shared::default());
  93. let listener = tokio::net::TcpListener::bind(bind).await.expect("bind");
  94. let addr = listener.local_addr().unwrap();
  95. let app = router(shared.clone());
  96. tokio::spawn(async move {
  97. axum::serve(listener, app).await.ok();
  98. });
  99. Standin { addr, shared }
  100. }
  101. fn text_of(content: &Value) -> (String, usize) {
  102. match content {
  103. Value::String(s) => (s.clone(), 0),
  104. Value::Array(parts) => {
  105. let mut text = String::new();
  106. let mut images = 0;
  107. for p in parts {
  108. match p.get("type").and_then(Value::as_str) {
  109. Some("text") => text.push_str(p.get("text").and_then(Value::as_str).unwrap_or("")),
  110. Some("image_url") => images += 1,
  111. _ => {}
  112. }
  113. }
  114. (text, images)
  115. }
  116. _ => (String::new(), 0),
  117. }
  118. }
  119. fn chunk(model: &str, delta: Value, finish: Option<&str>) -> String {
  120. json!({"id": "standin-1", "object": "chat.completion.chunk", "model": model,
  121. "choices": [{"index": 0, "delta": delta, "finish_reason": finish}]})
  122. .to_string()
  123. }
  124. /// DeepInfra's list needs no key, but a bad one gets a 401.
  125. async fn list_models(headers: HeaderMap) -> Response {
  126. if headers.get("authorization").and_then(|v| v.to_str().ok()) == Some("Bearer bad-key") {
  127. let e = json!({"detail": "User is not authorized to access this resource"});
  128. return (StatusCode::UNAUTHORIZED, Json(e)).into_response();
  129. }
  130. Json(models()).into_response()
  131. }
  132. async fn chat(State(shared): State<Arc<Shared>>, headers: HeaderMap, Json(body): Json<Value>) -> Response {
  133. let authorization = headers.get("authorization").and_then(|v| v.to_str().ok()).map(str::to_string);
  134. shared.requests.lock().unwrap().push(Recorded { authorization: authorization.clone(), body: body.clone() });
  135. if authorization.as_deref() == Some("Bearer bad-key") {
  136. let e = json!({"error": {"message": "Invalid API key", "type": "invalid_request_error", "code": "invalid_api_key"}});
  137. return (StatusCode::UNAUTHORIZED, Json(e)).into_response();
  138. }
  139. let model = body.get("model").and_then(Value::as_str).unwrap_or("").to_string();
  140. match model.as_str() {
  141. "standin/402" => {
  142. return (StatusCode::PAYMENT_REQUIRED, Json(json!({"error": {"message": "Insufficient balance"}})))
  143. .into_response()
  144. }
  145. "standin/429" => {
  146. return (
  147. StatusCode::TOO_MANY_REQUESTS,
  148. [("retry-after", "7")],
  149. Json(json!({"error": {"message": "Rate limited"}})),
  150. )
  151. .into_response()
  152. }
  153. "standin/500" => {
  154. return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"detail": "upstream exploded"}))).into_response()
  155. }
  156. _ => {}
  157. }
  158. let messages = body.get("messages").and_then(Value::as_array).cloned().unwrap_or_default();
  159. let mut prompt_words = 0usize;
  160. let mut images = 0usize;
  161. let mut last_user = String::new();
  162. for m in &messages {
  163. let (t, n) = text_of(m.get("content").unwrap_or(&Value::Null));
  164. prompt_words += t.split_whitespace().count();
  165. images += n;
  166. if m.get("role").and_then(Value::as_str) == Some("user") {
  167. last_user = t;
  168. }
  169. }
  170. let prompt_tokens = (prompt_words + 85 * images + 4 * messages.len()) as i64;
  171. let reply: String = match model.as_str() {
  172. "standin/vision" => format!("I see {images} image{}.", if images == 1 { "" } else { "s" }),
  173. "standin/slow" => (1..=200).map(|i| format!("word{i}")).collect::<Vec<_>>().join(" "),
  174. _ => format!("You said: {last_user}"),
  175. };
  176. let words: Vec<String> = {
  177. let parts: Vec<&str> = reply.split(' ').collect();
  178. parts.iter().enumerate().map(|(i, w)| if i == 0 { w.to_string() } else { format!(" {w}") }).collect()
  179. };
  180. let completion_tokens = words.len() as i64;
  181. let want_usage = body.pointer("/stream_options/include_usage").and_then(Value::as_bool).unwrap_or(false);
  182. let (tx, rx) = mpsc::channel::<Result<Bytes, Infallible>>(8);
  183. let shared2 = shared.clone();
  184. tokio::spawn(async move {
  185. let send = |s: String| {
  186. let tx = tx.clone();
  187. async move { tx.send(Ok(Bytes::from(format!("data: {s}\n\n")))).await.is_ok() }
  188. };
  189. let usage = |cached: i64| {
  190. json!({"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
  191. "total_tokens": prompt_tokens + completion_tokens,
  192. "prompt_tokens_details": {"cached_tokens": cached}})
  193. };
  194. let mut ok = send(chunk(&model, json!({"role": "assistant", "content": ""}), None)).await;
  195. if model == "standin/reasoning" && ok {
  196. for r in ["Checking", " the", " question."] {
  197. ok = ok && send(chunk(&model, json!({"reasoning_content": r}), None)).await;
  198. }
  199. }
  200. let n = words.len();
  201. for (i, w) in words.into_iter().enumerate() {
  202. if !ok {
  203. break;
  204. }
  205. if model == "standin/midstream-error" && i == 3 {
  206. let _ = send(json!({"error": {"message": "model overloaded"}}).to_string()).await;
  207. return;
  208. }
  209. if model == "standin/slow" {
  210. tokio::time::sleep(Duration::from_millis(50)).await;
  211. }
  212. let last = i + 1 == n;
  213. if last && model == "standin/inline-usage" {
  214. let mut c: Value = serde_json::from_str(&chunk(&model, json!({"content": w}), Some("stop"))).unwrap();
  215. c["usage"] = usage(0);
  216. ok = send(c.to_string()).await;
  217. } else {
  218. ok = send(chunk(&model, json!({"content": w}), if last { Some("stop") } else { None })).await;
  219. }
  220. }
  221. if ok && want_usage && model != "standin/no-usage" && model != "standin/inline-usage" {
  222. let cached = if model == "standin/cached" { 40.min(prompt_tokens) } else { 0 };
  223. let mut c =
  224. json!({"id": "standin-1", "object": "chat.completion.chunk", "choices": [], "usage": usage(cached)});
  225. if model == "standin/cached" {
  226. c["usage"]["estimated_cost"] = json!(0.0000123);
  227. }
  228. ok = send(c.to_string()).await;
  229. }
  230. if ok {
  231. ok = send("[DONE]".into()).await;
  232. }
  233. if !ok {
  234. shared2.disconnects.fetch_add(1, Ordering::SeqCst);
  235. }
  236. });
  237. Response::builder()
  238. .header("content-type", "text/event-stream")
  239. .header("cache-control", "no-cache")
  240. .body(Body::from_stream(ReceiverStream::new(rx)))
  241. .unwrap()
  242. }