1use schemars::{JsonSchema, generate::SchemaSettings};
27use serde::{Deserialize, Serialize};
28
29use crate::error;
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct GenerationConfig {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub temperature: Option<f64>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub max_tokens: Option<i32>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub top_p: Option<f64>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub top_k: Option<i32>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub stop_sequences: Option<Vec<String>>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub json_mode: Option<bool>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub json_schema: Option<serde_json::Value>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub max_tool_rounds: Option<usize>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub prompt_caching: Option<bool>,
96}
97
98impl GenerationConfig {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn with_temperature(mut self, temperature: f64) -> Self {
108 self.temperature = Some(temperature);
109 self
110 }
111
112 pub fn with_max_tokens(mut self, max_tokens: i32) -> Self {
114 self.max_tokens = Some(max_tokens);
115 self
116 }
117
118 pub fn with_top_p(mut self, top_p: f64) -> Self {
122 self.top_p = Some(top_p);
123 self
124 }
125
126 pub fn with_top_k(mut self, top_k: i32) -> Self {
128 self.top_k = Some(top_k);
129 self
130 }
131
132 pub fn with_stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
134 self.stop_sequences = Some(stop_sequences);
135 self
136 }
137
138 pub fn with_json_mode(mut self, json_mode: bool) -> Self {
145 self.json_mode = Some(json_mode);
146 self
147 }
148
149 pub fn with_json_schema(mut self, json_schema: serde_json::Value) -> Self {
154 self.json_schema = Some(json_schema);
155 self
156 }
157
158 pub fn with_json_schema_for<T>(mut self) -> error::Result<Self>
175 where
176 T: JsonSchema,
177 {
178 let generator = SchemaSettings::default()
179 .with(|settings| {
180 settings.inline_subschemas = true;
181 settings.meta_schema = None;
182 })
183 .into_generator();
184 let mut schema = serde_json::to_value(generator.into_root_schema_for::<T>())?;
185 normalize_strict_json_schema(&mut schema);
186 self.json_schema = Some(schema);
187 Ok(self)
188 }
189
190 pub fn with_max_tool_rounds(mut self, max_tool_rounds: usize) -> Self {
194 self.max_tool_rounds = Some(max_tool_rounds);
195 self
196 }
197
198 pub fn with_prompt_caching(mut self, enabled: bool) -> Self {
213 self.prompt_caching = Some(enabled);
214 self
215 }
216
217 pub fn tool_round_limit(&self) -> usize {
219 self.max_tool_rounds.unwrap_or(8)
220 }
221}
222
223pub(crate) fn normalize_strict_json_schema(schema: &mut serde_json::Value) {
243 match schema {
244 serde_json::Value::Object(obj) => {
245 obj.remove("$schema");
246
247 let is_object_schema = obj.get("type").and_then(serde_json::Value::as_str)
248 == Some("object")
249 || obj.contains_key("properties");
250
251 if is_object_schema {
252 obj.entry("type")
253 .or_insert(serde_json::Value::String("object".to_string()));
254 obj.entry("additionalProperties")
255 .or_insert(serde_json::Value::Bool(false));
256 }
257
258 for value in obj.values_mut() {
259 normalize_strict_json_schema(value);
260 }
261 }
262 serde_json::Value::Array(items) => {
263 for item in items {
264 normalize_strict_json_schema(item);
265 }
266 }
267 _ => {}
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn builder_chain() {
277 let config = GenerationConfig::new()
278 .with_temperature(0.5)
279 .with_max_tokens(1024)
280 .with_top_p(0.9);
281
282 assert_eq!(config.temperature, Some(0.5));
283 assert_eq!(config.max_tokens, Some(1024));
284 assert_eq!(config.top_p, Some(0.9));
285 }
286
287 #[test]
288 fn tool_round_limit_default() {
289 assert_eq!(GenerationConfig::new().tool_round_limit(), 8);
290 assert_eq!(
291 GenerationConfig::new()
292 .with_max_tool_rounds(3)
293 .tool_round_limit(),
294 3
295 );
296 }
297
298 #[test]
299 fn normalize_adds_additional_properties() {
300 let mut schema = serde_json::json!({
301 "type": "object",
302 "properties": {
303 "name": { "type": "string" }
304 }
305 });
306 normalize_strict_json_schema(&mut schema);
307 assert_eq!(
308 schema["additionalProperties"],
309 serde_json::Value::Bool(false)
310 );
311 }
312
313 #[test]
314 fn normalize_adds_missing_object_type_when_properties_exist() {
315 let mut schema = serde_json::json!({
316 "properties": {
317 "name": { "type": "string" }
318 }
319 });
320 normalize_strict_json_schema(&mut schema);
321 assert_eq!(
322 schema["type"],
323 serde_json::Value::String("object".to_string())
324 );
325 assert_eq!(
326 schema["additionalProperties"],
327 serde_json::Value::Bool(false)
328 );
329 }
330
331 #[test]
332 fn normalize_preserves_explicit_additional_properties() {
333 let mut schema = serde_json::json!({
334 "type": "object",
335 "properties": {
336 "entries": {
337 "type": "object",
338 "additionalProperties": { "type": "string" }
339 }
340 }
341 });
342 normalize_strict_json_schema(&mut schema);
343 assert_eq!(
345 schema["additionalProperties"],
346 serde_json::Value::Bool(false)
347 );
348 assert_eq!(
350 schema["properties"]["entries"]["additionalProperties"],
351 serde_json::json!({ "type": "string" })
352 );
353 }
354
355 #[allow(dead_code)]
356 #[derive(JsonSchema)]
357 struct StructuredAnswer {
358 answer: String,
359 confidence: f64,
360 }
361
362 #[allow(dead_code)]
363 #[derive(JsonSchema)]
364 struct NestedMetadata {
365 tags: Vec<String>,
366 }
367
368 #[allow(dead_code)]
369 #[derive(JsonSchema)]
370 struct StructuredEnvelope {
371 answer: StructuredAnswer,
372 metadata: NestedMetadata,
373 }
374
375 #[allow(dead_code)]
376 #[derive(JsonSchema)]
377 struct Inner {
378 a: u64,
379 b: String,
380 }
381
382 #[allow(dead_code)]
383 #[derive(JsonSchema)]
384 struct Outer {
385 items: Vec<Inner>,
386 }
387
388 #[allow(dead_code)]
389 #[derive(JsonSchema)]
390 enum StructuredChoice {
391 First,
392 Second,
393 }
394
395 #[allow(dead_code)]
396 #[derive(JsonSchema)]
397 struct StructuredWithOptionalAndEnum {
398 required_field: String,
399 optional_field: Option<String>,
400 choice: StructuredChoice,
401 }
402
403 fn assert_no_dollar_keys(value: &serde_json::Value) {
406 match value {
407 serde_json::Value::Object(object) => {
408 for (key, nested) in object {
409 assert!(
410 !key.starts_with('$'),
411 "schema should not contain a '{key}' keyword: {value}"
412 );
413 assert_no_dollar_keys(nested);
414 }
415 }
416 serde_json::Value::Array(items) => {
417 for item in items {
418 assert_no_dollar_keys(item);
419 }
420 }
421 _ => {}
422 }
423 }
424
425 #[test]
426 fn test_generation_config_with_json_schema_for() -> error::Result<()> {
427 let generator = SchemaSettings::default()
433 .with(|settings| {
434 settings.inline_subschemas = true;
435 settings.meta_schema = None;
436 })
437 .into_generator();
438 let mut expected_schema =
439 serde_json::to_value(generator.into_root_schema_for::<StructuredAnswer>())?;
440 normalize_strict_json_schema(&mut expected_schema);
441 let config = GenerationConfig::new().with_json_schema_for::<StructuredAnswer>()?;
442
443 assert_eq!(config.json_schema, Some(expected_schema));
444
445 Ok(())
446 }
447
448 #[test]
449 fn test_generation_config_with_json_schema_for_inlines_nested_objects() -> error::Result<()> {
450 let config = GenerationConfig::new().with_json_schema_for::<StructuredEnvelope>()?;
454 let schema = config.json_schema.expect("schema should be present");
455
456 assert_no_dollar_keys(&schema);
457 assert!(schema.get("$defs").is_none());
458 assert!(schema.get("definitions").is_none());
459
460 assert_eq!(
461 schema["additionalProperties"],
462 serde_json::Value::Bool(false)
463 );
464 assert_eq!(
465 schema["properties"]["answer"]["additionalProperties"],
466 serde_json::Value::Bool(false)
467 );
468 assert_eq!(
469 schema["properties"]["metadata"]["additionalProperties"],
470 serde_json::Value::Bool(false)
471 );
472
473 Ok(())
474 }
475
476 #[test]
477 fn test_generation_config_with_json_schema_for_inlines_vec_of_nested_struct()
478 -> error::Result<()> {
479 let config = GenerationConfig::new().with_json_schema_for::<Outer>()?;
480 let schema = config.json_schema.expect("schema should be present");
481
482 assert_no_dollar_keys(&schema);
483
484 let inner_properties = &schema["properties"]["items"]["items"]["properties"];
488 assert_eq!(inner_properties["a"]["type"], "integer");
489 assert_eq!(inner_properties["b"]["type"], "string");
490
491 assert_eq!(
492 schema["additionalProperties"],
493 serde_json::Value::Bool(false)
494 );
495 assert_eq!(
496 schema["properties"]["items"]["items"]["additionalProperties"],
497 serde_json::Value::Bool(false)
498 );
499
500 Ok(())
501 }
502
503 #[test]
504 fn test_generation_config_with_json_schema_for_optional_and_enum_fields() -> error::Result<()> {
505 let config =
506 GenerationConfig::new().with_json_schema_for::<StructuredWithOptionalAndEnum>()?;
507 let schema = config.json_schema.expect("schema should be present");
508
509 assert_no_dollar_keys(&schema);
510
511 let properties = &schema["properties"];
512 assert!(properties.get("required_field").is_some());
513 assert!(properties.get("optional_field").is_some());
514 assert!(properties.get("choice").is_some());
515
516 let required = schema["required"]
517 .as_array()
518 .expect("required array should be present");
519 let required_names: Vec<&str> = required
520 .iter()
521 .filter_map(serde_json::Value::as_str)
522 .collect();
523 assert!(required_names.contains(&"required_field"));
524 assert!(required_names.contains(&"choice"));
525
526 Ok(())
527 }
528}