dynamo_llm/protocols/openai/
validate.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::Display;
5
6//
7// Hyperparameter Contraints
8//
9
10/// Minimum allowed value for OpenAI's `temperature` sampling option
11pub const MIN_TEMPERATURE: f32 = 0.0;
12/// Maximum allowed value for OpenAI's `temperature` sampling option
13pub const MAX_TEMPERATURE: f32 = 2.0;
14/// Allowed range of values for OpenAI's `temperature`` sampling option
15pub const TEMPERATURE_RANGE: (f32, f32) = (MIN_TEMPERATURE, MAX_TEMPERATURE);
16
17/// Minimum allowed value for OpenAI's `top_p` sampling option
18pub const MIN_TOP_P: f32 = 0.0;
19/// Maximum allowed value for OpenAI's `top_p` sampling option
20pub const MAX_TOP_P: f32 = 1.0;
21/// Allowed range of values for OpenAI's `top_p` sampling option
22pub const TOP_P_RANGE: (f32, f32) = (MIN_TOP_P, MAX_TOP_P);
23
24/// Minimum allowed value for `min_p`
25pub const MIN_MIN_P: f32 = 0.0;
26/// Maximum allowed value for `min_p`
27pub const MAX_MIN_P: f32 = 1.0;
28/// Allowed range of values for `min_p`
29pub const MIN_P_RANGE: (f32, f32) = (MIN_MIN_P, MAX_MIN_P);
30
31/// Minimum allowed value for OpenAI's `frequency_penalty` sampling option
32pub const MIN_FREQUENCY_PENALTY: f32 = -2.0;
33/// Maximum allowed value for OpenAI's `frequency_penalty` sampling option
34pub const MAX_FREQUENCY_PENALTY: f32 = 2.0;
35/// Allowed range of values for OpenAI's `frequency_penalty` sampling option
36pub const FREQUENCY_PENALTY_RANGE: (f32, f32) = (MIN_FREQUENCY_PENALTY, MAX_FREQUENCY_PENALTY);
37
38/// Minimum allowed value for OpenAI's `presence_penalty` sampling option
39pub const MIN_PRESENCE_PENALTY: f32 = -2.0;
40/// Maximum allowed value for OpenAI's `presence_penalty` sampling option
41pub const MAX_PRESENCE_PENALTY: f32 = 2.0;
42/// Allowed range of values for OpenAI's `presence_penalty` sampling option
43pub const PRESENCE_PENALTY_RANGE: (f32, f32) = (MIN_PRESENCE_PENALTY, MAX_PRESENCE_PENALTY);
44
45/// Minimum allowed value for `length_penalty`
46pub const MIN_LENGTH_PENALTY: f32 = -2.0;
47/// Maximum allowed value for `length_penalty`
48pub const MAX_LENGTH_PENALTY: f32 = 2.0;
49/// Allowed range of values for `length_penalty`
50pub const LENGTH_PENALTY_RANGE: (f32, f32) = (MIN_LENGTH_PENALTY, MAX_LENGTH_PENALTY);
51
52/// Maximum allowed value for `top_logprobs`
53pub const MIN_TOP_LOGPROBS: u8 = 0;
54/// Maximum allowed value for `top_logprobs`
55pub const MAX_TOP_LOGPROBS: u8 = 20;
56
57/// Minimum allowed value for `logprobs` in completion requests
58pub const MIN_LOGPROBS: u8 = 0;
59/// Maximum allowed value for `logprobs` in completion requests
60pub const MAX_LOGPROBS: u8 = 5;
61
62/// Minimum allowed value for `n` (number of choices)
63pub const MIN_N: u8 = 1;
64/// Maximum allowed value for `n` (number of choices)
65pub const MAX_N: u8 = 128;
66/// Allowed range of values for `n` (number of choices)
67pub const N_RANGE: (u8, u8) = (MIN_N, MAX_N);
68
69/// Minimum allowed value for OpenAI's `logit_bias` values
70pub const MIN_LOGIT_BIAS: f32 = -100.0;
71/// Maximum allowed value for OpenAI's `logit_bias` values
72pub const MAX_LOGIT_BIAS: f32 = 100.0;
73
74/// Minimum allowed value for `best_of`
75pub const MIN_BEST_OF: u8 = 0;
76/// Maximum allowed value for `best_of`
77pub const MAX_BEST_OF: u8 = 20;
78/// Allowed range of values for `best_of`
79pub const BEST_OF_RANGE: (u8, u8) = (MIN_BEST_OF, MAX_BEST_OF);
80
81/// Maximum allowed number of stop sequences
82pub const MAX_STOP_SEQUENCES: usize = 4;
83/// Maximum allowed number of tools
84pub const MAX_TOOLS: usize = 128;
85/// Maximum allowed number of metadata key-value pairs
86pub const MAX_METADATA_PAIRS: usize = 16;
87/// Maximum allowed length for metadata keys
88pub const MAX_METADATA_KEY_LENGTH: usize = 64;
89/// Maximum allowed length for metadata values
90pub const MAX_METADATA_VALUE_LENGTH: usize = 512;
91/// Maximum allowed length for function names
92pub const MAX_FUNCTION_NAME_LENGTH: usize = 64;
93/// Maximum allowed value for Prompt IntegerArray elements
94pub const MAX_PROMPT_TOKEN_ID: u32 = 50256;
95/// Minimum allowed value for `repetition_penalty`
96pub const MIN_REPETITION_PENALTY: f32 = 0.0;
97/// Maximum allowed value for `repetition_penalty`
98pub const MAX_REPETITION_PENALTY: f32 = 2.0;
99
100//
101// Shared Fields
102//
103
104/// Validates the temperature parameter
105pub 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
119/// Validates the top_p parameter
120pub 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
134/// Validates mutual exclusion of temperature and top_p
135pub 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
147/// Validates frequency penalty parameter
148pub 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
162/// Validates presence penalty parameter
163pub 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
191/// Validates logit bias map
192pub 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
222/// Validates n parameter (number of choices)
223pub 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
232/// Validates model parameter
233pub 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
240/// Validates user parameter
241pub 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
250/// Validates stop sequences
251pub 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
281//
282// Chat Completion Specific
283//
284
285/// Validates messages array
286pub 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
295/// Validates top_logprobs parameter
296pub 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
309/// Validates tools array
310pub 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
342/// Validates metadata
343pub 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
381/// Validates reasoning effort parameter
382pub fn validate_reasoning_effort(
383    _reasoning_effort: &Option<dynamo_async_openai::types::ReasoningEffort>,
384) -> Result<(), anyhow::Error> {
385    // TODO ADD HERE
386    // ReasoningEffort is an enum, so if it exists, it's valid by definition
387    // This function is here for completeness and future validation needs
388    Ok(())
389}
390
391/// Validates service tier parameter
392pub fn validate_service_tier(
393    _service_tier: &Option<dynamo_async_openai::types::ServiceTier>,
394) -> Result<(), anyhow::Error> {
395    // TODO ADD HERE
396    // ServiceTier is an enum, so if it exists, it's valid by definition
397    // This function is here for completeness and future validation needs
398    Ok(())
399}
400
401//
402// Completion Specific
403//
404
405/// Validates prompt
406pub 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
463/// Validates logprobs parameter (for completion requests)
464pub 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
477/// Validates best_of parameter
478pub 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
501/// Validates suffix parameter
502pub fn validate_suffix(suffix: Option<&str>) -> Result<(), anyhow::Error> {
503    if let Some(suffix_str) = suffix {
504        // Suffix can be empty, but if it's very long it might cause issues
505        if suffix_str.len() > 10000 {
506            anyhow::bail!("Suffix is too long, maximum 10000 characters");
507        }
508    }
509    Ok(())
510}
511
512/// Validates max_tokens parameter
513pub 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
522/// Validates max_completion_tokens parameter
523pub 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
537//
538// Helpers
539//
540
541pub 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}