//! The host proxy catalog selects distinct pi-ai transports with shared proxy authentication. use sausage::{ exec::{Command, Exec, Options, Stream}, world::World, }; use std::{sync::Arc, time::Duration}; struct NoExec; #[async_trait::async_trait] impl Exec for NoExec { async fn exec( &self, _: Command, _: Options, _: &mut (dyn for<'a> FnMut(Stream, &'a [u8]) + Send), ) -> anyhow::Result { anyhow::bail!("unexpected world command") } } fn event(name: &str, value: serde_json::Value) -> String { format!("event: {name}\ndata: {value}\n\n") } #[tokio::test(flavor = "current_thread")] async fn provider_transport_and_prices() -> anyhow::Result<()> { use serde_json::json; // Production initializes this in the process runtime; this fixture uses NoExec. let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let server = tiny_http::Server::http("127.0.0.1:0").map_err(|e| anyhow::anyhow!(e))?; let base = format!("http://{}", server.server_addr()); let proxy = json!({"base_url":base,"api_key":"test-proxy-key"}); let thread = std::thread::spawn(move || -> anyhow::Result<()> { for path in [ "/v1/models", "/v1/responses", "/apis/anthropic/v1/messages?beta=true", "/offline/v1/models", ] { let mut request = server .recv_timeout(Duration::from_secs(20))? .ok_or_else(|| anyhow::anyhow!("request timeout"))?; assert_eq!(request.url(), path); let authenticated = request.headers().iter().any(|h| { (h.field.equiv("Authorization") && h.value.as_str() == "Bearer test-proxy-key") || (h.field.equiv("x-api-key") && h.value.as_str() == "test-proxy-key") }); assert!(authenticated, "missing proxy key"); if path == "/offline/v1/models" { request.respond(tiny_http::Response::empty(503))?; continue; } let (body, mime) = if path == "/v1/models" { let rows: Vec<_> = [("openai", "gpt-6.1-sol", "openai-responses", "/v1/responses"), ("anthropic", "claude-opus-5-5", "anthropic-messages", "/apis/anthropic/v1/messages")] .into_iter().map(|(creator,id,api,path)|json!({"creator":creator,"id":id,"endpoints":[{"api":api,"path":path}], "providers":[{"pricing":{"currency":"USD","unit_tokens":1000,"input":0.002,"output":0.008,"cached_input":0.0001,"cache_write":0.003}}]})).collect(); (json!({"data":rows}).to_string(), "application/json") } else { let mut body = String::new(); request.as_reader().read_to_string(&mut body)?; let body: serde_json::Value = serde_json::from_str(&body)?; assert_eq!(body["stream"], true); assert!( request .headers() .iter() .any(|h| h.field.equiv("Shopify-Usage-Tag") && h.value.as_str() == "[\"pi\",\"sausage\"]") ); let stream = if path == "/v1/responses" { assert_eq!(body["model"], "gpt-6.1-sol"); [ event("response.output_item.added", json!({"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"m","role":"assistant","content":[]}})), event("response.content_part.added", json!({"type":"response.content_part.added","output_index":0,"content_index":0,"part":{"type":"output_text","text":""}})), event("response.output_text.delta", json!({"type":"response.output_text.delta","output_index":0,"content_index":0,"delta":"hello"})), event("response.completed", json!({"type":"response.completed","response":{"id":"r","status":"completed","output":[],"usage":{"input_tokens":10,"output_tokens":1,"total_tokens":11}}})), ].concat() } else { assert_eq!(body["model"], "claude-opus-5-5"); assert_eq!(body["thinking"]["type"], "adaptive"); assert_eq!(body["output_config"]["effort"], "low"); [ event("message_start", json!({"type":"message_start","message":{"id":"m","type":"message","role":"assistant","model":"claude-opus-5-5","content":[],"usage":{"input_tokens":10,"output_tokens":0}}})), event("content_block_start", json!({"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}})), event("content_block_delta", json!({"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}})), event("content_block_stop", json!({"type":"content_block_stop","index":0})), event("message_delta", json!({"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}})), event("message_stop", json!({"type":"message_stop"})), ].concat() }; (stream, "text/event-stream") }; request.respond( tiny_http::Response::from_string(body) .with_header(tiny_http::Header::from_bytes("Content-Type", mime).unwrap()), )?; } Ok(()) }); let dir = tempfile::tempdir()?; let config = dir.path().join("world/etc/sausage"); std::fs::create_dir_all(&config)?; std::fs::write( config.join("config.yaml"), serde_json::to_vec(&json!({"proxy":proxy}))?, )?; let source = format!( "globalThis.testProxy = {proxy};\n{}", include_str!("../../js/proxy-models-test-bundle.js") ); let result = sausage::harness::run_module(World::open(dir.path())?, Arc::new(NoExec), &source).await; thread.join().unwrap()?; result }