use std::sync::Arc;
use axum::http::StatusCode;
use axum::Json;
use ferrox_models::grammar::json_schema::json_schema_to_grammar_value;
use ferrox_models::grammar::{Grammar, SchemaError};
use serde::Deserialize;
use crate::ApiError;
const ROOT: &str = "root";
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResponseFormat {
#[serde(default, rename = "type")]
kind: Option<String>,
#[serde(default)]
json_schema: Option<JsonSchemaSpec>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct JsonSchemaSpec {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
schema: Option<serde_json::Value>,
#[serde(default)]
strict: Option<bool>,
}
pub(crate) fn compile(src: &str) -> Result<Arc<Grammar>, ApiError> {
Grammar::from_str_with_root(src, ROOT)
.map(Arc::new)
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": {
"message": format!("grammar: {e}"),
"type": "invalid_request_error",
"param": "grammar",
}
})),
)
})
}
pub(crate) fn from_schema(
schema: &serde_json::Value,
param: &str,
) -> Result<Arc<Grammar>, ApiError> {
let text = json_schema_to_grammar_value(schema).map_err(|e| schema_refused(&e, param))?;
Grammar::from_str_with_root(&text, ROOT)
.map(Arc::new)
.map_err(|e| {
internal(format!(
"the grammar generated from {param} does not compile: {e}"
))
})
}
pub(crate) fn for_request(
grammar: Option<&str>,
response_format: Option<&serde_json::Value>,
) -> Result<Option<Arc<Grammar>>, ApiError> {
let gbnf = grammar.map(str::trim).filter(|s| !s.is_empty());
let from_format = match response_format {
Some(fmt) => schema_grammar(fmt)?,
None => None,
};
match (gbnf, from_format) {
(Some(_), Some(_)) => Err(two_constraints()),
(Some(src), None) => compile(src).map(Some),
(None, from_format @ Some(_)) => Ok(from_format),
(None, None) => Ok(None),
}
}
fn schema_grammar(fmt: &serde_json::Value) -> Result<Option<Arc<Grammar>>, ApiError> {
let parsed: ResponseFormat = serde_json::from_value(fmt.clone())
.map_err(|e| invalid(format!("response_format: {e}"), "response_format"))?;
let ResponseFormat { kind, json_schema } = parsed;
match kind.as_deref() {
Some("json_schema") => {}
Some("json_object") => {
if json_schema.is_some() {
return Err(invalid(
"response_format carries a \"json_schema\" but asks for type \"json_object\", \
which enforces nothing but the character class; send type \"json_schema\" to \
have the schema enforced",
"response_format",
));
}
return Ok(None);
}
Some(other) => {
return Err(invalid(
format!(
"response_format type {other:?} is not supported (only json_object and \
json_schema)"
),
"response_format",
));
}
None => {
return Err(invalid(
"response_format must include \"type\" (only json_object and json_schema are \
supported)",
"response_format",
));
}
}
let spec = json_schema.ok_or_else(|| {
invalid(
"response_format type \"json_schema\" carries the schema in a \"json_schema\" object: \
{\"type\": \"json_schema\", \"json_schema\": {\"name\": \"…\", \"schema\": {…}}}",
"response_format.json_schema",
)
})?;
let JsonSchemaSpec {
name: _name,
description: _description,
schema,
strict,
} = spec;
if strict == Some(false) {
return Err(invalid(
"response_format json_schema with \"strict\": false is not supported: this server has \
one behaviour for a schema, which is to enforce it with a grammar, and serving that \
under a flag asking for best-effort guidance would report a guarantee the caller \
declined. Send \"strict\": true to have the schema enforced, or response_format \
{\"type\": \"json_object\"} for the best-effort character-class mode.",
"response_format.json_schema.strict",
));
}
let schema = schema.ok_or_else(|| {
invalid(
"response_format json_schema must carry a \"schema\"; there is nothing to enforce \
without one",
"response_format.json_schema.schema",
)
})?;
from_schema(&schema, "response_format.json_schema.schema").map(Some)
}
fn schema_refused(err: &SchemaError, param: &str) -> ApiError {
invalid(format!("{param}: {err}"), param)
}
fn two_constraints() -> ApiError {
invalid(
"a \"grammar\" and a response_format \"json_schema\" are two different constraints on the \
same generation; send one",
"response_format",
)
}
fn invalid(message: impl Into<String>, param: &str) -> ApiError {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": {
"message": message.into(),
"type": "invalid_request_error",
"param": param,
}
})),
)
}
fn internal(message: String) -> ApiError {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": {
"message": message,
"type": "server_error",
}
})),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn schema_format(schema: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"type": "json_schema",
"json_schema": {"name": "answer", "schema": schema},
})
}
fn feed(grammar: &Grammar, pieces: &[&str]) -> Result<bool, String> {
let mut g = grammar.clone();
for (i, piece) in pieces.iter().enumerate() {
g.accept_token(i as u32, piece.as_bytes())
.map_err(|e| format!("piece {piece:?}: {e}"))?;
}
Ok(g.allows_eog())
}
#[test]
fn a_gbnf_grammar_compiles_and_starts_at_root() {
let g = for_request(Some(r#"root ::= "a"+"#), None)
.expect("a valid grammar is not an error")
.expect("a grammar was asked for");
assert!(!g.stacks().is_empty(), "the machine has a live stack");
}
#[test]
fn no_grammar_field_means_no_grammar() {
assert!(for_request(None, None).unwrap().is_none());
assert!(for_request(Some(" "), None).unwrap().is_none());
}
#[test]
fn an_unparseable_grammar_is_a_400_naming_the_field() {
let (status, Json(body)) =
for_request(Some(r#"root ::= "a"#), None).expect_err("this does not parse");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "grammar");
}
#[test]
fn a_grammar_with_no_root_rule_is_refused() {
let (status, _) =
for_request(Some(r#"start ::= "a""#), None).expect_err("there is no root rule");
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn a_json_schema_compiles_to_a_grammar_that_constrains() {
let fmt = schema_format(serde_json::json!({
"type": "object",
"properties": {
"city": {"type": "string"},
"hot": {"type": "boolean"},
},
"required": ["city", "hot"],
"additionalProperties": false,
}));
let g = for_request(None, Some(&fmt))
.expect("this schema converts")
.expect("and it states a grammar");
assert!(
feed(&g, &[r#"{"city": "Rome", "hot": true}"#]).expect("the document is schema-valid"),
"a complete document must finish the parse"
);
assert!(
feed(&g, &[r#"{"city": "Rome"}"#]).is_err(),
"a required property must not be droppable"
);
assert!(
feed(&g, &[r#"{"city": "Rome", "hot": true, "extra"#]).is_err(),
"additionalProperties: false must close the object"
);
assert!(
feed(&g, &[r#"{"city": 3"#]).is_err(),
"a property's own type must be enforced"
);
}
#[test]
fn an_unconvertible_schema_is_a_400_naming_the_keyword() {
let fmt = schema_format(serde_json::json!({
"type": "object",
"properties": {"x": {"allOf": [{"type": "string"}]}},
}));
let (status, Json(body)) = for_request(None, Some(&fmt)).expect_err("allOf has no grammar");
assert_eq!(status, StatusCode::BAD_REQUEST);
let message = body["error"]["message"].as_str().expect("a message");
assert!(
message.contains("allOf"),
"the refusal must name the keyword: {message}"
);
assert_eq!(body["error"]["param"], "response_format.json_schema.schema");
}
#[test]
fn a_grammar_and_a_schema_together_are_refused() {
let fmt = schema_format(serde_json::json!({"type": "string"}));
let (status, Json(body)) = for_request(Some(r#"root ::= "a""#), Some(&fmt))
.expect_err("two constraints, one generation");
assert_eq!(status, StatusCode::BAD_REQUEST);
let message = body["error"]["message"].as_str().expect("a message");
assert!(message.contains("two different constraints"), "{message}");
assert!(for_request(Some(r#"root ::= "a""#), None)
.unwrap()
.is_some());
assert!(for_request(None, Some(&fmt)).unwrap().is_some());
}
#[test]
fn strict_false_is_refused_rather_than_treated_as_strict_true() {
let mut fmt = schema_format(serde_json::json!({"type": "string"}));
fmt["json_schema"]["strict"] = serde_json::json!(false);
let (status, Json(body)) =
for_request(None, Some(&fmt)).expect_err("best-effort is not a mode here");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "response_format.json_schema.strict");
fmt["json_schema"]["strict"] = serde_json::json!(true);
assert!(for_request(None, Some(&fmt)).unwrap().is_some());
let absent = schema_format(serde_json::json!({"type": "string"}));
assert!(for_request(None, Some(&absent)).unwrap().is_some());
}
#[test]
fn an_unknown_member_of_json_schema_is_refused_by_name() {
let mut fmt = schema_format(serde_json::json!({"type": "string"}));
fmt["json_schema"]["max_depth"] = serde_json::json!(3);
let (status, Json(body)) =
for_request(None, Some(&fmt)).expect_err("nothing honours max_depth");
assert_eq!(status, StatusCode::BAD_REQUEST);
let message = body["error"]["message"].as_str().expect("a message");
assert!(message.contains("max_depth"), "{message}");
let stray = serde_json::json!({"type": "json_object", "schema": {"type": "string"}});
let (status, Json(body)) = for_request(None, Some(&stray)).expect_err(
"`schema` is not a \
member of response_format",
);
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(body["error"]["message"]
.as_str()
.expect("a message")
.contains("schema"));
}
#[test]
fn the_label_members_do_not_change_the_grammar() {
let schema = serde_json::json!({"type": "boolean"});
let bare = serde_json::json!({"type": "json_schema", "json_schema": {"schema": schema}});
let labelled = serde_json::json!({
"type": "json_schema",
"json_schema": {
"name": "yes_or_no",
"description": "whether the answer is yes",
"schema": schema,
"strict": true,
},
});
for fmt in [&bare, &labelled] {
let g = for_request(None, Some(fmt)).unwrap().expect("a grammar");
assert!(feed(&g, &["true"]).unwrap());
assert!(
feed(&g, &["\"true\""]).is_err(),
"a boolean is not a string"
);
}
}
#[test]
fn a_json_schema_format_with_no_schema_is_refused() {
let empty = serde_json::json!({"type": "json_schema"});
let (status, Json(body)) =
for_request(None, Some(&empty)).expect_err("there is no schema here");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "response_format.json_schema");
let no_schema = serde_json::json!({
"type": "json_schema",
"json_schema": {"name": "answer"},
});
let (status, Json(body)) =
for_request(None, Some(&no_schema)).expect_err("still nothing to enforce");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "response_format.json_schema.schema");
}
#[test]
fn json_object_is_not_the_schema_arm() {
let fmt = serde_json::json!({"type": "json_object"});
assert!(for_request(None, Some(&fmt)).unwrap().is_none());
assert!(for_request(Some(r#"root ::= "a""#), Some(&fmt))
.unwrap()
.is_some());
}
#[test]
fn an_unknown_response_format_type_is_refused_by_name() {
let text = serde_json::json!({"type": "text"});
let (status, Json(body)) =
for_request(None, Some(&text)).expect_err("chat has no text arm");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(body["error"]["message"]
.as_str()
.expect("a message")
.contains("\"text\""));
let typeless = serde_json::json!({});
let (status, Json(body)) =
for_request(None, Some(&typeless)).expect_err("there is no type");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(body["error"]["message"]
.as_str()
.expect("a message")
.contains("must include"));
}
#[test]
fn the_bare_schema_entry_point_names_the_callers_own_field() {
let good = from_schema(&serde_json::json!({"type": "integer"}), "json_schema")
.expect("integers have a grammar");
assert!(feed(&good, &["42"]).unwrap());
let (status, Json(body)) = from_schema(
&serde_json::json!({"type": "object", "minProperties": 1}),
"json_schema",
)
.expect_err("minProperties has no grammar");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "json_schema");
assert!(body["error"]["message"]
.as_str()
.expect("a message")
.contains("minProperties"));
}
}