pub mod actions;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use serde_json::json;
use tokio::sync::mpsc;
use tracing::{debug, error};
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::openai::actions::{OpenAiProtocol, OPENAI_REQUEST_EVENT};
use crate::state::app_state::AppState;
const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
pub const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
Content-Type: application/json\r\nContent-Length: 83\r\nRetry-After: 5\r\n\
Connection: close\r\n\r\n\
{\"error\":{\"message\":\"server at connection limit\",\"type\":\"server_error\",\"code\":503}}";
pub struct OpenAiServer;
impl OpenAiServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
_send_first: bool,
server_id: crate::state::ServerId,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> anyhow::Result<SocketAddr> {
let first_byte_timeout = first_byte_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(FIRST_BYTE_READ_TIMEOUT);
let idle_timeout = idle_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_REQUESTS_TIMEOUT);
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("OpenAI API server listening on {}", local_addr));
let protocol = Arc::new(OpenAiProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"OpenAI",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
Log::new(Some(&status_tx)).info(format!(
"OpenAI API connection {} from {}",
connection_id, remote_addr
));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let spoke = matches!(
tokio::time::timeout(
first_byte_timeout,
stream.peek(&mut [0u8; 1])
)
.await,
Ok(Ok(n)) if n > 0
);
if spoke {
let io = TokioIo::new(stream);
let status_for_service = status_tx_clone.clone();
let app_state_for_service = app_state_clone.clone();
let activity = Arc::new(
crate::server::accept_bounded::ConnectionActivity::new(),
);
let activity_for_service = Arc::clone(&activity);
let service = service_fn(move |req: Request<Incoming>| {
let llm_clone = llm_client_clone.clone();
let state_clone = app_state_for_service.clone();
let status_clone = status_for_service.clone();
let protocol_clone = protocol_clone.clone();
let activity = Arc::clone(&activity_for_service);
async move {
let _busy = activity.busy();
handle_openai_request(
req,
connection_id,
llm_clone,
state_clone,
status_clone,
protocol_clone,
server_id,
)
.await
}
});
let conn = http1::Builder::new().serve_connection(io, service);
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(err) = result {
error!(
"Error serving OpenAI API connection: {:?}",
err
);
}
}
_ = crate::server::accept_bounded::watch_idle(
Arc::clone(&activity),
idle_timeout,
) => {
Log::new(Some(&status_tx_clone)).debug(format!(
"OpenAI API connection {} idle for {}s; closing \
decision=fail_closed_idle_timeout",
connection_id,
idle_timeout.as_secs()
));
}
}
} else {
Log::new(Some(&status_tx_clone)).debug(format!(
"OpenAI API peer {} sent no request within {}s; closing \
decision=fail_closed_first_byte_timeout",
remote_addr,
first_byte_timeout.as_secs()
));
}
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
Log::new(Some(&status_tx_clone)).info(format!(
"OpenAI API connection {} closed",
connection_id
));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept OpenAI API connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
pub const MAX_REQUEST_BODY_BYTES: usize = 8 * 1024 * 1024;
fn error_response(
status: StatusCode,
message: &str,
error_type: &str,
code: &str,
) -> Response<Full<Bytes>> {
let body = json!({
"error": { "message": message, "type": error_type, "code": code }
})
.to_string();
Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body.clone())))
.unwrap_or_else(|_| {
let mut response = Response::new(Full::new(Bytes::from(body)));
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
response
})
}
async fn handle_openai_request(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OpenAiProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let method = req.method().to_string();
let uri = req.uri().clone();
let path = uri.path().to_string();
Log::new(Some(&status_tx)).debug(format!("OpenAI API request: {} {}", method, path));
let limited = http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BODY_BYTES);
let body_bytes = match limited.collect().await {
Ok(collected) => collected.to_bytes(),
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"OpenAI {} {} decision=fail_closed_body_too_large: {} (limit {} bytes)",
method, path, e, MAX_REQUEST_BODY_BYTES
));
return Ok(error_response(
StatusCode::PAYLOAD_TOO_LARGE,
"request body too large",
"invalid_request_error",
"request_too_large",
));
}
};
let body_text = String::from_utf8_lossy(&body_bytes);
let event = Event::new(
&OPENAI_REQUEST_EVENT,
json!({
"method": method,
"path": path,
"body": if body_text.is_empty() { "" } else { body_text.as_ref() }
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
debug!("LLM OpenAI response received");
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
build_openai_response(
execution_result.protocol_results,
&method,
&path,
&status_tx,
)
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
let status = if failure.is_overloaded() {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
Log::new(Some(&status_tx)).warn(format!(
"OpenAI {} {} decision=fail_closed_llm_error category={}: {}",
method,
path,
if failure.is_overloaded() {
"overloaded"
} else {
"unavailable"
},
e
));
Ok(error_response(
status,
failure.text(),
"server_error",
"internal_error",
))
}
}
}
fn build_openai_response(
protocol_results: Vec<ActionResult>,
method: &str,
path: &str,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<Response<Full<Bytes>>, Infallible> {
for result in &protocol_results {
if let ActionResult::Custom { name, data } = result {
if name == "openai_response" {
let status = match data.get("status").and_then(|v| v.as_u64()) {
None => 200,
Some(code) => u16::try_from(code)
.ok()
.filter(|code| (100..=599).contains(code))
.unwrap_or_else(|| {
error!(
"OpenAI executor produced status {} which is not an HTTP \
status; answering 500",
code
);
500
}),
};
let headers = data
.get("headers")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let body = data.get("body").and_then(|v| v.as_str()).unwrap_or("{}");
Log::new(Some(status_tx))
.debug(format!("OpenAI {} {} -> {}", method, path, status));
let mut response_builder = Response::builder().status(status);
for header in headers {
if let (Some(name_val), Some(value_val)) = (
header.get(0).and_then(|v| v.as_str()),
header.get(1).and_then(|v| v.as_str()),
) {
let candidate = Response::builder().header(name_val, value_val);
if candidate.headers_ref().is_some() {
response_builder = response_builder.header(name_val, value_val);
} else {
error!("OpenAI dropping unusable response header {:?}", name_val);
}
}
}
return Ok(response_builder
.body(Full::new(Bytes::from(body.to_string())))
.unwrap_or_else(|e| {
error!("OpenAI could not build the response ({e}); answering 500");
error_response(
StatusCode::INTERNAL_SERVER_ERROR,
crate::utils::WireFailure::Unavailable.text(),
"server_error",
"internal_error",
)
}));
}
}
}
Log::new(Some(status_tx)).warn(format!(
"OpenAI {} {} decision=fail_closed_no_action: no openai_response action in the result",
method, path
));
Ok(error_response(
StatusCode::INTERNAL_SERVER_ERROR,
crate::utils::WireFailure::Unavailable.text(),
"server_error",
"internal_error",
))
}