use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use axum::extract::{Extension, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use axum::{extract::rejection::JsonRejection, Json};
use serde::{Deserialize, Serialize};
use tensor_wasm_core::types::TenantId;
use tensor_wasm_exec::executor::{ExecError, SpawnConfig};
use tensor_wasm_wasi_gpu::streaming::StreamingContext;
use uuid::Uuid;
use crate::openai_translator::{
translate_chat_completions_request, translate_completions_request, TranslatedRequest,
};
use crate::rate_limit::AuthContext;
use crate::routes::AppState;
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[non_exhaustive]
pub struct CompletionsRequest {
#[serde(default)]
pub model: String,
#[serde(default)]
pub prompt: serde_json::Value,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(default)]
pub echo: Option<bool>,
#[serde(default)]
pub n: Option<u32>,
#[serde(default)]
pub user: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[non_exhaustive]
pub struct ChatMessage {
#[serde(default)]
pub role: String,
#[serde(default)]
pub content: serde_json::Value,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[non_exhaustive]
pub struct ChatCompletionsRequest {
#[serde(default)]
pub model: String,
#[serde(default)]
pub messages: Vec<ChatMessage>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(default)]
pub n: Option<u32>,
#[serde(default)]
pub tools: Option<serde_json::Value>,
#[serde(default)]
pub user: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OpenAiErrorBody {
pub message: String,
#[serde(rename = "type")]
pub kind: String,
pub param: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OpenAiError {
pub error: OpenAiErrorBody,
}
impl OpenAiError {
pub fn not_yet_wired(message: impl Into<String>) -> Self {
Self {
error: OpenAiErrorBody {
message: message.into(),
kind: "not_implemented".to_string(),
param: None,
code: Some("openai_not_yet_wired".to_string()),
},
}
}
pub fn invalid_request(message: impl Into<String>, param: Option<String>) -> Self {
Self {
error: OpenAiErrorBody {
message: message.into(),
kind: "invalid_request_error".to_string(),
param,
code: Some("openai_invalid_request".to_string()),
},
}
}
pub fn with_code(mut self, code: &'static str) -> Self {
self.error.code = Some(code.to_string());
self
}
pub fn model_not_found(message: impl Into<String>) -> Self {
Self {
error: OpenAiErrorBody {
message: message.into(),
kind: "invalid_request_error".to_string(),
param: Some("model".to_string()),
code: Some("model_not_found".to_string()),
},
}
}
pub fn server_error(message: impl Into<String>, code: &'static str) -> Self {
Self {
error: OpenAiErrorBody {
message: message.into(),
kind: "server_error".to_string(),
param: None,
code: Some(code.to_string()),
},
}
}
pub fn tenant_scope_denied(message: impl Into<String>) -> Self {
Self {
error: OpenAiErrorBody {
message: message.into(),
kind: "invalid_request_error".to_string(),
param: None,
code: Some("tenant_scope_denied".to_string()),
},
}
}
}
impl IntoResponse for OpenAiError {
fn into_response(self) -> Response {
let status = match (self.error.kind.as_str(), self.error.code.as_deref()) {
("invalid_request_error", Some("model_not_found")) => StatusCode::NOT_FOUND,
("invalid_request_error", Some("tenant_scope_denied")) => StatusCode::FORBIDDEN,
("invalid_request_error", _) => StatusCode::BAD_REQUEST,
("server_error", _) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::NOT_IMPLEMENTED,
};
(status, Json(self)).into_response()
}
}
const OPENAI_INVOKE_DEADLINE: Duration = Duration::from_secs(30);
const OPENAI_STREAM_BUFFER: usize = 32;
fn unix_seconds_now() -> u64 {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_) => 0,
}
}
fn empty_usage() -> serde_json::Value {
serde_json::json!({
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
})
}
#[tracing::instrument(
name = "http.openai.completions",
skip(state, payload, auth),
fields(model = tracing::field::Empty, stream = tracing::field::Empty),
)]
pub async fn completions_handler(
State(state): State<Arc<AppState>>,
tenant: Option<Extension<TenantId>>,
auth: Option<Extension<AuthContext>>,
payload: Result<Json<CompletionsRequest>, JsonRejection>,
) -> Response {
let tenant = tenant.map(|Extension(t)| t).unwrap_or(TenantId(0));
if let Err(err) = crate::routes::require_authorize(&auth, tenant) {
return err.into_response();
}
let req = match payload {
Ok(Json(r)) => r,
Err(rej) => {
return (
StatusCode::BAD_REQUEST,
Json(OpenAiError::invalid_request(rej.body_text(), None)),
)
.into_response();
}
};
tracing::Span::current().record("model", tracing::field::display(&req.model));
let model_echo = req.model.clone();
let translated = match translate_completions_request(&req, state.openai_model_map.as_ref()) {
Ok(t) => t,
Err(e) => return e.into_response(),
};
tracing::Span::current().record("stream", tracing::field::display(translated.stream));
run_translated(
state,
tenant,
translated,
model_echo,
OpenAiObject::TextCompletion,
)
.await
}
#[tracing::instrument(
name = "http.openai.chat_completions",
skip(state, payload, auth),
fields(model = tracing::field::Empty, stream = tracing::field::Empty),
)]
pub async fn chat_completions_handler(
State(state): State<Arc<AppState>>,
tenant: Option<Extension<TenantId>>,
auth: Option<Extension<AuthContext>>,
payload: Result<Json<ChatCompletionsRequest>, JsonRejection>,
) -> Response {
let tenant = tenant.map(|Extension(t)| t).unwrap_or(TenantId(0));
if let Err(err) = crate::routes::require_authorize(&auth, tenant) {
return err.into_response();
}
let req = match payload {
Ok(Json(r)) => r,
Err(rej) => {
return (
StatusCode::BAD_REQUEST,
Json(OpenAiError::invalid_request(rej.body_text(), None)),
)
.into_response();
}
};
tracing::Span::current().record("model", tracing::field::display(&req.model));
let model_echo = req.model.clone();
let translated = match translate_chat_completions_request(&req, state.openai_model_map.as_ref())
{
Ok(t) => t,
Err(e) => return e.into_response(),
};
tracing::Span::current().record("stream", tracing::field::display(translated.stream));
run_translated(
state,
tenant,
translated,
model_echo,
OpenAiObject::ChatCompletion,
)
.await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpenAiObject {
TextCompletion,
ChatCompletion,
}
impl OpenAiObject {
fn object_kind(self) -> &'static str {
match self {
OpenAiObject::TextCompletion => "text_completion",
OpenAiObject::ChatCompletion => "chat.completion",
}
}
fn chunk_object_kind(self) -> &'static str {
match self {
OpenAiObject::TextCompletion => "text_completion",
OpenAiObject::ChatCompletion => "chat.completion.chunk",
}
}
fn id_prefix(self) -> &'static str {
match self {
OpenAiObject::TextCompletion => "cmpl-",
OpenAiObject::ChatCompletion => "chatcmpl-",
}
}
}
async fn run_translated(
state: Arc<AppState>,
tenant: TenantId,
translated: TranslatedRequest,
model_echo: String,
object_kind: OpenAiObject,
) -> Response {
let wasm_bytes = match state.functions.get(&translated.function_id) {
Some(entry) => {
let owner = entry.value().tenant_id;
if owner != tenant {
return OpenAiError::tenant_scope_denied(
"model maps to a function owned by a different tenant",
)
.into_response();
}
Arc::clone(&entry.value().wasm_bytes)
}
None => {
return OpenAiError::model_not_found(format!(
"model maps to function {} which is no longer deployed",
translated.function_id,
))
.into_response();
}
};
if translated.stream {
run_streaming(
state,
tenant,
wasm_bytes,
translated,
model_echo,
object_kind,
)
.await
} else {
run_buffered(
state,
tenant,
wasm_bytes,
translated,
model_echo,
object_kind,
)
.await
}
}
async fn run_buffered(
state: Arc<AppState>,
tenant: TenantId,
wasm_bytes: Arc<[u8]>,
translated: TranslatedRequest,
model_echo: String,
object_kind: OpenAiObject,
) -> Response {
let (chunk_tx, mut chunk_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(OPENAI_STREAM_BUFFER);
let streaming = StreamingContext::with_channel(chunk_tx);
let executor = state.executor.clone();
let args = translated.args.clone();
let function_id = translated.function_id;
let input = translated.prompt.into_bytes();
let exec_handle = tokio::spawn(async move {
let cfg = SpawnConfig::for_tenant(tenant)
.with_deadline(OPENAI_INVOKE_DEADLINE)
.with_streaming(streaming)
.with_input(input);
let instance_id = executor.spawn_instance(cfg, &wasm_bytes).await?;
executor
.call_export_with_args_then_terminate(instance_id, "_start", &args)
.await
.map(|_| ())
});
let mut collected: Vec<u8> = Vec::new();
while let Some(chunk) = chunk_rx.recv().await {
if collected.len() + chunk.len() > 16 * 1024 * 1024 {
tracing::warn!(
target: "tensor_wasm_api::openai",
function_id = %function_id,
"guest output exceeded 16 MiB on the buffered (non-streaming) branch; \
truncating",
);
break;
}
collected.extend_from_slice(&chunk);
}
let exec_result = match exec_handle.await {
Ok(r) => r,
Err(e) => {
return OpenAiError::server_error(format!("executor task panicked: {e}"), "wasm_error")
.into_response();
}
};
if let Err(e) = exec_result {
return map_exec_error_to_openai(e).into_response();
}
let text = String::from_utf8_lossy(&collected).into_owned();
let response_id = format!("{}{}", object_kind.id_prefix(), Uuid::new_v4());
let created = unix_seconds_now();
let envelope = match object_kind {
OpenAiObject::TextCompletion => serde_json::json!({
"id": response_id,
"object": object_kind.object_kind(),
"created": created,
"model": model_echo,
"choices": [{
"text": text,
"index": 0,
"finish_reason": "stop",
"logprobs": serde_json::Value::Null,
}],
"usage": empty_usage(),
}),
OpenAiObject::ChatCompletion => serde_json::json!({
"id": response_id,
"object": object_kind.object_kind(),
"created": created,
"model": model_echo,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": text,
},
"finish_reason": "stop",
}],
"usage": empty_usage(),
}),
};
(StatusCode::OK, Json(envelope)).into_response()
}
async fn run_streaming(
state: Arc<AppState>,
tenant: TenantId,
wasm_bytes: Arc<[u8]>,
translated: TranslatedRequest,
model_echo: String,
object_kind: OpenAiObject,
) -> Response {
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(OPENAI_STREAM_BUFFER);
let streaming = StreamingContext::with_channel(chunk_tx);
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<Result<(), ExecError>>();
let executor = state.executor.clone();
let args = translated.args.clone();
let input = translated.prompt.into_bytes();
tokio::spawn(async move {
let cfg = SpawnConfig::for_tenant(tenant)
.with_deadline(OPENAI_INVOKE_DEADLINE)
.with_streaming(streaming)
.with_input(input);
let outcome = match executor.spawn_instance(cfg, &wasm_bytes).await {
Ok(instance_id) => executor
.call_export_with_args_then_terminate(instance_id, "_start", &args)
.await
.map(|_| ()),
Err(e) => Err(e),
};
let _ = done_tx.send(outcome);
});
let response_id = format!("{}{}", object_kind.id_prefix(), Uuid::new_v4());
let created = unix_seconds_now();
let initial = OpenAiStreamState::Streaming {
rx: chunk_rx,
done_rx,
};
let sse_stream = futures::stream::unfold(initial, move |st| {
let response_id = response_id.clone();
let model_echo = model_echo.clone();
async move {
match st {
OpenAiStreamState::Streaming {
mut rx,
mut done_rx,
} => {
tokio::select! {
biased;
maybe = rx.recv() => match maybe {
Some(chunk) => {
let ev = make_chunk_event(
&response_id,
created,
&model_echo,
object_kind,
&chunk,
);
Some((
Ok::<Event, std::convert::Infallible>(ev),
OpenAiStreamState::Streaming { rx, done_rx },
))
}
None => {
let terminal = done_rx.await.unwrap_or(Ok(()));
Some((
Ok(make_terminal_event(
&response_id,
created,
&model_echo,
object_kind,
terminal.as_ref().err(),
)),
OpenAiStreamState::EmitDone,
))
}
},
done = &mut done_rx => {
let terminal = done.unwrap_or(Ok(()));
match rx.try_recv() {
Ok(chunk) => {
let ev = make_chunk_event(
&response_id,
created,
&model_echo,
object_kind,
&chunk,
);
Some((
Ok(ev),
OpenAiStreamState::DrainOnly {
rx,
terminal: Some(terminal),
},
))
}
Err(_) => Some((
Ok(make_terminal_event(
&response_id,
created,
&model_echo,
object_kind,
terminal.as_ref().err(),
)),
OpenAiStreamState::EmitDone,
)),
}
}
}
}
OpenAiStreamState::DrainOnly { mut rx, terminal } => match rx.try_recv() {
Ok(chunk) => {
let ev = make_chunk_event(
&response_id,
created,
&model_echo,
object_kind,
&chunk,
);
Some((Ok(ev), OpenAiStreamState::DrainOnly { rx, terminal }))
}
Err(_) => Some((
Ok(make_terminal_event(
&response_id,
created,
&model_echo,
object_kind,
terminal.as_ref().and_then(|r| r.as_ref().err()),
)),
OpenAiStreamState::EmitDone,
)),
},
OpenAiStreamState::EmitDone => Some((
Ok(Event::default().data("[DONE]")),
OpenAiStreamState::Closed,
)),
OpenAiStreamState::Closed => None,
}
}
});
Sse::new(sse_stream)
.keep_alive(KeepAlive::default())
.into_response()
}
enum OpenAiStreamState {
Streaming {
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
done_rx: tokio::sync::oneshot::Receiver<Result<(), ExecError>>,
},
DrainOnly {
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
terminal: Option<Result<(), ExecError>>,
},
EmitDone,
Closed,
}
fn make_chunk_event(
id: &str,
created: u64,
model: &str,
object_kind: OpenAiObject,
chunk_bytes: &[u8],
) -> Event {
let text = String::from_utf8_lossy(chunk_bytes).into_owned();
let payload = match object_kind {
OpenAiObject::TextCompletion => serde_json::json!({
"id": id,
"object": object_kind.chunk_object_kind(),
"created": created,
"model": model,
"choices": [{
"text": text,
"index": 0,
"finish_reason": serde_json::Value::Null,
"logprobs": serde_json::Value::Null,
}],
}),
OpenAiObject::ChatCompletion => serde_json::json!({
"id": id,
"object": object_kind.chunk_object_kind(),
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": { "content": text },
"finish_reason": serde_json::Value::Null,
}],
}),
};
Event::default().data(payload.to_string())
}
fn make_terminal_event(
id: &str,
created: u64,
model: &str,
object_kind: OpenAiObject,
err: Option<&ExecError>,
) -> Event {
let payload = if let Some(err) = err {
serde_json::json!({
"id": id,
"object": object_kind.chunk_object_kind(),
"created": created,
"model": model,
"error": {
"message": crate::routes::sanitised_exec_error_message(err),
"type": "server_error",
"code": exec_error_code(err),
},
})
} else {
match object_kind {
OpenAiObject::TextCompletion => serde_json::json!({
"id": id,
"object": object_kind.chunk_object_kind(),
"created": created,
"model": model,
"choices": [{
"text": "",
"index": 0,
"finish_reason": "stop",
"logprobs": serde_json::Value::Null,
}],
}),
OpenAiObject::ChatCompletion => serde_json::json!({
"id": id,
"object": object_kind.chunk_object_kind(),
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop",
}],
}),
}
};
Event::default().data(payload.to_string())
}
fn map_exec_error_to_openai(err: ExecError) -> OpenAiError {
let code = exec_error_code(&err);
OpenAiError::server_error(format!("{err}"), code)
}
fn exec_error_code(err: &ExecError) -> &'static str {
match err {
ExecError::Timeout(_) => "deadline_elapsed",
ExecError::CapacityExhausted { .. } => "capacity_exhausted",
ExecError::TenantCapacityExhausted { .. } => "tenant_capacity_exhausted",
ExecError::ModuleMemoryTooLarge { .. } => "module_memory_too_large",
ExecError::ModuleTooLarge { .. } => "module_too_large",
ExecError::MissingExport(_) => "missing_export",
ExecError::EpochTickerNotRunning => "epoch_ticker_not_running",
ExecError::NotFound(_) => "instance_not_found",
ExecError::Wasmtime(_) => "wasm_error",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completions_request_deserialises_minimal_body() {
let raw = r#"{"model":"gpt-3.5-turbo","prompt":"hello"}"#;
let parsed: CompletionsRequest = serde_json::from_str(raw).expect("parses");
assert_eq!(parsed.model, "gpt-3.5-turbo");
assert_eq!(parsed.prompt, serde_json::json!("hello"));
assert!(parsed.max_tokens.is_none());
}
#[test]
fn completions_request_accepts_array_prompt() {
let raw = r#"{"model":"m","prompt":["a","b","c"]}"#;
let parsed: CompletionsRequest = serde_json::from_str(raw).expect("parses");
assert!(parsed.prompt.is_array());
}
#[test]
fn chat_completions_request_deserialises_minimal_body() {
let raw = r#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}"#;
let parsed: ChatCompletionsRequest = serde_json::from_str(raw).expect("parses");
assert_eq!(parsed.model, "gpt-4");
assert_eq!(parsed.messages.len(), 1);
assert_eq!(parsed.messages[0].role, "user");
}
#[test]
fn chat_completions_request_accepts_empty_messages() {
let raw = r#"{"model":"m","messages":[]}"#;
let parsed: ChatCompletionsRequest = serde_json::from_str(raw).expect("parses");
assert!(parsed.messages.is_empty());
}
#[test]
fn openai_error_envelope_serialises_to_openai_shape() {
let env = OpenAiError::not_yet_wired("nope");
let v = serde_json::to_value(&env).expect("serialises");
let inner = v.get("error").expect("error key present");
assert_eq!(inner.get("message").and_then(|x| x.as_str()), Some("nope"));
assert_eq!(
inner.get("type").and_then(|x| x.as_str()),
Some("not_implemented"),
);
assert!(inner.get("param").is_some_and(|x| x.is_null()));
assert_eq!(
inner.get("code").and_then(|x| x.as_str()),
Some("openai_not_yet_wired"),
);
}
#[test]
fn openai_error_invalid_request_carries_param() {
let env = OpenAiError::invalid_request("bad", Some("model".to_string()));
let v = serde_json::to_value(&env).expect("serialises");
let inner = v.get("error").unwrap();
assert_eq!(
inner.get("type").and_then(|x| x.as_str()),
Some("invalid_request_error"),
);
assert_eq!(inner.get("param").and_then(|x| x.as_str()), Some("model"),);
}
}