use serde_json::{Value, json};
use crate::{
clock::{GatewayClock, SystemGatewayClock},
ids::GatewayIdGenerator,
objects::{GatewayRequest, GatewayResponse},
server::GatewayRouteState,
storage::{GatewayStateStore, GatewayThread, GatewayThreadMessage},
};
use super::errors::OpenAiRouteError;
pub const THREADS_PATH: &str = "/v1/threads";
pub const THREAD_RETRIEVAL_PREFIX: &str = "/v1/threads/";
pub const THREAD_RETRIEVAL_ROUTE: &str = "/v1/threads/{id}";
pub const THREAD_MESSAGES_ROUTE: &str = "/v1/threads/{id}/messages";
type RouteResult<T> = std::result::Result<T, OpenAiRouteError>;
pub fn handle_threads(request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
let mut clock = SystemGatewayClock;
let seed = clock.now_ms().unwrap_or(1);
let mut ids = GatewayIdGenerator::deterministic("thread", seed);
match state.store().lock() {
Ok(mut store) => create_thread(&mut *store, &mut ids, &mut clock, request)
.unwrap_or_else(OpenAiRouteError::into_response),
Err(err) => OpenAiRouteError::internal_message(format!("gateway store lock failed: {err}"))
.into_response(),
}
}
pub fn handle_thread_post(request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
let Some(thread_id) = message_thread_id_from_path(request.path()) else {
return OpenAiRouteError::not_found_kind("thread", request.path()).into_response();
};
let mut clock = SystemGatewayClock;
let seed = clock.now_ms().unwrap_or(1);
let mut ids = GatewayIdGenerator::deterministic("msg", seed);
match state.store().lock() {
Ok(mut store) => append_message(&mut *store, &mut ids, &mut clock, thread_id, request)
.unwrap_or_else(OpenAiRouteError::into_response),
Err(err) => OpenAiRouteError::internal_message(format!("gateway store lock failed: {err}"))
.into_response(),
}
}
pub fn handle_thread_get(request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
match thread_path(request.path()) {
Some(ThreadPath::Thread(thread_id)) => match state.store().lock() {
Ok(store) => retrieve_thread(&*store, thread_id),
Err(err) => {
OpenAiRouteError::internal_message(format!("gateway store lock failed: {err}"))
.into_response()
}
},
Some(ThreadPath::Messages(thread_id)) => match state.store().lock() {
Ok(store) => list_messages(&*store, thread_id),
Err(err) => {
OpenAiRouteError::internal_message(format!("gateway store lock failed: {err}"))
.into_response()
}
},
None => OpenAiRouteError::not_found_kind("thread", request.path()).into_response(),
}
}
pub fn retrieve_thread<S>(store: &S, thread_id: &str) -> GatewayResponse
where
S: GatewayStateStore,
{
store
.thread(thread_id)
.map(|thread| GatewayResponse::json(200, thread_json(&thread).to_string().into_bytes()))
.unwrap_or_else(|| OpenAiRouteError::not_found_kind("thread", thread_id).into_response())
}
pub fn list_messages<S>(store: &S, thread_id: &str) -> GatewayResponse
where
S: GatewayStateStore,
{
if store.thread(thread_id).is_none() {
return OpenAiRouteError::not_found_kind("thread", thread_id).into_response();
}
let data = store
.thread_messages(thread_id)
.iter()
.map(message_json)
.collect::<Vec<_>>();
GatewayResponse::json(
200,
json!({ "object": "list", "data": data })
.to_string()
.into_bytes(),
)
}
fn create_thread<S, C>(
store: &mut S,
ids: &mut GatewayIdGenerator,
clock: &mut C,
request: &GatewayRequest,
) -> RouteResult<GatewayResponse>
where
S: GatewayStateStore,
C: GatewayClock,
{
let object = request_object(request.body())?;
let thread = GatewayThread::new(
ids.next_id().map_err(OpenAiRouteError::internal)?,
clock.now_ms().map_err(OpenAiRouteError::internal)?,
metadata(object.get("metadata"))?,
);
store
.put_thread(thread.clone())
.map_err(OpenAiRouteError::internal)?;
Ok(GatewayResponse::json(
200,
thread_json(&thread).to_string().into_bytes(),
))
}
fn append_message<S, C>(
store: &mut S,
ids: &mut GatewayIdGenerator,
clock: &mut C,
thread_id: &str,
request: &GatewayRequest,
) -> RouteResult<GatewayResponse>
where
S: GatewayStateStore,
C: GatewayClock,
{
if store.thread(thread_id).is_none() {
return Err(OpenAiRouteError::not_found_kind("thread", thread_id));
}
let object = request_object(request.body())?;
let role = required_string(&object, "role")?.to_owned();
let content = required_string(&object, "content")?.to_owned();
let message = GatewayThreadMessage::new(
ids.next_id().map_err(OpenAiRouteError::internal)?,
thread_id,
role,
content,
clock.now_ms().map_err(OpenAiRouteError::internal)?,
);
store
.put_thread_message(message.clone())
.map_err(OpenAiRouteError::internal)?;
Ok(GatewayResponse::json(
200,
message_json(&message).to_string().into_bytes(),
))
}
use crate::routes::request_json::{request_object_or_empty as request_object, required_string};
fn metadata(value: Option<&Value>) -> RouteResult<Vec<(String, String)>> {
let Some(value) = value else {
return Ok(Vec::new());
};
let object = value.as_object().ok_or_else(|| {
OpenAiRouteError::bad_request(
"metadata must be an object",
Some("metadata"),
"invalid_metadata",
)
})?;
object
.iter()
.map(|(key, value)| {
value
.as_str()
.map(|value| (key.clone(), value.to_owned()))
.ok_or_else(|| {
OpenAiRouteError::bad_request(
"metadata values must be strings",
Some("metadata"),
"invalid_metadata",
)
})
})
.collect()
}
enum ThreadPath<'a> {
Thread(&'a str),
Messages(&'a str),
}
fn thread_path(path: &str) -> Option<ThreadPath<'_>> {
if let Some(thread_id) = message_thread_id_from_path(path) {
return Some(ThreadPath::Messages(thread_id));
}
super::path::id_from_path(path, THREAD_RETRIEVAL_PREFIX).map(ThreadPath::Thread)
}
fn message_thread_id_from_path(path: &str) -> Option<&str> {
super::path::id_from_path_with_suffix(path, THREAD_RETRIEVAL_PREFIX, "/messages")
}
fn thread_json(thread: &GatewayThread) -> Value {
json!({
"id": thread.id(),
"object": "thread",
"created_at": thread.created_at_ms(),
"metadata": metadata_json(thread.metadata()),
})
}
fn message_json(message: &GatewayThreadMessage) -> Value {
json!({
"id": message.id(),
"object": "thread.message",
"thread_id": message.thread_id(),
"role": message.role(),
"content": message.content(),
"created_at": message.created_at_ms(),
})
}
fn metadata_json(metadata: &[(String, String)]) -> Value {
Value::Object(
metadata
.iter()
.map(|(key, value)| (key.clone(), Value::String(value.clone())))
.collect(),
)
}