108 lines
5.8 KiB
Rust
108 lines
5.8 KiB
Rust
//! 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<i32> {
|
|
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 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
|
|
}
|