use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use mecha_core::agent::{Agent, AgentEvent, Conversation};
use mecha_core::message::Message;
use mecha_core::outbox::{OutboxRoute, OutboxStore};
use mecha_core::session::{Record, RunConfig, Session, SessionMeta};
use serde_json::{json, Value};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::commands::voice_serve::Args;
use crate::GlobalOpts;
const LISTEN_HOST: &str = "127.0.0.1";
const MAX_BODY_BYTES: usize = 8 << 20;
pub(crate) const VOICE_BLOCK: &str = "\
Voice mode: everything you write is spoken aloud by a text-to-speech voice, \
and the user is listening, not reading. Answer in short conversational \
sentences. Make the first sentence a short one, a handful of words: \
speaking begins as soon as that sentence is finished, so a long opener \
is silence the listener sits through. Never use markdown, bullet lists, \
headings, tables or code blocks; write numbers, dates and times as they \
are spoken. When a tool \
returns something long, say the gist in a sentence or two instead of \
reciting it. Before a slow step, say one short line about what you are \
doing. When a message or email was staged for review rather than sent, say \
so out loud. Keep replies brief unless the user asks you to go deep.";
pub struct HostedTurn {
pub events: tokio::sync::mpsc::UnboundedReceiver<AgentEvent>,
pub done: tokio::sync::oneshot::Receiver<Result<HostedAnswer, String>>,
pub cancel: CancellationToken,
}
pub struct HostedAnswer {
pub text: String,
pub input_tokens: u64,
pub output_tokens: u64,
}
pub enum Hosted {
Started(Box<HostedTurn>),
Unknown,
Busy,
Failed(String),
}
#[async_trait::async_trait]
pub trait SessionHost: Send + Sync {
async fn speak(&self, key: &str, utterance: &str, approve_all: bool) -> Hosted;
}
pub(crate) fn open_spoken_turn(text: &str, previous_turn_was_spoken: bool) -> String {
if previous_turn_was_spoken {
text.to_string()
} else {
format!("{VOICE_BLOCK}\n\n{text}")
}
}
struct Slot {
convo: Conversation,
session: Session,
}
enum SlotState {
Idle(Box<Slot>),
Running(CancellationToken),
}
#[derive(Default)]
pub struct Mount {
pub inject_voice_block: bool,
pub approve_all: bool,
pub host: Option<Arc<dyn SessionHost>>,
}
struct Shared {
agent: Arc<Agent>,
mount: Mount,
slots: Mutex<HashMap<String, SlotState>>,
session_dir: PathBuf,
outbox_root: PathBuf,
provider_name: String,
model: String,
config: mecha_core::config::Config,
token: Option<String>,
}
pub struct Facade {
shared: Arc<Shared>,
}
impl Facade {
#[allow(clippy::too_many_arguments)]
pub fn new(
agent: Arc<Agent>,
provider_name: String,
model: String,
config: mecha_core::config::Config,
outbox_root: PathBuf,
token: Option<String>,
mount: Mount,
) -> Result<Self> {
Ok(Self {
shared: Arc::new(Shared {
agent,
mount,
slots: Mutex::new(HashMap::new()),
session_dir: Session::default_dir()?,
outbox_root,
provider_name,
model,
config,
token,
}),
})
}
pub async fn bind(&self, port: u16) -> Result<TcpListener> {
let addr = format!("{LISTEN_HOST}:{port}");
TcpListener::bind(&addr)
.await
.with_context(|| format!("binding {addr}"))
}
pub async fn serve(&self, listener: TcpListener, stop: CancellationToken) -> Result<()> {
loop {
tokio::select! {
accepted = listener.accept() => {
let (stream, _) = accepted?;
let shared = Arc::clone(&self.shared);
tokio::spawn(async move {
if let Err(e) = handle(stream, shared).await {
tracing::debug!("voice connection ended: {e}");
}
});
}
_ = stop.cancelled() => return Ok(()),
}
}
}
pub async fn shutdown(&self) {
{
let slots = self.shared.slots.lock().await;
for state in slots.values() {
if let SlotState::Running(tok) = state {
tok.cancel();
}
}
}
for _ in 0..150 {
let busy = {
let slots = self.shared.slots.lock().await;
slots.values().any(|s| matches!(s, SlotState::Running(_)))
};
if !busy {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
pub async fn run(global: &GlobalOpts, args: Args) -> Result<()> {
let mut opts = global.clone();
opts.global_config_only = true;
opts.system_extra = Some(VOICE_BLOCK.to_string());
if opts.workspace.is_none() {
let dir = mecha_core::work::producer_dir("voice")?;
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
opts.workspace = Some(dir);
}
let prepared = crate::setup::prepare(&opts, false).await?;
let global_cfg = mecha_core::config::Config::load_global()?;
let outbox_root = match global_cfg.outbox.dir.clone() {
Some(dir) => dir,
None => OutboxStore::default_root()?,
};
let workspace = prepared.workspace.clone();
let facade = Facade::new(
Arc::new(prepared.agent),
prepared.provider_name.clone(),
prepared.model.clone(),
prepared.config,
outbox_root,
args.token.clone(),
Mount::default(),
)?;
println!(
"mecha voice-serve · {} ({}) · listening on http://{LISTEN_HOST}:{}/v1/chat/completions · workspace {}",
facade.shared.model,
facade.shared.provider_name,
args.port,
workspace.display()
);
let stop = CancellationToken::new();
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
let listener = facade.bind(args.port).await?;
let server = facade.serve(listener, stop.clone());
tokio::pin!(server);
tokio::select! {
r = &mut server => r?,
_ = tokio::signal::ctrl_c() => stop.cancel(),
_ = sigterm.recv() => stop.cancel(),
}
facade.shutdown().await;
println!("\nvoice-serve: shutting down.");
Ok(())
}
struct Head {
method: String,
path: String,
content_length: usize,
authorization: Option<String>,
session: Option<String>,
chat: Option<String>,
body_start: usize,
}
fn parse_head(buf: &[u8]) -> Result<Option<Head>> {
let mut headers = [httparse::EMPTY_HEADER; 32];
let mut req = httparse::Request::new(&mut headers);
match req.parse(buf)? {
httparse::Status::Partial => Ok(None),
httparse::Status::Complete(body_start) => {
let mut content_length = 0usize;
let mut authorization = None;
let mut session = None;
let mut chat = None;
for h in req.headers.iter() {
if h.name.eq_ignore_ascii_case("content-length") {
content_length = std::str::from_utf8(h.value)?.trim().parse()?;
} else if h.name.eq_ignore_ascii_case("authorization") {
authorization = Some(String::from_utf8_lossy(h.value).into_owned());
} else if h.name.eq_ignore_ascii_case("x-voice-session") {
session = Some(String::from_utf8_lossy(h.value).trim().to_string());
} else if h.name.eq_ignore_ascii_case("x-chat-session") {
chat = Some(String::from_utf8_lossy(h.value).trim().to_string());
}
}
Ok(Some(Head {
method: req.method.unwrap_or("").to_string(),
path: req.path.unwrap_or("").to_string(),
content_length,
authorization,
session,
chat,
body_start,
}))
}
}
}
fn auth_ok(required: &Option<String>, header: &Option<String>) -> bool {
match required {
None => true,
Some(want) => matches!(header, Some(h) if h.strip_prefix("Bearer ") == Some(want)),
}
}
async fn handle(mut stream: TcpStream, shared: Arc<Shared>) -> Result<()> {
let mut buf = Vec::with_capacity(8192);
let head = loop {
let mut chunk = [0u8; 8192];
let n = stream.read(&mut chunk).await?;
if n == 0 {
return Ok(()); }
buf.extend_from_slice(&chunk[..n]);
if let Some(head) = parse_head(&buf)? {
break head;
}
if buf.len() > 1 << 20 {
anyhow::bail!("request head too large");
}
};
if head.method == "POST" && !auth_ok(&shared.token, &head.authorization) {
return write_json(&mut stream, 401, &json!({"error": "unauthorized"})).await;
}
if head.content_length > MAX_BODY_BYTES {
return write_json(&mut stream, 413, &json!({"error": "body too large"})).await;
}
let mut body = buf[head.body_start..].to_vec();
while body.len() < head.content_length {
let mut chunk = [0u8; 8192];
let n = stream.read(&mut chunk).await?;
if n == 0 {
anyhow::bail!("connection closed mid-body");
}
body.extend_from_slice(&chunk[..n]);
}
match (head.method.as_str(), head.path.as_str()) {
("GET", "/health") => write_json(&mut stream, 200, &json!({"status": "ok"})).await,
("POST", "/v1/chat/completions") => completion(&mut stream, &shared, &head, &body).await,
_ => write_json(&mut stream, 404, &json!({"error": "not found"})).await,
}
}
async fn write_json(stream: &mut TcpStream, status: u16, body: &Value) -> Result<()> {
let text = body.to_string();
let reason = match status {
200 => "OK",
400 => "Bad Request",
401 => "Unauthorized",
404 => "Not Found",
_ => "Error",
};
let head = format!(
"HTTP/1.1 {status} {reason}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
text.len()
);
stream.write_all(head.as_bytes()).await?;
stream.write_all(text.as_bytes()).await?;
Ok(())
}
fn session_key(body: &Value, header: &Option<String>) -> String {
if let Some(h) = header {
if !h.is_empty() {
return h.clone();
}
}
body.get("user")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.unwrap_or("default")
.to_string()
}
fn last_user_text(body: &Value) -> Option<String> {
let messages = body.get("messages")?.as_array()?;
let last = messages
.iter()
.rev()
.find(|m| m.get("role").and_then(Value::as_str) == Some("user"))?;
match last.get("content") {
Some(Value::String(s)) if !s.trim().is_empty() => Some(s.clone()),
Some(Value::Array(parts)) => {
let text: String = parts
.iter()
.filter(|p| p.get("type").and_then(Value::as_str) == Some("text"))
.filter_map(|p| p.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join(" ");
if text.trim().is_empty() {
None
} else {
Some(text)
}
}
_ => None,
}
}
fn sse_chunk(id: &str, model: &str, delta: Value, finish: Option<&str>) -> String {
let payload = json!({
"id": id,
"object": "chat.completion.chunk",
"created": chrono::Utc::now().timestamp(),
"model": model,
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
});
format!("data: {payload}\n\n")
}
async fn write_chunk(stream: &mut TcpStream, data: &[u8]) -> std::io::Result<()> {
stream
.write_all(format!("{:x}\r\n", data.len()).as_bytes())
.await?;
stream.write_all(data).await?;
stream.write_all(b"\r\n").await
}
async fn take_slot(
shared: &Arc<Shared>,
key: &str,
) -> Result<Option<(Box<Slot>, CancellationToken)>> {
for _ in 0..200 {
{
let mut slots = shared.slots.lock().await;
match slots.remove(key) {
None => {
let token = CancellationToken::new();
slots.insert(key.to_string(), SlotState::Running(token.clone()));
drop(slots);
let created = Session::create(
&shared.session_dir,
SessionMeta {
id: Session::new_id(),
created_at: chrono::Utc::now(),
provider: shared.provider_name.clone(),
model: shared.model.clone(),
workspace: shared.agent.context().tools.workspace.clone(),
title: Some(format!("voice: {key}")),
},
)
.and_then(|session| {
session.append(&Record::Config(RunConfig::of(
&shared.agent,
&shared.config,
&shared.provider_name,
)))?;
Ok(session)
});
match created {
Ok(session) => {
return Ok(Some((
Box::new(Slot {
convo: Conversation::new(),
session,
}),
token,
)))
}
Err(e) => {
shared.slots.lock().await.remove(key);
return Err(e);
}
}
}
Some(SlotState::Idle(slot)) => {
let token = CancellationToken::new();
slots.insert(key.to_string(), SlotState::Running(token.clone()));
return Ok(Some((slot, token)));
}
Some(SlotState::Running(tok)) => {
tok.cancel();
slots.insert(key.to_string(), SlotState::Running(tok));
}
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
Ok(None)
}
async fn pump(
stream: &mut TcpStream,
id: &str,
model: &str,
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AgentEvent>,
cancel: &CancellationToken,
want_stream: bool,
) -> bool {
if !want_stream {
while rx.recv().await.is_some() {}
return false;
}
let mut disconnected = false;
let head_bytes = "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\ntransfer-encoding: chunked\r\nconnection: close\r\n\r\n";
if stream.write_all(head_bytes.as_bytes()).await.is_err() {
cancel.cancel();
disconnected = true;
}
if !disconnected {
let first = sse_chunk(id, model, json!({"role": "assistant"}), None);
if write_chunk(stream, first.as_bytes()).await.is_err() {
cancel.cancel();
disconnected = true;
}
}
let mut keepalive = tokio::time::interval(Duration::from_secs(5));
keepalive.reset();
loop {
tokio::select! {
ev = rx.recv() => match ev {
Some(AgentEvent::TextDelta(t)) if !disconnected => {
let chunk = sse_chunk(id, model, json!({"content": t}), None);
if write_chunk(stream, chunk.as_bytes()).await.is_err() {
cancel.cancel();
disconnected = true;
}
}
Some(_) => {}
None => break,
},
_ = keepalive.tick() => {
if !disconnected
&& write_chunk(stream, b": ping\n\n").await.is_err() {
cancel.cancel();
disconnected = true;
}
}
}
}
disconnected
}
async fn finish_stream(stream: &mut TcpStream, id: &str, model: &str, error: Option<&str>) {
if let Some(e) = error {
let spoken = sse_chunk(
id,
model,
json!({"content": format!("I hit a problem and could not answer: {e}")}),
None,
);
let _ = write_chunk(stream, spoken.as_bytes()).await;
}
let done = sse_chunk(id, model, json!({}), Some("stop"));
let _ = write_chunk(stream, done.as_bytes()).await;
let _ = write_chunk(stream, b"data: [DONE]\n\n").await;
let _ = stream.write_all(b"0\r\n\r\n").await;
}
async fn hosted_completion(
stream: &mut TcpStream,
shared: &Arc<Shared>,
id: &str,
want_stream: bool,
mut turn: HostedTurn,
) -> Result<()> {
let disconnected = pump(
stream,
id,
&shared.model,
&mut turn.events,
&turn.cancel,
want_stream,
)
.await;
let answer = turn
.done
.await
.unwrap_or_else(|_| Err("the run ended without answering".to_string()));
if want_stream {
if !disconnected {
finish_stream(
stream,
id,
&shared.model,
answer.as_ref().err().map(|e| &**e),
)
.await;
}
return Ok(());
}
match answer {
Ok(a) => {
write_json(
stream,
200,
&json!({
"id": id,
"object": "chat.completion",
"created": chrono::Utc::now().timestamp(),
"model": shared.model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": a.text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": a.input_tokens,
"completion_tokens": a.output_tokens,
},
}),
)
.await
}
Err(e) => write_json(stream, 500, &json!({"error": e})).await,
}
}
async fn completion(
stream: &mut TcpStream,
shared: &Arc<Shared>,
head: &Head,
body: &[u8],
) -> Result<()> {
let body: Value = match serde_json::from_slice(body) {
Ok(v) => v,
Err(_) => {
return write_json(stream, 400, &json!({"error": "invalid JSON body"})).await;
}
};
let key = session_key(&body, &head.session);
let Some(text) = last_user_text(&body) else {
return write_json(stream, 400, &json!({"error": "no user message"})).await;
};
let want_stream = body.get("stream").and_then(Value::as_bool).unwrap_or(false);
let id = format!("chatcmpl-{}", Session::new_id());
if let (Some(chat_key), Some(host)) = (&head.chat, &shared.mount.host) {
if !chat_key.is_empty() {
match host.speak(chat_key, &text, shared.mount.approve_all).await {
Hosted::Started(turn) => {
return hosted_completion(stream, shared, &id, want_stream, *turn).await
}
Hosted::Busy => return write_json(
stream,
503,
&json!({"error": "still finishing the previous step — try again in a moment"}),
)
.await,
Hosted::Failed(e) => {
tracing::error!("voice turn on chat session {chat_key:?} failed: {e}");
return write_json(stream, 500, &json!({"error": e})).await;
}
Hosted::Unknown => {
tracing::warn!(
"voice call named chat session {chat_key:?}, which no front-end \
holds — answering in a conversation of its own instead"
);
}
}
}
}
let Some((mut slot, cancel)) = take_slot(shared, &key).await? else {
return write_json(
stream,
503,
&json!({"error": "still finishing the previous step — try again in a moment"}),
)
.await;
};
let mut cx = (**shared.agent.context()).clone();
cx.cancel = Some(cancel.clone());
if shared.mount.approve_all {
cx.approver = Arc::new(mecha_core::tool::ModeApprover {
mode: mecha_core::config::PermissionMode::Allow,
});
}
if let Some(shared_route) = &shared.agent.context().outbox {
match OutboxStore::open(&shared.outbox_root) {
Ok(store) => {
let mine = OutboxRoute::new(
store,
shared_route.routed().map(String::from).collect::<Vec<_>>(),
shared_route
.publishes()
.map(String::from)
.collect::<Vec<_>>(),
);
mine.set_session_id(&slot.session.meta.id);
cx.outbox = Some(Arc::new(mine));
}
Err(e) => {
tracing::error!("outbox store unavailable, refusing the turn: {e}");
shared.slots.lock().await.insert(key, SlotState::Idle(slot));
return write_json(stream, 503, &json!({"error": "outbox store unavailable"}))
.await;
}
}
}
let text = if shared.mount.inject_voice_block {
open_spoken_turn(&text, !slot.convo.is_empty())
} else {
text
};
let user = Message::user(&text);
slot.convo.push(user.clone());
let _ = slot.session.append(&Record::Message(user));
let recorded = slot.convo.messages.clone();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentEvent>();
let agent = Arc::clone(&shared.agent);
let run = tokio::spawn(async move {
let outcome = agent.run_in(&cx, &mut slot.convo, Some(tx)).await;
(slot, outcome)
});
let disconnected = pump(stream, &id, &shared.model, &mut rx, &cancel, want_stream).await;
let (mut slot, outcome) = match run.await {
Ok(pair) => pair,
Err(e) => {
tracing::error!("voice run task died: {e}");
shared.slots.lock().await.remove(&key);
return Ok(());
}
};
match &outcome {
Ok(o) => {
if slot
.convo
.messages
.last()
.is_some_and(|m| matches!(m.role, mecha_core::message::Role::User))
{
slot.convo.messages.pop();
}
let _ = slot.session.record_run(&recorded, &slot.convo);
let _ = slot.session.record_outcome(o);
let _ = slot.session.append(&Record::Taint(slot.convo.taint));
}
Err(e) => {
tracing::error!("voice run failed: {e}");
slot.convo.messages = recorded.clone();
slot.convo.messages.pop();
}
}
if want_stream {
if !disconnected {
let failed = outcome.as_ref().err().map(|e| format!("{e:#}"));
finish_stream(stream, &id, &shared.model, failed.as_deref()).await;
}
} else {
match &outcome {
Ok(o) => {
let _ = write_json(
stream,
200,
&json!({
"id": id,
"object": "chat.completion",
"created": chrono::Utc::now().timestamp(),
"model": shared.model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": o.text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": o.usage.input_tokens,
"completion_tokens": o.usage.output_tokens,
},
}),
)
.await;
}
Err(e) => {
let _ = write_json(stream, 500, &json!({"error": e.to_string()})).await;
}
}
}
shared.slots.lock().await.insert(key, SlotState::Idle(slot));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_bind_address_is_loopback_and_stays_that_way() {
assert!(LISTEN_HOST.starts_with("127.0.0.1"));
}
#[test]
fn the_voice_block_carries_no_markdown() {
for banned in ["```", "\n- ", "# ", "**"] {
assert!(
!VOICE_BLOCK.contains(banned),
"voice block contains {banned:?}"
);
}
}
#[test]
fn last_user_text_reads_string_content() {
let body = serde_json::json!({"messages": [
{"role": "system", "content": "ignored"},
{"role": "user", "content": "first"},
{"role": "assistant", "content": "reply"},
{"role": "user", "content": "second"},
]});
assert_eq!(last_user_text(&body).as_deref(), Some("second"));
}
#[test]
fn last_user_text_joins_text_parts_and_skips_other_kinds() {
let body = serde_json::json!({"messages": [{"role": "user", "content": [
{"type": "text", "text": "hello"},
{"type": "input_audio", "input_audio": {"data": "zzz"}},
{"type": "text", "text": "there"},
]}]});
assert_eq!(last_user_text(&body).as_deref(), Some("hello there"));
}
#[test]
fn a_history_with_no_user_turn_is_refused_not_guessed() {
let body = serde_json::json!({"messages": [{"role": "system", "content": "x"}]});
assert_eq!(last_user_text(&body), None);
let empty = serde_json::json!({"messages": [{"role": "user", "content": " "}]});
assert_eq!(last_user_text(&empty), None);
}
#[test]
fn session_key_prefers_header_then_user_field_then_default() {
let none = None;
assert_eq!(
session_key(&serde_json::json!({"user": "call-7"}), &none),
"call-7"
);
assert_eq!(session_key(&serde_json::json!({}), &none), "default");
assert_eq!(
session_key(&serde_json::json!({"user": ""}), &none),
"default"
);
let header = Some("conn-abc".to_string());
assert_eq!(
session_key(&serde_json::json!({"user": "call-7"}), &header),
"conn-abc"
);
}
#[test]
fn the_two_session_headers_do_not_bleed_into_each_other() {
let raw = b"POST /v1/chat/completions HTTP/1.1\r\nX-Voice-Session: webrtc-1a2b\r\nX-Chat-Session: main\r\nContent-Length: 0\r\n\r\n";
let head = parse_head(raw).unwrap().expect("complete");
assert_eq!(head.session.as_deref(), Some("webrtc-1a2b"));
assert_eq!(head.chat.as_deref(), Some("main"));
let raw = b"POST /v1/chat/completions HTTP/1.1\r\nX-Voice-Session: webrtc-1a2b\r\nContent-Length: 0\r\n\r\n";
let head = parse_head(raw).unwrap().expect("complete");
assert_eq!(head.session.as_deref(), Some("webrtc-1a2b"));
assert_eq!(head.chat, None);
}
#[test]
fn the_voice_block_opens_a_spoken_stretch_and_nothing_else() {
let opened = open_spoken_turn("what is on my calendar", false);
assert!(opened.starts_with(VOICE_BLOCK));
assert!(opened.ends_with("what is on my calendar"));
assert_eq!(
open_spoken_turn("and tomorrow?", true),
"and tomorrow?",
"a spoken turn following a spoken turn must not re-send the block"
);
}
#[test]
fn parse_head_reads_method_path_length_and_auth() {
let raw = b"POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nAuthorization: Bearer tok\r\n\r\nhello";
let head = parse_head(raw).unwrap().expect("complete");
assert_eq!(head.method, "POST");
assert_eq!(head.path, "/v1/chat/completions");
assert_eq!(head.content_length, 5);
assert_eq!(head.authorization.as_deref(), Some("Bearer tok"));
assert_eq!(&raw[head.body_start..], b"hello");
}
#[test]
fn a_partial_head_asks_for_more_rather_than_erroring() {
assert!(parse_head(b"POST /v1/chat").unwrap().is_none());
}
#[test]
fn auth_is_open_without_a_token_and_exact_with_one() {
assert!(auth_ok(&None, &None));
let want = Some("secret".to_string());
assert!(auth_ok(&want, &Some("Bearer secret".into())));
assert!(!auth_ok(&want, &Some("Bearer wrong".into())));
assert!(!auth_ok(&want, &None));
}
#[test]
fn sse_chunks_are_data_framed_json() {
let chunk = sse_chunk("id1", "m", serde_json::json!({"content": "hi"}), None);
assert!(chunk.starts_with("data: "));
assert!(chunk.ends_with("\n\n"));
let v: Value = serde_json::from_str(chunk.trim_start_matches("data: ").trim()).unwrap();
assert_eq!(v["choices"][0]["delta"]["content"], "hi");
assert!(v["choices"][0]["finish_reason"].is_null());
}
}