dynamo_llm/protocols/openai/
common_ext.rs1use super::nvext::validate_top_k;
5use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9#[derive(Serialize, Deserialize, Builder, Validate, Debug, Clone, Default)]
12pub struct CommonExt {
13 #[serde(default, skip_serializing_if = "Option::is_none")]
16 #[builder(default, setter(strip_option))]
17 pub ignore_eos: Option<bool>,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
22 #[builder(default, setter(strip_option))]
23 pub min_tokens: Option<u32>,
24
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 #[builder(default, setter(strip_option))]
28 #[validate(custom(function = "validate_top_k"))]
29 pub top_k: Option<i32>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 #[builder(default, setter(strip_option))]
34 #[validate(range(min = 0.0, max = 1.0))]
35 pub min_p: Option<f32>,
36
37 #[serde(default, skip_serializing_if = "Option::is_none")]
40 #[builder(default, setter(strip_option))]
41 #[validate(range(exclusive_min = 0.0, max = 2.0))]
42 pub repetition_penalty: Option<f32>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 #[builder(default, setter(strip_option))]
47 pub include_stop_str_in_output: Option<bool>,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
52 #[builder(default, setter(strip_option))]
53 pub guided_json: Option<serde_json::Value>,
54
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 #[builder(default, setter(strip_option))]
58 pub guided_regex: Option<String>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 #[builder(default, setter(strip_option))]
63 pub guided_grammar: Option<String>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 #[builder(default, setter(strip_option))]
68 pub guided_choice: Option<Vec<String>>,
69
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 #[builder(default, setter(strip_option))]
73 pub guided_decoding_backend: Option<String>,
74}
75
76impl CommonExt {
77 pub fn builder() -> CommonExtBuilder {
78 CommonExtBuilder::default()
79 }
80}
81
82pub trait CommonExtProvider {
84 fn common_ext(&self) -> Option<&CommonExt>;
86
87 fn get_guided_json(&self) -> Option<&serde_json::Value>;
89 fn get_guided_regex(&self) -> Option<String>;
90 fn get_guided_grammar(&self) -> Option<String>;
91 fn get_guided_choice(&self) -> Option<Vec<String>>;
92 fn get_guided_decoding_backend(&self) -> Option<String>;
93
94 fn get_top_k(&self) -> Option<i32>;
96 fn get_min_p(&self) -> Option<f32>;
97 fn get_repetition_penalty(&self) -> Option<f32>;
98 fn get_include_stop_str_in_output(&self) -> Option<bool>;
99}
100
101pub fn emit_nvext_deprecation_warning(
103 field_name: &str,
104 nvext_has_value: bool,
105 common_has_value: bool,
106) {
107 if nvext_has_value && !common_has_value {
108 tracing::warn!(
109 "DEPRECATION WARNING: 'nvext.{field_name}' is deprecated and will be removed in a future release. Use '{field_name}' at the top level or in 'extra_body' instead."
110 );
111 } else if nvext_has_value && common_has_value {
112 tracing::warn!(
113 "DEPRECATION WARNING: 'nvext.{field_name}' is deprecated and will be removed in a future release. Top-level '{field_name}' takes precedence. Use '{field_name}' at the top level or in 'extra_body' instead."
114 );
115 }
116}
117
118pub fn choose_with_deprecation<T: Clone>(
120 field: &'static str,
121 common: Option<&T>,
122 nv: Option<&T>,
123) -> Option<T> {
124 if nv.is_some() {
125 emit_nvext_deprecation_warning(field, true, common.is_some());
126 }
127 common.cloned().or_else(|| nv.cloned())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 use serde_json;
135
136 #[test]
137 fn test_common_ext_builder_default() {
138 let common_ext = CommonExt::builder().build().unwrap();
139 assert_eq!(common_ext.ignore_eos, None);
140 assert_eq!(common_ext.min_tokens, None);
141 assert_eq!(common_ext.top_k, None);
142 assert_eq!(common_ext.repetition_penalty, None);
143 assert_eq!(common_ext.guided_json, None);
144 assert_eq!(common_ext.guided_regex, None);
145 assert_eq!(common_ext.guided_grammar, None);
146 assert_eq!(common_ext.guided_choice, None);
147 assert_eq!(common_ext.guided_decoding_backend, None);
148 assert_eq!(common_ext.include_stop_str_in_output, None);
149 }
150
151 #[test]
152 fn test_common_ext_builder_with_values() {
153 let common_ext = CommonExt::builder()
154 .ignore_eos(true)
155 .min_tokens(10)
156 .top_k(50)
157 .repetition_penalty(1.2)
158 .include_stop_str_in_output(true)
159 .guided_json(serde_json::json!({"key": "value"}))
160 .guided_regex("regex".to_string())
161 .guided_grammar("grammar".to_string())
162 .guided_choice(vec!["choice1".to_string(), "choice2".to_string()])
163 .guided_decoding_backend("backend".to_string())
164 .build()
165 .unwrap();
166
167 assert_eq!(common_ext.ignore_eos, Some(true));
168 assert_eq!(common_ext.min_tokens, Some(10));
169 assert_eq!(common_ext.top_k, Some(50));
170 assert_eq!(common_ext.repetition_penalty, Some(1.2));
171 assert_eq!(common_ext.include_stop_str_in_output, Some(true));
172 assert_eq!(
173 common_ext.guided_json.as_ref(),
174 Some(&serde_json::json!({"key": "value"}))
175 );
176 assert_eq!(common_ext.guided_regex, Some("regex".to_string()));
177 assert_eq!(common_ext.guided_grammar, Some("grammar".to_string()));
178 assert_eq!(
179 common_ext.guided_choice,
180 Some(vec!["choice1".to_string(), "choice2".to_string()])
181 );
182 assert_eq!(
183 common_ext.guided_decoding_backend,
184 Some("backend".to_string())
185 );
186 }
187
188 #[test]
189 fn test_common_ext_fields() {
190 let common_ext = CommonExt::builder()
192 .ignore_eos(false)
193 .min_tokens(5)
194 .include_stop_str_in_output(true)
195 .build()
196 .unwrap();
197
198 assert_eq!(common_ext.ignore_eos, Some(false));
199 assert_eq!(common_ext.min_tokens, Some(5));
200 assert_eq!(common_ext.include_stop_str_in_output, Some(true));
201 }
202
203 #[test]
204 fn test_validation_min_tokens() {
205 let common_ext = CommonExt {
207 ignore_eos: None,
208 min_tokens: Some(0), top_k: None,
210 min_p: None,
211 repetition_penalty: None,
212 include_stop_str_in_output: None,
213 guided_json: None,
214 guided_regex: None,
215 guided_grammar: None,
216 guided_choice: None,
217 guided_decoding_backend: None,
218 };
219 assert!(common_ext.validate().is_ok());
220 }
221
222 #[test]
223 fn test_common_ext_neither_specified() {
224 let common_ext = CommonExt::builder().build().unwrap();
226
227 assert_eq!(common_ext.ignore_eos, None);
228 assert_eq!(common_ext.min_tokens, None);
229 assert_eq!(common_ext.top_k, None);
230 assert_eq!(common_ext.repetition_penalty, None);
231 assert_eq!(common_ext.include_stop_str_in_output, None);
232 assert!(common_ext.validate().is_ok());
233 }
234
235 #[test]
236 fn test_common_ext_default() {
237 let common_ext = CommonExt::default();
239
240 assert_eq!(common_ext.ignore_eos, None);
241 assert_eq!(common_ext.min_tokens, None);
242 assert_eq!(common_ext.top_k, None);
243 assert_eq!(common_ext.repetition_penalty, None);
244 assert_eq!(common_ext.include_stop_str_in_output, None);
245 assert!(common_ext.validate().is_ok());
246 }
247
248 #[test]
249 fn test_choose_with_deprecation() {
250 let result = choose_with_deprecation(
252 "test_field",
253 Some(&"common_value".to_string()),
254 Some(&"nvext_value".to_string()),
255 );
256 assert_eq!(result, Some("common_value".to_string()));
257
258 let result = choose_with_deprecation("test_field", None, Some(&"nvext_value".to_string()));
260 assert_eq!(result, Some("nvext_value".to_string()));
261
262 let result: Option<String> = choose_with_deprecation("test_field", None, None);
264 assert_eq!(result, None);
265 }
266}