use schemars::{JsonSchema, generate::SchemaSettings};
use serde::{Deserialize, Serialize};
use crate::error;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GenerationConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_mode: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_rounds: Option<usize>,
}
impl GenerationConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_temperature(mut self, temperature: f64) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_max_tokens(mut self, max_tokens: i32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_top_p(mut self, top_p: f64) -> Self {
self.top_p = Some(top_p);
self
}
pub fn with_top_k(mut self, top_k: i32) -> Self {
self.top_k = Some(top_k);
self
}
pub fn with_stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
self.stop_sequences = Some(stop_sequences);
self
}
pub fn with_json_mode(mut self, json_mode: bool) -> Self {
self.json_mode = Some(json_mode);
self
}
pub fn with_json_schema(mut self, json_schema: serde_json::Value) -> Self {
self.json_schema = Some(json_schema);
self
}
pub fn with_json_schema_for<T>(mut self) -> error::Result<Self>
where
T: JsonSchema,
{
let generator = SchemaSettings::default()
.with(|settings| {
settings.inline_subschemas = true;
settings.meta_schema = None;
})
.into_generator();
let mut schema = serde_json::to_value(generator.into_root_schema_for::<T>())?;
normalize_strict_json_schema(&mut schema);
self.json_schema = Some(schema);
Ok(self)
}
pub fn with_max_tool_rounds(mut self, max_tool_rounds: usize) -> Self {
self.max_tool_rounds = Some(max_tool_rounds);
self
}
pub fn tool_round_limit(&self) -> usize {
self.max_tool_rounds.unwrap_or(8)
}
}
pub(crate) fn normalize_strict_json_schema(schema: &mut serde_json::Value) {
match schema {
serde_json::Value::Object(obj) => {
obj.remove("$schema");
let is_object_schema = obj.get("type").and_then(serde_json::Value::as_str)
== Some("object")
|| obj.contains_key("properties");
if is_object_schema {
obj.entry("type")
.or_insert(serde_json::Value::String("object".to_string()));
obj.entry("additionalProperties")
.or_insert(serde_json::Value::Bool(false));
}
for value in obj.values_mut() {
normalize_strict_json_schema(value);
}
}
serde_json::Value::Array(items) => {
for item in items {
normalize_strict_json_schema(item);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_chain() {
let config = GenerationConfig::new()
.with_temperature(0.5)
.with_max_tokens(1024)
.with_top_p(0.9);
assert_eq!(config.temperature, Some(0.5));
assert_eq!(config.max_tokens, Some(1024));
assert_eq!(config.top_p, Some(0.9));
}
#[test]
fn tool_round_limit_default() {
assert_eq!(GenerationConfig::new().tool_round_limit(), 8);
assert_eq!(
GenerationConfig::new()
.with_max_tool_rounds(3)
.tool_round_limit(),
3
);
}
#[test]
fn normalize_adds_additional_properties() {
let mut schema = serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
normalize_strict_json_schema(&mut schema);
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false)
);
}
#[test]
fn normalize_adds_missing_object_type_when_properties_exist() {
let mut schema = serde_json::json!({
"properties": {
"name": { "type": "string" }
}
});
normalize_strict_json_schema(&mut schema);
assert_eq!(
schema["type"],
serde_json::Value::String("object".to_string())
);
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false)
);
}
#[test]
fn normalize_preserves_explicit_additional_properties() {
let mut schema = serde_json::json!({
"type": "object",
"properties": {
"entries": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
});
normalize_strict_json_schema(&mut schema);
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false)
);
assert_eq!(
schema["properties"]["entries"]["additionalProperties"],
serde_json::json!({ "type": "string" })
);
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct StructuredAnswer {
answer: String,
confidence: f64,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct NestedMetadata {
tags: Vec<String>,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct StructuredEnvelope {
answer: StructuredAnswer,
metadata: NestedMetadata,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct Inner {
a: u64,
b: String,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct Outer {
items: Vec<Inner>,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
enum StructuredChoice {
First,
Second,
}
#[allow(dead_code)]
#[derive(JsonSchema)]
struct StructuredWithOptionalAndEnum {
required_field: String,
optional_field: Option<String>,
choice: StructuredChoice,
}
fn assert_no_dollar_keys(value: &serde_json::Value) {
match value {
serde_json::Value::Object(object) => {
for (key, nested) in object {
assert!(
!key.starts_with('$'),
"schema should not contain a '{key}' keyword: {value}"
);
assert_no_dollar_keys(nested);
}
}
serde_json::Value::Array(items) => {
for item in items {
assert_no_dollar_keys(item);
}
}
_ => {}
}
}
#[test]
fn test_generation_config_with_json_schema_for() -> error::Result<()> {
let generator = SchemaSettings::default()
.with(|settings| {
settings.inline_subschemas = true;
settings.meta_schema = None;
})
.into_generator();
let mut expected_schema =
serde_json::to_value(generator.into_root_schema_for::<StructuredAnswer>())?;
normalize_strict_json_schema(&mut expected_schema);
let config = GenerationConfig::new().with_json_schema_for::<StructuredAnswer>()?;
assert_eq!(config.json_schema, Some(expected_schema));
Ok(())
}
#[test]
fn test_generation_config_with_json_schema_for_inlines_nested_objects() -> error::Result<()> {
let config = GenerationConfig::new().with_json_schema_for::<StructuredEnvelope>()?;
let schema = config.json_schema.expect("schema should be present");
assert_no_dollar_keys(&schema);
assert!(schema.get("$defs").is_none());
assert!(schema.get("definitions").is_none());
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false)
);
assert_eq!(
schema["properties"]["answer"]["additionalProperties"],
serde_json::Value::Bool(false)
);
assert_eq!(
schema["properties"]["metadata"]["additionalProperties"],
serde_json::Value::Bool(false)
);
Ok(())
}
#[test]
fn test_generation_config_with_json_schema_for_inlines_vec_of_nested_struct()
-> error::Result<()> {
let config = GenerationConfig::new().with_json_schema_for::<Outer>()?;
let schema = config.json_schema.expect("schema should be present");
assert_no_dollar_keys(&schema);
let inner_properties = &schema["properties"]["items"]["items"]["properties"];
assert_eq!(inner_properties["a"]["type"], "integer");
assert_eq!(inner_properties["b"]["type"], "string");
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false)
);
assert_eq!(
schema["properties"]["items"]["items"]["additionalProperties"],
serde_json::Value::Bool(false)
);
Ok(())
}
#[test]
fn test_generation_config_with_json_schema_for_optional_and_enum_fields() -> error::Result<()> {
let config =
GenerationConfig::new().with_json_schema_for::<StructuredWithOptionalAndEnum>()?;
let schema = config.json_schema.expect("schema should be present");
assert_no_dollar_keys(&schema);
let properties = &schema["properties"];
assert!(properties.get("required_field").is_some());
assert!(properties.get("optional_field").is_some());
assert!(properties.get("choice").is_some());
let required = schema["required"]
.as_array()
.expect("required array should be present");
let required_names: Vec<&str> = required
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
assert!(required_names.contains(&"required_field"));
assert!(required_names.contains(&"choice"));
Ok(())
}
}