1use schemars::{JsonSchema, generate::SchemaSettings};
26use serde::{Deserialize, Serialize};
27
28use crate::error;
29
30#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59pub struct GenerationConfig {
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub temperature: Option<f64>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub max_tokens: Option<i32>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub top_p: Option<f64>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub top_k: Option<i32>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub stop_sequences: Option<Vec<String>>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub json_mode: Option<bool>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub json_schema: Option<serde_json::Value>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub max_tool_rounds: Option<usize>,
91}
92
93impl GenerationConfig {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn with_temperature(mut self, temperature: f64) -> Self {
103 self.temperature = Some(temperature);
104 self
105 }
106
107 pub fn with_max_tokens(mut self, max_tokens: i32) -> Self {
109 self.max_tokens = Some(max_tokens);
110 self
111 }
112
113 pub fn with_top_p(mut self, top_p: f64) -> Self {
117 self.top_p = Some(top_p);
118 self
119 }
120
121 pub fn with_top_k(mut self, top_k: i32) -> Self {
123 self.top_k = Some(top_k);
124 self
125 }
126
127 pub fn with_stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
129 self.stop_sequences = Some(stop_sequences);
130 self
131 }
132
133 pub fn with_json_mode(mut self, json_mode: bool) -> Self {
140 self.json_mode = Some(json_mode);
141 self
142 }
143
144 pub fn with_json_schema(mut self, json_schema: serde_json::Value) -> Self {
149 self.json_schema = Some(json_schema);
150 self
151 }
152
153 pub fn with_json_schema_for<T>(mut self) -> error::Result<Self>
170 where
171 T: JsonSchema,
172 {
173 let generator = SchemaSettings::default()
174 .with(|settings| {
175 settings.inline_subschemas = true;
176 settings.meta_schema = None;
177 })
178 .into_generator();
179 let mut schema = serde_json::to_value(generator.into_root_schema_for::<T>())?;
180 normalize_strict_json_schema(&mut schema);
181 self.json_schema = Some(schema);
182 Ok(self)
183 }
184
185 pub fn with_max_tool_rounds(mut self, max_tool_rounds: usize) -> Self {
189 self.max_tool_rounds = Some(max_tool_rounds);
190 self
191 }
192
193 pub fn tool_round_limit(&self) -> usize {
195 self.max_tool_rounds.unwrap_or(8)
196 }
197}
198
199pub(crate) fn normalize_strict_json_schema(schema: &mut serde_json::Value) {
219 match schema {
220 serde_json::Value::Object(obj) => {
221 obj.remove("$schema");
222
223 let is_object_schema = obj.get("type").and_then(serde_json::Value::as_str)
224 == Some("object")
225 || obj.contains_key("properties");
226
227 if is_object_schema {
228 obj.entry("type")
229 .or_insert(serde_json::Value::String("object".to_string()));
230 obj.entry("additionalProperties")
231 .or_insert(serde_json::Value::Bool(false));
232 }
233
234 for value in obj.values_mut() {
235 normalize_strict_json_schema(value);
236 }
237 }
238 serde_json::Value::Array(items) => {
239 for item in items {
240 normalize_strict_json_schema(item);
241 }
242 }
243 _ => {}
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn builder_chain() {
253 let config = GenerationConfig::new()
254 .with_temperature(0.5)
255 .with_max_tokens(1024)
256 .with_top_p(0.9);
257
258 assert_eq!(config.temperature, Some(0.5));
259 assert_eq!(config.max_tokens, Some(1024));
260 assert_eq!(config.top_p, Some(0.9));
261 }
262
263 #[test]
264 fn tool_round_limit_default() {
265 assert_eq!(GenerationConfig::new().tool_round_limit(), 8);
266 assert_eq!(
267 GenerationConfig::new()
268 .with_max_tool_rounds(3)
269 .tool_round_limit(),
270 3
271 );
272 }
273
274 #[test]
275 fn normalize_adds_additional_properties() {
276 let mut schema = serde_json::json!({
277 "type": "object",
278 "properties": {
279 "name": { "type": "string" }
280 }
281 });
282 normalize_strict_json_schema(&mut schema);
283 assert_eq!(
284 schema["additionalProperties"],
285 serde_json::Value::Bool(false)
286 );
287 }
288
289 #[test]
290 fn normalize_adds_missing_object_type_when_properties_exist() {
291 let mut schema = serde_json::json!({
292 "properties": {
293 "name": { "type": "string" }
294 }
295 });
296 normalize_strict_json_schema(&mut schema);
297 assert_eq!(
298 schema["type"],
299 serde_json::Value::String("object".to_string())
300 );
301 assert_eq!(
302 schema["additionalProperties"],
303 serde_json::Value::Bool(false)
304 );
305 }
306
307 #[test]
308 fn normalize_preserves_explicit_additional_properties() {
309 let mut schema = serde_json::json!({
310 "type": "object",
311 "properties": {
312 "entries": {
313 "type": "object",
314 "additionalProperties": { "type": "string" }
315 }
316 }
317 });
318 normalize_strict_json_schema(&mut schema);
319 assert_eq!(
321 schema["additionalProperties"],
322 serde_json::Value::Bool(false)
323 );
324 assert_eq!(
326 schema["properties"]["entries"]["additionalProperties"],
327 serde_json::json!({ "type": "string" })
328 );
329 }
330
331 #[allow(dead_code)]
332 #[derive(JsonSchema)]
333 struct StructuredAnswer {
334 answer: String,
335 confidence: f64,
336 }
337
338 #[allow(dead_code)]
339 #[derive(JsonSchema)]
340 struct NestedMetadata {
341 tags: Vec<String>,
342 }
343
344 #[allow(dead_code)]
345 #[derive(JsonSchema)]
346 struct StructuredEnvelope {
347 answer: StructuredAnswer,
348 metadata: NestedMetadata,
349 }
350
351 #[allow(dead_code)]
352 #[derive(JsonSchema)]
353 struct Inner {
354 a: u64,
355 b: String,
356 }
357
358 #[allow(dead_code)]
359 #[derive(JsonSchema)]
360 struct Outer {
361 items: Vec<Inner>,
362 }
363
364 #[allow(dead_code)]
365 #[derive(JsonSchema)]
366 enum StructuredChoice {
367 First,
368 Second,
369 }
370
371 #[allow(dead_code)]
372 #[derive(JsonSchema)]
373 struct StructuredWithOptionalAndEnum {
374 required_field: String,
375 optional_field: Option<String>,
376 choice: StructuredChoice,
377 }
378
379 fn assert_no_dollar_keys(value: &serde_json::Value) {
382 match value {
383 serde_json::Value::Object(object) => {
384 for (key, nested) in object {
385 assert!(
386 !key.starts_with('$'),
387 "schema should not contain a '{key}' keyword: {value}"
388 );
389 assert_no_dollar_keys(nested);
390 }
391 }
392 serde_json::Value::Array(items) => {
393 for item in items {
394 assert_no_dollar_keys(item);
395 }
396 }
397 _ => {}
398 }
399 }
400
401 #[test]
402 fn test_generation_config_with_json_schema_for() -> error::Result<()> {
403 let generator = SchemaSettings::default()
409 .with(|settings| {
410 settings.inline_subschemas = true;
411 settings.meta_schema = None;
412 })
413 .into_generator();
414 let mut expected_schema =
415 serde_json::to_value(generator.into_root_schema_for::<StructuredAnswer>())?;
416 normalize_strict_json_schema(&mut expected_schema);
417 let config = GenerationConfig::new().with_json_schema_for::<StructuredAnswer>()?;
418
419 assert_eq!(config.json_schema, Some(expected_schema));
420
421 Ok(())
422 }
423
424 #[test]
425 fn test_generation_config_with_json_schema_for_inlines_nested_objects() -> error::Result<()> {
426 let config = GenerationConfig::new().with_json_schema_for::<StructuredEnvelope>()?;
430 let schema = config.json_schema.expect("schema should be present");
431
432 assert_no_dollar_keys(&schema);
433 assert!(schema.get("$defs").is_none());
434 assert!(schema.get("definitions").is_none());
435
436 assert_eq!(
437 schema["additionalProperties"],
438 serde_json::Value::Bool(false)
439 );
440 assert_eq!(
441 schema["properties"]["answer"]["additionalProperties"],
442 serde_json::Value::Bool(false)
443 );
444 assert_eq!(
445 schema["properties"]["metadata"]["additionalProperties"],
446 serde_json::Value::Bool(false)
447 );
448
449 Ok(())
450 }
451
452 #[test]
453 fn test_generation_config_with_json_schema_for_inlines_vec_of_nested_struct()
454 -> error::Result<()> {
455 let config = GenerationConfig::new().with_json_schema_for::<Outer>()?;
456 let schema = config.json_schema.expect("schema should be present");
457
458 assert_no_dollar_keys(&schema);
459
460 let inner_properties = &schema["properties"]["items"]["items"]["properties"];
464 assert_eq!(inner_properties["a"]["type"], "integer");
465 assert_eq!(inner_properties["b"]["type"], "string");
466
467 assert_eq!(
468 schema["additionalProperties"],
469 serde_json::Value::Bool(false)
470 );
471 assert_eq!(
472 schema["properties"]["items"]["items"]["additionalProperties"],
473 serde_json::Value::Bool(false)
474 );
475
476 Ok(())
477 }
478
479 #[test]
480 fn test_generation_config_with_json_schema_for_optional_and_enum_fields() -> error::Result<()> {
481 let config =
482 GenerationConfig::new().with_json_schema_for::<StructuredWithOptionalAndEnum>()?;
483 let schema = config.json_schema.expect("schema should be present");
484
485 assert_no_dollar_keys(&schema);
486
487 let properties = &schema["properties"];
488 assert!(properties.get("required_field").is_some());
489 assert!(properties.get("optional_field").is_some());
490 assert!(properties.get("choice").is_some());
491
492 let required = schema["required"]
493 .as_array()
494 .expect("required array should be present");
495 let required_names: Vec<&str> = required
496 .iter()
497 .filter_map(serde_json::Value::as_str)
498 .collect();
499 assert!(required_names.contains(&"required_field"));
500 assert!(required_names.contains(&"choice"));
501
502 Ok(())
503 }
504}