dynamo_llm/protocols/openai/
validate.rs1use std::fmt::Display;
5
6pub const MIN_TEMPERATURE: f32 = 0.0;
12pub const MAX_TEMPERATURE: f32 = 2.0;
14pub const TEMPERATURE_RANGE: (f32, f32) = (MIN_TEMPERATURE, MAX_TEMPERATURE);
16
17pub const MIN_TOP_P: f32 = 0.0;
19pub const MAX_TOP_P: f32 = 1.0;
21pub const TOP_P_RANGE: (f32, f32) = (MIN_TOP_P, MAX_TOP_P);
23
24pub const MIN_MIN_P: f32 = 0.0;
26pub const MAX_MIN_P: f32 = 1.0;
28pub const MIN_P_RANGE: (f32, f32) = (MIN_MIN_P, MAX_MIN_P);
30
31pub const MIN_FREQUENCY_PENALTY: f32 = -2.0;
33pub const MAX_FREQUENCY_PENALTY: f32 = 2.0;
35pub const FREQUENCY_PENALTY_RANGE: (f32, f32) = (MIN_FREQUENCY_PENALTY, MAX_FREQUENCY_PENALTY);
37
38pub const MIN_PRESENCE_PENALTY: f32 = -2.0;
40pub const MAX_PRESENCE_PENALTY: f32 = 2.0;
42pub const PRESENCE_PENALTY_RANGE: (f32, f32) = (MIN_PRESENCE_PENALTY, MAX_PRESENCE_PENALTY);
44
45pub const MIN_LENGTH_PENALTY: f32 = -2.0;
47pub const MAX_LENGTH_PENALTY: f32 = 2.0;
49pub const LENGTH_PENALTY_RANGE: (f32, f32) = (MIN_LENGTH_PENALTY, MAX_LENGTH_PENALTY);
51
52pub const MIN_TOP_LOGPROBS: u8 = 0;
54pub const MAX_TOP_LOGPROBS: u8 = 20;
56
57pub const MIN_LOGPROBS: u8 = 0;
59pub const MAX_LOGPROBS: u8 = 5;
61
62pub const MIN_N: u8 = 1;
64pub const MAX_N: u8 = 128;
66pub const N_RANGE: (u8, u8) = (MIN_N, MAX_N);
68
69pub const MIN_LOGIT_BIAS: f32 = -100.0;
71pub const MAX_LOGIT_BIAS: f32 = 100.0;
73
74pub const MIN_BEST_OF: u8 = 0;
76pub const MAX_BEST_OF: u8 = 20;
78pub const BEST_OF_RANGE: (u8, u8) = (MIN_BEST_OF, MAX_BEST_OF);
80
81pub const MAX_STOP_SEQUENCES: usize = 4;
83pub const MAX_TOOLS: usize = 128;
85pub const MAX_METADATA_PAIRS: usize = 16;
87pub const MAX_METADATA_KEY_LENGTH: usize = 64;
89pub const MAX_METADATA_VALUE_LENGTH: usize = 512;
91pub const MAX_FUNCTION_NAME_LENGTH: usize = 64;
93pub const MAX_PROMPT_TOKEN_ID: u32 = 50256;
95pub const MIN_REPETITION_PENALTY: f32 = 0.0;
97pub const MAX_REPETITION_PENALTY: f32 = 2.0;
99
100pub fn validate_temperature(temperature: Option<f32>) -> Result<(), anyhow::Error> {
106 if let Some(temp) = temperature
107 && !(MIN_TEMPERATURE..=MAX_TEMPERATURE).contains(&temp)
108 {
109 anyhow::bail!(
110 "Temperature must be between {} and {}, got {}",
111 MIN_TEMPERATURE,
112 MAX_TEMPERATURE,
113 temp
114 );
115 }
116 Ok(())
117}
118
119pub fn validate_top_p(top_p: Option<f32>) -> Result<(), anyhow::Error> {
121 if let Some(p) = top_p
122 && !(MIN_TOP_P..=MAX_TOP_P).contains(&p)
123 {
124 anyhow::bail!(
125 "Top_p must be between {} and {}, got {}",
126 MIN_TOP_P,
127 MAX_TOP_P,
128 p
129 );
130 }
131 Ok(())
132}
133
134pub fn validate_temperature_top_p_exclusion(
136 temperature: Option<f32>,
137 top_p: Option<f32>,
138) -> Result<(), anyhow::Error> {
139 match (temperature, top_p) {
140 (Some(t), Some(p)) if t != 1.0 && p != 1.0 => {
141 anyhow::bail!("Only one of temperature or top_p should be set (not both)");
142 }
143 _ => Ok(()),
144 }
145}
146
147pub fn validate_frequency_penalty(frequency_penalty: Option<f32>) -> Result<(), anyhow::Error> {
149 if let Some(penalty) = frequency_penalty
150 && !(MIN_FREQUENCY_PENALTY..=MAX_FREQUENCY_PENALTY).contains(&penalty)
151 {
152 anyhow::bail!(
153 "Frequency penalty must be between {} and {}, got {}",
154 MIN_FREQUENCY_PENALTY,
155 MAX_FREQUENCY_PENALTY,
156 penalty
157 );
158 }
159 Ok(())
160}
161
162pub fn validate_presence_penalty(presence_penalty: Option<f32>) -> Result<(), anyhow::Error> {
164 if let Some(penalty) = presence_penalty
165 && !(MIN_PRESENCE_PENALTY..=MAX_PRESENCE_PENALTY).contains(&penalty)
166 {
167 anyhow::bail!(
168 "Presence penalty must be between {} and {}, got {}",
169 MIN_PRESENCE_PENALTY,
170 MAX_PRESENCE_PENALTY,
171 penalty
172 );
173 }
174 Ok(())
175}
176
177pub fn validate_repetition_penalty(repetition_penalty: Option<f32>) -> Result<(), anyhow::Error> {
178 if let Some(penalty) = repetition_penalty
179 && !(MIN_REPETITION_PENALTY..=MAX_REPETITION_PENALTY).contains(&penalty)
180 {
181 anyhow::bail!(
182 "Repetition penalty must be between {} and {}, got {}",
183 MIN_REPETITION_PENALTY,
184 MAX_REPETITION_PENALTY,
185 penalty
186 );
187 }
188 Ok(())
189}
190
191pub fn validate_logit_bias(
193 logit_bias: &Option<std::collections::HashMap<String, serde_json::Value>>,
194) -> Result<(), anyhow::Error> {
195 let logit_bias = match logit_bias {
196 Some(val) => val,
197 None => return Ok(()),
198 };
199
200 for (token, bias_value) in logit_bias {
201 let bias = bias_value.as_f64().ok_or_else(|| {
202 anyhow::anyhow!(
203 "Logit bias value for token '{}' must be a number, got {:?}",
204 token,
205 bias_value
206 )
207 })? as f32;
208
209 if !(MIN_LOGIT_BIAS..=MAX_LOGIT_BIAS).contains(&bias) {
210 anyhow::bail!(
211 "Logit bias for token '{}' must be between {} and {}, got {}",
212 token,
213 MIN_LOGIT_BIAS,
214 MAX_LOGIT_BIAS,
215 bias
216 );
217 }
218 }
219 Ok(())
220}
221
222pub fn validate_n(n: Option<u8>) -> Result<(), anyhow::Error> {
224 if let Some(value) = n
225 && !(MIN_N..=MAX_N).contains(&value)
226 {
227 anyhow::bail!("n must be between {} and {}, got {}", MIN_N, MAX_N, value);
228 }
229 Ok(())
230}
231
232pub fn validate_model(model: &str) -> Result<(), anyhow::Error> {
234 if model.trim().is_empty() {
235 anyhow::bail!("Model cannot be empty");
236 }
237 Ok(())
238}
239
240pub fn validate_user(user: Option<&str>) -> Result<(), anyhow::Error> {
242 if let Some(user_id) = user
243 && user_id.trim().is_empty()
244 {
245 anyhow::bail!("User ID cannot be empty");
246 }
247 Ok(())
248}
249
250pub fn validate_stop(stop: &Option<dynamo_async_openai::types::Stop>) -> Result<(), anyhow::Error> {
252 if let Some(stop_value) = stop {
253 match stop_value {
254 dynamo_async_openai::types::Stop::String(s) => {
255 if s.is_empty() {
256 anyhow::bail!("Stop sequence cannot be empty");
257 }
258 }
259 dynamo_async_openai::types::Stop::StringArray(sequences) => {
260 if sequences.is_empty() {
261 anyhow::bail!("Stop sequences array cannot be empty");
262 }
263 if sequences.len() > MAX_STOP_SEQUENCES {
264 anyhow::bail!(
265 "Maximum of {} stop sequences allowed, got {}",
266 MAX_STOP_SEQUENCES,
267 sequences.len()
268 );
269 }
270 for (i, sequence) in sequences.iter().enumerate() {
271 if sequence.is_empty() {
272 anyhow::bail!("Stop sequence at index {} cannot be empty", i);
273 }
274 }
275 }
276 }
277 }
278 Ok(())
279}
280
281pub fn validate_messages(
287 messages: &[dynamo_async_openai::types::ChatCompletionRequestMessage],
288) -> Result<(), anyhow::Error> {
289 if messages.is_empty() {
290 anyhow::bail!("Messages array cannot be empty");
291 }
292 Ok(())
293}
294
295pub fn validate_top_logprobs(top_logprobs: Option<u8>) -> Result<(), anyhow::Error> {
297 if let Some(value) = top_logprobs
298 && !(0..=20).contains(&value)
299 {
300 anyhow::bail!(
301 "Top_logprobs must be between 0 and {}, got {}",
302 MAX_TOP_LOGPROBS,
303 value
304 );
305 }
306 Ok(())
307}
308
309pub fn validate_tools(
311 tools: &Option<&[dynamo_async_openai::types::ChatCompletionTool]>,
312) -> Result<(), anyhow::Error> {
313 let tools = match tools {
314 Some(val) => val,
315 None => return Ok(()),
316 };
317
318 if tools.len() > MAX_TOOLS {
319 anyhow::bail!(
320 "Maximum of {} tools are supported, got {}",
321 MAX_TOOLS,
322 tools.len()
323 );
324 }
325
326 for (i, tool) in tools.iter().enumerate() {
327 if tool.function.name.len() > MAX_FUNCTION_NAME_LENGTH {
328 anyhow::bail!(
329 "Function name at index {} exceeds {} character limit, got {} characters",
330 i,
331 MAX_FUNCTION_NAME_LENGTH,
332 tool.function.name.len()
333 );
334 }
335 if tool.function.name.trim().is_empty() {
336 anyhow::bail!("Function name at index {} cannot be empty", i);
337 }
338 }
339 Ok(())
340}
341
342pub fn validate_metadata(metadata: &Option<serde_json::Value>) -> Result<(), anyhow::Error> {
344 let metadata = match metadata {
345 Some(val) => val,
346 None => return Ok(()),
347 };
348
349 if let Some(obj) = metadata.as_object() {
350 if obj.len() > MAX_METADATA_PAIRS {
351 anyhow::bail!(
352 "Metadata cannot have more than {} key-value pairs, got {}",
353 MAX_METADATA_PAIRS,
354 obj.len()
355 );
356 }
357
358 for (key, value) in obj {
359 if key.len() > MAX_METADATA_KEY_LENGTH {
360 anyhow::bail!(
361 "Metadata key '{}' exceeds {} character limit",
362 key,
363 MAX_METADATA_KEY_LENGTH
364 );
365 }
366
367 if let Some(value_str) = value.as_str()
368 && value_str.len() > MAX_METADATA_VALUE_LENGTH
369 {
370 anyhow::bail!(
371 "Metadata value for key '{}' exceeds {} character limit",
372 key,
373 MAX_METADATA_VALUE_LENGTH
374 );
375 }
376 }
377 }
378 Ok(())
379}
380
381pub fn validate_reasoning_effort(
383 _reasoning_effort: &Option<dynamo_async_openai::types::ReasoningEffort>,
384) -> Result<(), anyhow::Error> {
385 Ok(())
389}
390
391pub fn validate_service_tier(
393 _service_tier: &Option<dynamo_async_openai::types::ServiceTier>,
394) -> Result<(), anyhow::Error> {
395 Ok(())
399}
400
401pub fn validate_prompt(prompt: &dynamo_async_openai::types::Prompt) -> Result<(), anyhow::Error> {
407 match prompt {
408 dynamo_async_openai::types::Prompt::String(s) => {
409 if s.is_empty() {
410 anyhow::bail!("Prompt string cannot be empty");
411 }
412 }
413 dynamo_async_openai::types::Prompt::StringArray(arr) => {
414 if arr.is_empty() {
415 anyhow::bail!("Prompt string array cannot be empty");
416 }
417 for (i, s) in arr.iter().enumerate() {
418 if s.is_empty() {
419 anyhow::bail!("Prompt string at index {} cannot be empty", i);
420 }
421 }
422 }
423 dynamo_async_openai::types::Prompt::IntegerArray(arr) => {
424 if arr.is_empty() {
425 anyhow::bail!("Prompt integer array cannot be empty");
426 }
427 for (i, &token_id) in arr.iter().enumerate() {
428 if token_id > MAX_PROMPT_TOKEN_ID {
429 anyhow::bail!(
430 "Token ID at index {} must be between 0 and {}, got {}",
431 i,
432 MAX_PROMPT_TOKEN_ID,
433 token_id
434 );
435 }
436 }
437 }
438 dynamo_async_openai::types::Prompt::ArrayOfIntegerArray(arr) => {
439 if arr.is_empty() {
440 anyhow::bail!("Prompt array of integer arrays cannot be empty");
441 }
442 for (i, inner_arr) in arr.iter().enumerate() {
443 if inner_arr.is_empty() {
444 anyhow::bail!("Prompt integer array at index {} cannot be empty", i);
445 }
446 for (j, &token_id) in inner_arr.iter().enumerate() {
447 if token_id > MAX_PROMPT_TOKEN_ID {
448 anyhow::bail!(
449 "Token ID at index [{}][{}] must be between 0 and {}, got {}",
450 i,
451 j,
452 MAX_PROMPT_TOKEN_ID,
453 token_id
454 );
455 }
456 }
457 }
458 }
459 }
460 Ok(())
461}
462
463pub fn validate_logprobs(logprobs: Option<u8>) -> Result<(), anyhow::Error> {
465 if let Some(value) = logprobs
466 && !(MIN_LOGPROBS..=MAX_LOGPROBS).contains(&value)
467 {
468 anyhow::bail!(
469 "Logprobs must be between 0 and {}, got {}",
470 MAX_LOGPROBS,
471 value
472 );
473 }
474 Ok(())
475}
476
477pub fn validate_best_of(best_of: Option<u8>, n: Option<u8>) -> Result<(), anyhow::Error> {
479 if let Some(best_of_value) = best_of {
480 if !(MIN_BEST_OF..=MAX_BEST_OF).contains(&best_of_value) {
481 anyhow::bail!(
482 "Best_of must be between 0 and {}, got {}",
483 MAX_BEST_OF,
484 best_of_value
485 );
486 }
487
488 if let Some(n_value) = n
489 && best_of_value < n_value
490 {
491 anyhow::bail!(
492 "Best_of must be greater than or equal to n, got best_of={} and n={}",
493 best_of_value,
494 n_value
495 );
496 }
497 }
498 Ok(())
499}
500
501pub fn validate_suffix(suffix: Option<&str>) -> Result<(), anyhow::Error> {
503 if let Some(suffix_str) = suffix {
504 if suffix_str.len() > 10000 {
506 anyhow::bail!("Suffix is too long, maximum 10000 characters");
507 }
508 }
509 Ok(())
510}
511
512pub fn validate_max_tokens(max_tokens: Option<u32>) -> Result<(), anyhow::Error> {
514 if let Some(tokens) = max_tokens
515 && tokens == 0
516 {
517 anyhow::bail!("Max tokens must be greater than 0, got {}", tokens);
518 }
519 Ok(())
520}
521
522pub fn validate_max_completion_tokens(
524 max_completion_tokens: Option<u32>,
525) -> Result<(), anyhow::Error> {
526 if let Some(tokens) = max_completion_tokens
527 && tokens == 0
528 {
529 anyhow::bail!(
530 "Max completion tokens must be greater than 0, got {}",
531 tokens
532 );
533 }
534 Ok(())
535}
536
537pub fn validate_range<T>(value: Option<T>, range: &(T, T)) -> anyhow::Result<Option<T>>
542where
543 T: PartialOrd + Display,
544{
545 if value.is_none() {
546 return Ok(None);
547 }
548 let value = value.unwrap();
549 if value < range.0 || value > range.1 {
550 anyhow::bail!("Value {} is out of range [{}, {}]", value, range.0, range.1);
551 }
552 Ok(Some(value))
553}