use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use skald_spec::wire::openai_chat::{OpenAiChatRequest, OpenAiChatResponse};
use skald_spec::wire::openai_embeddings::{OpenAiEmbeddingUsage, OpenAiEmbeddingsRequest};
use skald_spec::wire::openai_responses::{OpenAiResponsesRequest, OpenAiResponsesResponse};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct GatewayChatCompletionsRequest(
pub OpenAiChatRequest,
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct GatewayChatCompletion(
pub OpenAiChatResponse,
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct GatewayResponsesRequest(
pub OpenAiResponsesRequest,
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct GatewayResponse(
pub OpenAiResponsesResponse,
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct GatewayEmbeddingsRequest(
pub OpenAiEmbeddingsRequest,
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayEmbeddings {
pub object: String,
pub model: String,
pub data: Vec<GatewayEmbedding>,
pub usage: OpenAiEmbeddingUsage,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayEmbedding {
pub object: String,
pub index: u32,
pub embedding: GatewayEmbeddingVector,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum GatewayEmbeddingVector {
Float(Vec<f32>),
Base64(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayImageGenerationRequest {
pub model: String,
pub prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GatewayFormFile;
impl JsonSchema for GatewayFormFile {
fn schema_name() -> std::borrow::Cow<'static, str> {
"GatewayFormFile".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "string", "format": "binary" })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayImageEditForm {
pub model: String,
pub image: Vec<GatewayFormFile>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mask: Option<GatewayFormFile>,
pub prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayImageVariationForm {
pub model: String,
pub image: GatewayFormFile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayImages {
pub created: u64,
pub data: Vec<GatewayImage>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayImage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub b64_json: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revised_prompt: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewaySpeechRequest {
pub model: String,
pub input: String,
pub voice: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<f64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayTranscriptionForm {
pub model: String,
pub file: GatewayFormFile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayTranslationForm {
pub model: String,
pub file: GatewayFormFile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayTranscription {
pub text: String,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayModelList {
pub object: String,
pub data: Vec<GatewayModel>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayModel {
pub id: String,
pub object: String,
pub created: u64,
pub owned_by: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayFileUploadForm {
pub purpose: String,
pub file: GatewayFormFile,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayFile {
pub id: String,
pub object: String,
pub bytes: i64,
pub created_at: i64,
pub filename: String,
pub purpose: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayFileDeleted {
pub id: String,
pub object: String,
pub deleted: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayBatchCreateRequest {
pub input_file_id: String,
pub endpoint: String,
pub completion_window: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<BTreeMap<String, String>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayBatch {
pub id: String,
pub object: String,
pub endpoint: String,
pub input_file_id: String,
pub completion_window: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_file_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_file_id: Option<String>,
pub created_at: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<BTreeMap<String, String>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GatewayBatchList {
pub object: String,
pub data: Vec<GatewayBatch>,
pub first_id: Option<String>,
pub last_id: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::IntoParams))]
#[cfg_attr(feature = "server", into_params(parameter_in = Query))]
pub struct GatewayBatchListQuery {
#[serde(default)]
pub after: Option<String>,
#[serde(default)]
#[schemars(range(min = 1, max = 100))]
#[cfg_attr(feature = "server", param(minimum = 1, maximum = 100))]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OpenAiErrorEnvelope {
pub error: OpenAiError,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OpenAiError {
pub message: String,
#[serde(rename = "type")]
pub kind: String,
pub param: Option<String>,
pub code: Option<String>,
}
#[cfg(feature = "server")]
macro_rules! openapi_schema {
($($name:ident),+ $(,)?) => {$(
impl utoipa::PartialSchema for $name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
$crate::gateway::openai::openapi::component::<Self>().0
}
}
impl utoipa::ToSchema for $name {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(stringify!($name))
}
fn schemas(
schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>,
) {
schemas.extend($crate::gateway::openai::openapi::component::<Self>().1);
}
}
)+};
}
#[cfg(feature = "server")]
pub(crate) use openapi_schema;
#[cfg(feature = "server")]
openapi_schema!(
GatewayChatCompletionsRequest,
GatewayChatCompletion,
GatewayResponsesRequest,
GatewayResponse,
GatewayEmbeddingsRequest,
GatewayEmbeddings,
GatewayImageGenerationRequest,
GatewayImageEditForm,
GatewayImageVariationForm,
GatewayImages,
GatewaySpeechRequest,
GatewayTranscriptionForm,
GatewayTranslationForm,
GatewayTranscription,
GatewayModelList,
GatewayFileUploadForm,
GatewayFile,
GatewayFileDeleted,
GatewayFormFile,
GatewayBatchCreateRequest,
GatewayBatch,
GatewayBatchList,
OpenAiErrorEnvelope,
);
#[cfg(feature = "server")]
pub(crate) mod openapi {
use schemars::JsonSchema;
use schemars::generate::SchemaSettings;
use serde_json::{Map, Value};
use utoipa::openapi::schema::{
AdditionalProperties, AllOfBuilder, AnyOfBuilder, ArrayBuilder, ObjectBuilder,
OneOfBuilder, Schema, SchemaFormat, SchemaType, Type,
};
use utoipa::openapi::{Ref, RefOr};
pub(crate) type Components = Vec<(String, RefOr<Schema>)>;
pub(crate) fn component<T: JsonSchema>() -> (RefOr<Schema>, Components) {
const PATH: &str = "/components/schemas";
let mut settings = SchemaSettings::draft2020_12();
settings.definitions_path = PATH.into();
settings.meta_schema = None;
let root = settings
.into_generator()
.into_root_schema_for::<T>()
.to_value();
let schemas = root
.pointer(PATH)
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let wrapped = root
.get("$ref")
.and_then(Value::as_str)
.and_then(|reference| reference.strip_prefix('#'))
.and_then(|pointer| root.pointer(pointer));
let definitions = schemas
.iter()
.map(|(name, schema)| (name.clone(), convert(schema)))
.collect();
(convert(wrapped.unwrap_or(&root)), definitions)
}
fn convert(schema: &Value) -> RefOr<Schema> {
match schema {
Value::Object(schema) => object(schema),
_ => ObjectBuilder::new()
.schema_type(SchemaType::AnyValue)
.build()
.into(),
}
}
fn object(schema: &Map<String, Value>) -> RefOr<Schema> {
if let Some(Value::String(reference)) = schema.get("$ref") {
return Ref::new(reference.clone()).into();
}
let description = schema
.get("description")
.and_then(Value::as_str)
.map(str::to_owned);
let items = |keyword: &str| {
schema
.get(keyword)
.and_then(Value::as_array)
.map(|schemas| schemas.iter().map(convert).collect::<Vec<_>>())
};
let composite: Option<Schema> = if let Some(one_of) = items("oneOf") {
Some(
one_of
.into_iter()
.fold(OneOfBuilder::new(), OneOfBuilder::item)
.into(),
)
} else if let Some(any_of) = items("anyOf") {
Some(
any_of
.into_iter()
.fold(AnyOfBuilder::new(), AnyOfBuilder::item)
.into(),
)
} else {
items("allOf").map(|all_of| {
all_of
.into_iter()
.fold(AllOfBuilder::new(), AllOfBuilder::item)
.into()
})
};
let constrained = ["type", "properties", "items", "enum", "const"]
.iter()
.any(|keyword| schema.contains_key(*keyword));
match composite {
Some(composite) if constrained => AllOfBuilder::new()
.item(plain(schema, None))
.item(composite)
.description(description)
.into(),
Some(composite) => wrap_description(composite, description),
None => plain(schema, description).into(),
}
}
fn wrap_description(composite: Schema, description: Option<String>) -> RefOr<Schema> {
match (composite, description) {
(Schema::OneOf(mut one_of), description) => {
one_of.description = description;
Schema::OneOf(one_of).into()
}
(Schema::AnyOf(mut any_of), description) => {
any_of.description = description;
Schema::AnyOf(any_of).into()
}
(Schema::AllOf(mut all_of), description) => {
all_of.description = description;
Schema::AllOf(all_of).into()
}
(other, _) => other.into(),
}
}
fn plain(schema: &Map<String, Value>, description: Option<String>) -> Schema {
let schema_type = match schema.get("type") {
Some(Value::String(kind)) => SchemaType::Type(type_of(kind)),
Some(Value::Array(kinds)) => SchemaType::Array(
kinds
.iter()
.filter_map(Value::as_str)
.map(type_of)
.collect(),
),
_ => SchemaType::AnyValue,
};
let count = |keyword: &str| {
schema
.get(keyword)
.and_then(Value::as_u64)
.and_then(|n| usize::try_from(n).ok())
};
if schema.get("type").and_then(Value::as_str) == Some("array")
|| schema.contains_key("items")
{
let items = match schema.get("items") {
Some(item @ Value::Object(_)) => convert(item),
_ => convert(&Value::Bool(true)),
};
return ArrayBuilder::new()
.schema_type(schema_type)
.items(items)
.min_items(count("minItems"))
.max_items(count("maxItems"))
.description(description)
.into();
}
let values = schema
.get("enum")
.and_then(Value::as_array)
.cloned()
.or_else(|| schema.get("const").map(|value| vec![value.clone()]));
let mut builder = ObjectBuilder::new()
.schema_type(schema_type)
.format(
schema
.get("format")
.and_then(Value::as_str)
.map(|format| SchemaFormat::Custom(format.to_owned())),
)
.description(description)
.enum_values(values)
.minimum(schema.get("minimum").and_then(Value::as_f64))
.maximum(schema.get("maximum").and_then(Value::as_f64));
if let Some(Value::Object(properties)) = schema.get("properties") {
for (name, property) in properties {
builder = builder.property(name, convert(property));
}
}
if let Some(Value::Array(required)) = schema.get("required") {
for name in required.iter().filter_map(Value::as_str) {
builder = builder.required(name);
}
}
builder =
builder.additional_properties(schema.get("additionalProperties").map(
|extra| match extra {
Value::Bool(allowed) => AdditionalProperties::FreeForm(*allowed),
extra => AdditionalProperties::RefOr(convert(extra)),
},
));
builder.into()
}
fn type_of(kind: &str) -> Type {
match kind {
"null" => Type::Null,
"boolean" => Type::Boolean,
"object" => Type::Object,
"array" => Type::Array,
"number" => Type::Number,
"integer" => Type::Integer,
_ => Type::String,
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn supported_extensions_round_trip() {
fn round_trip<T: serde::de::DeserializeOwned + Serialize>(body: &Value) {
let decoded: T = serde_json::from_value(body.clone()).expect("contract decodes");
assert_eq!(&serde_json::to_value(decoded).expect("encodes"), body);
}
round_trip::<GatewayChatCompletionsRequest>(&json!({
"model": "acme/m", "messages": [{"role": "user", "content": "hi"}],
"vendor_hint": {"cache": true}
}));
round_trip::<GatewayResponsesRequest>(&json!({
"model": "acme/m", "input": [], "vendor_hint": 1
}));
round_trip::<GatewayImageGenerationRequest>(&json!({
"model": "acme/i", "prompt": "a fox", "n": 2, "background": "transparent"
}));
round_trip::<GatewaySpeechRequest>(&json!({
"model": "acme/t", "input": "hi", "voice": "alloy", "stream_format": "audio"
}));
round_trip::<GatewayBatchCreateRequest>(&json!({
"input_file_id": "file-1", "endpoint": "/v1/chat/completions",
"completion_window": "24h", "metadata": {"k": "v"}, "output_expires_after": {"seconds": 3600}
}));
round_trip::<GatewayEmbeddings>(&json!({
"object": "list", "model": "acme/e",
"data": [{"object": "embedding", "index": 0, "embedding": "AAAAAA=="}],
"usage": {"prompt_tokens": 1, "total_tokens": 1}, "vendor": "x"
}));
round_trip::<GatewayBatch>(&json!({
"id": "batch_1", "object": "batch", "endpoint": "/v1/chat/completions",
"input_file_id": "file-1", "completion_window": "24h", "status": "completed",
"output_file_id": "file-1-output", "created_at": 1,
"request_counts": {"total": 1, "completed": 1, "failed": 0}
}));
}
}