Merge pull request #796 from fabro-sh/codex/twin-openai-unknown-fields
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run

fix(twin-openai): accept unknown chat fields
This commit is contained in:
Bryan Helmkamp 2026-08-24 16:46:55 -04:00 committed by GitHub
commit 79168d3a27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 8 deletions

View file

@ -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

View file

@ -526,8 +526,10 @@ pub fn normalize_whitespace(input: &str) -> String {
input.split_whitespace().collect::<Vec<_>>().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<ChatMessage>,
@ -539,6 +541,13 @@ pub struct ChatCompletionsRequest {
pub tool_choice: Option<Value>,
pub response_format: Option<ChatResponseFormat>,
pub stop: Option<Value>,
/// 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<String, Value>,
}
#[derive(Clone, Debug, Deserialize)]

View file

@ -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::<serde_json::Value>().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]