From 300aec7f1c9035ddba55e66fb0e3848802cc50fd Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 24 Aug 2026 16:41:02 -0400 Subject: [PATCH] fix(twin-openai): accept unknown chat fields --- test/twin/openai/docs/compatibility-matrix.md | 3 +++ test/twin/openai/src/openai/models.rs | 11 ++++++++++- .../openai/tests/chat_completions_contract.rs | 18 +++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/test/twin/openai/docs/compatibility-matrix.md b/test/twin/openai/docs/compatibility-matrix.md index 6fad3d925..bf6bcebb3 100644 --- a/test/twin/openai/docs/compatibility-matrix.md +++ b/test/twin/openai/docs/compatibility-matrix.md @@ -38,6 +38,9 @@ Supported `/v1/chat/completions` fields: - `stop` - reasoning-bearing assistant content +Unknown top-level fields are accepted and ignored. The twin does not simulate the behavior of +fields that are not listed above. + Structured output subset: - object roots diff --git a/test/twin/openai/src/openai/models.rs b/test/twin/openai/src/openai/models.rs index 72c539410..be41db099 100644 --- a/test/twin/openai/src/openai/models.rs +++ b/test/twin/openai/src/openai/models.rs @@ -526,8 +526,10 @@ pub fn normalize_whitespace(input: &str) -> String { input.split_whitespace().collect::>().join(" ") } +/// Accepts all known OpenAI Chat Completions API fields. Unknown top-level +/// fields are ignored via `#[serde(flatten)]` so the twin stays compatible as +/// clients add request options. #[derive(Clone, Debug, Deserialize)] -#[serde(deny_unknown_fields)] pub struct ChatCompletionsRequest { pub model: String, pub messages: Vec, @@ -539,6 +541,13 @@ pub struct ChatCompletionsRequest { pub tool_choice: Option, pub response_format: Option, pub stop: Option, + /// Catch-all for fields the twin doesn't use (temperature, top_p, etc.) + #[allow( + dead_code, + reason = "Serde captures unknown request fields for forward compatibility." + )] + #[serde(flatten)] + extra: Map, } #[derive(Clone, Debug, Deserialize)] diff --git a/test/twin/openai/tests/chat_completions_contract.rs b/test/twin/openai/tests/chat_completions_contract.rs index e17369c42..7837dfe5b 100644 --- a/test/twin/openai/tests/chat_completions_contract.rs +++ b/test/twin/openai/tests/chat_completions_contract.rs @@ -369,20 +369,24 @@ async fn chat_completions_reject_reasoning_parts_on_non_assistant_messages() { } #[tokio::test] -async fn chat_completions_reject_unknown_top_level_fields() { +async fn chat_completions_accept_unknown_top_level_fields() { let server = common::spawn_server().await.expect("server should start"); - let response = server - .post_chat(json!({ + let (status, chunks) = server + .post_chat_stream(json!({ "model": "gpt-test", "messages": [{ "role": "user", "content": "hello" }], - "unexpected_field": true + "stream": true, + "temperature": 0.7, + "top_p": 0.9, + "prompt_cache_key": "conversation-123" })) .await; - assert_eq!(response.status(), 400); - let body = response.json::().await.expect("json"); - assert_eq!(body["error"]["type"], "invalid_request_error"); + assert_eq!(status, 200); + let transcript = + common::parse_sse_transcript(chunks.join("").as_bytes()).expect("valid SSE transcript"); + assert!(transcript.done); } #[tokio::test]