Skip to content

Commit 3d036fc

Browse files
feat: Reject unsupported parameters with 400 Bad Request (/v1/completions) (#4140)
Signed-off-by: Krishnan Prashanth <[email protected]>
1 parent 6e2b22e commit 3d036fc

File tree

5 files changed

+82
-2
lines changed

5 files changed

+82
-2
lines changed

lib/bindings/python/Cargo.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/llm/src/grpc/service/openai.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ impl TryFrom<inference::ModelInferRequest> for NvCreateCompletionRequest {
315315
common: Default::default(),
316316
nvext: None,
317317
metadata: None,
318+
unsupported_fields: Default::default(),
318319
})
319320
}
320321
}

lib/llm/src/http/service/openai.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,7 @@ mod tests {
14691469
common: Default::default(),
14701470
nvext: None,
14711471
metadata: None,
1472+
unsupported_fields: Default::default(),
14721473
};
14731474

14741475
let result = validate_completion_fields_generic(&request);
@@ -1492,6 +1493,7 @@ mod tests {
14921493
common: Default::default(),
14931494
nvext: None,
14941495
metadata: None,
1496+
unsupported_fields: Default::default(),
14951497
};
14961498
let result = validate_completion_fields_generic(&request);
14971499
assert!(result.is_err());
@@ -1514,6 +1516,7 @@ mod tests {
15141516
common: Default::default(),
15151517
nvext: None,
15161518
metadata: None,
1519+
unsupported_fields: Default::default(),
15171520
};
15181521
let result = validate_completion_fields_generic(&request);
15191522
assert!(result.is_err());
@@ -1536,6 +1539,7 @@ mod tests {
15361539
common: Default::default(),
15371540
nvext: None,
15381541
metadata: None,
1542+
unsupported_fields: Default::default(),
15391543
};
15401544
let result = validate_completion_fields_generic(&request);
15411545
assert!(result.is_err());
@@ -1560,6 +1564,7 @@ mod tests {
15601564
.unwrap(),
15611565
nvext: None,
15621566
metadata: None,
1567+
unsupported_fields: Default::default(),
15631568
};
15641569
let result = validate_completion_fields_generic(&request);
15651570
assert!(result.is_err());
@@ -1582,6 +1587,7 @@ mod tests {
15821587
common: Default::default(),
15831588
nvext: None,
15841589
metadata: None,
1590+
unsupported_fields: Default::default(),
15851591
};
15861592
let result = validate_completion_fields_generic(&request);
15871593
assert!(result.is_err());
@@ -1612,6 +1618,7 @@ mod tests {
16121618
"session": {"id": "session-1", "timestamp": 1640995200}
16131619
})
16141620
.into(),
1621+
unsupported_fields: Default::default(),
16151622
};
16161623

16171624
let result = validate_completion_fields_generic(&request);
@@ -1797,8 +1804,8 @@ mod tests {
17971804
}
17981805

17991806
#[test]
1800-
fn test_unknown_fields_rejected() {
1801-
// Test that all known unsupported fields are rejected and all shown in error message
1807+
fn test_chat_completions_unknown_fields_rejected() {
1808+
// Test that known unsupported fields are rejected and all shown in error message
18021809
let json = r#"{
18031810
"messages": [{"role": "user", "content": "Hello"}],
18041811
"model": "test-model",
@@ -1837,4 +1844,36 @@ mod tests {
18371844
assert!(msg.contains("chat_template_kwargs"));
18381845
}
18391846
}
1847+
1848+
#[test]
1849+
fn test_completions_unsupported_fields_rejected() {
1850+
// Test that known unsupported fields are rejected and all shown in error message
1851+
let json = r#"{
1852+
"model": "test-model",
1853+
"prompt": "Hello",
1854+
"add_special_tokens": true,
1855+
"response_format": {"type": "json_object"}
1856+
}"#;
1857+
1858+
let request: NvCreateCompletionRequest = serde_json::from_str(json).unwrap();
1859+
1860+
// Verify both unsupported fields were captured
1861+
assert!(
1862+
request
1863+
.unsupported_fields
1864+
.contains_key("add_special_tokens")
1865+
);
1866+
assert!(request.unsupported_fields.contains_key("response_format"));
1867+
1868+
let result = validate_completion_fields_generic(&request);
1869+
assert!(result.is_err());
1870+
if let Err(error_response) = result {
1871+
assert_eq!(error_response.0, StatusCode::BAD_REQUEST);
1872+
let msg = &error_response.1.message;
1873+
assert!(msg.contains("Unsupported parameter"));
1874+
// Verify both fields appear in error message
1875+
assert!(msg.contains("add_special_tokens"));
1876+
assert!(msg.contains("response_format"));
1877+
}
1878+
}
18401879
}

lib/llm/src/protocols/openai/completions.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub struct NvCreateCompletionRequest {
3737
// metadata - passthrough parameter without restrictions
3838
#[serde(skip_serializing_if = "Option::is_none")]
3939
pub metadata: Option<serde_json::Value>,
40+
41+
/// Catch-all for unsupported fields - checked during validation
42+
#[serde(flatten, default, skip_serializing)]
43+
pub unsupported_fields: std::collections::HashMap<String, serde_json::Value>,
4044
}
4145

4246
#[derive(Serialize, Deserialize, Validate, Debug, Clone)]
@@ -372,6 +376,7 @@ impl OpenAIOutputOptionsProvider for NvCreateCompletionRequest {
372376
/// allowing us to validate the data.
373377
impl ValidateRequest for NvCreateCompletionRequest {
374378
fn validate(&self) -> Result<(), anyhow::Error> {
379+
validate::validate_no_unsupported_fields(&self.unsupported_fields)?;
375380
validate::validate_model(&self.inner.model)?;
376381
validate::validate_prompt(&self.inner.prompt)?;
377382
validate::validate_suffix(self.inner.suffix.as_deref())?;

lib/llm/tests/openai_completions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ impl CompletionSample {
2929
common: Default::default(),
3030
nvext: None,
3131
metadata: None,
32+
unsupported_fields: Default::default(),
3233
};
3334

3435
Ok(Self {

0 commit comments

Comments
 (0)