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() -> String {
"GatewayFormFile".to_owned()
}
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
format: Some("binary".to_owned()),
..Default::default()
}
.into()
}
}
#[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::r#gen::SchemaSettings;
use schemars::schema::{InstanceType, Schema as Json, SchemaObject, SingleOrVec};
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) {
let mut settings = SchemaSettings::draft2019_09();
"#/components/schemas/".clone_into(&mut settings.definitions_path);
settings.meta_schema = None;
let root = settings.into_generator().into_root_schema_for::<T>();
let definitions = root
.definitions
.iter()
.map(|(name, schema)| (name.clone(), convert(schema)))
.collect();
(object(&root.schema), definitions)
}
fn convert(schema: &Json) -> RefOr<Schema> {
match schema {
Json::Bool(_) => ObjectBuilder::new()
.schema_type(SchemaType::AnyValue)
.build()
.into(),
Json::Object(schema) => object(schema),
}
}
fn object(schema: &SchemaObject) -> RefOr<Schema> {
if let Some(reference) = &schema.reference {
return Ref::new(reference.clone()).into();
}
let description = schema.metadata.as_ref().and_then(|m| m.description.clone());
let composite: Option<Schema> = schema.subschemas.as_ref().and_then(|sub| {
let items = |schemas: &Vec<Json>| schemas.iter().map(convert).collect::<Vec<_>>();
if let Some(one_of) = &sub.one_of {
Some(
items(one_of)
.into_iter()
.fold(OneOfBuilder::new(), OneOfBuilder::item)
.into(),
)
} else if let Some(any_of) = &sub.any_of {
Some(
items(any_of)
.into_iter()
.fold(AnyOfBuilder::new(), AnyOfBuilder::item)
.into(),
)
} else {
sub.all_of.as_ref().map(|all_of| {
items(all_of)
.into_iter()
.fold(AllOfBuilder::new(), AllOfBuilder::item)
.into()
})
}
});
let constrained = schema.instance_type.is_some()
|| schema.object.is_some()
|| schema.array.is_some()
|| schema.enum_values.is_some()
|| schema.const_value.is_some();
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: &SchemaObject, description: Option<String>) -> Schema {
let schema_type = match &schema.instance_type {
None => SchemaType::AnyValue,
Some(SingleOrVec::Single(kind)) => SchemaType::Type(type_of(**kind)),
Some(SingleOrVec::Vec(kinds)) => {
SchemaType::Array(kinds.iter().copied().map(type_of).collect())
}
};
if let Some(array) = &schema.array {
let items = match &array.items {
Some(SingleOrVec::Single(item)) => convert(item),
_ => convert(&Json::Bool(true)),
};
return ArrayBuilder::new()
.schema_type(schema_type)
.items(items)
.min_items(array.min_items.map(|n| n as usize))
.max_items(array.max_items.map(|n| n as usize))
.description(description)
.into();
}
let values = schema
.enum_values
.clone()
.or_else(|| schema.const_value.clone().map(|value| vec![value]));
let mut builder = ObjectBuilder::new()
.schema_type(schema_type)
.format(schema.format.clone().map(SchemaFormat::Custom))
.description(description)
.enum_values(values);
if let Some(number) = &schema.number {
builder = builder.minimum(number.minimum).maximum(number.maximum);
}
if let Some(object) = &schema.object {
for (name, property) in &object.properties {
builder = builder.property(name, convert(property));
}
for name in &object.required {
builder = builder.required(name);
}
builder = builder.additional_properties(object.additional_properties.as_deref().map(
|extra| match extra {
Json::Bool(allowed) => AdditionalProperties::FreeForm(*allowed),
Json::Object(_) => AdditionalProperties::RefOr(convert(extra)),
},
));
}
builder.into()
}
fn type_of(kind: InstanceType) -> Type {
match kind {
InstanceType::Null => Type::Null,
InstanceType::Boolean => Type::Boolean,
InstanceType::Object => Type::Object,
InstanceType::Array => Type::Array,
InstanceType::Number => Type::Number,
InstanceType::String => Type::String,
InstanceType::Integer => Type::Integer,
}
}
}
#[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}
}));
}
}