From dd592d918d56b122fcca2a273c509de8063c130f Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 4 Jun 2026 13:57:43 +0300 Subject: [PATCH] =?UTF-8?q?test(neuron):=20C2=20=E2=80=94=20guard=20Respon?= =?UTF-8?q?ses=E2=86=92chat=20image=20translation=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Responses request translator already emits the chat `image_url` Parts array Stage B5's vision path consumes, and the non-streaming (`chat_completion`) and streaming (`responses_stream` → `inference_stream`, Stage C1) Responses paths both route image content to the vision-aware prefill — so vision works end-to-end through `/v1/responses` with no translator change required. Add a multi-image test asserting order preservation and that the `detail` hint is tolerated (and dropped, since chat image_url has no analogue), locking the translator's output to the exact `image_url.url` shape `extract_images_from_request` walks. Closes part of #16 (Stage C2). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/neuron/src/wire/openai_responses.rs | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/neuron/src/wire/openai_responses.rs b/crates/neuron/src/wire/openai_responses.rs index 8821910..5cec8ac 100644 --- a/crates/neuron/src/wire/openai_responses.rs +++ b/crates/neuron/src/wire/openai_responses.rs @@ -646,6 +646,54 @@ mod tests { assert_eq!(parts[1]["image_url"]["url"], "data:image/png;base64,AAA="); } + #[test] + fn multiple_images_translate_in_order_and_tolerate_detail() { + // C2: a Responses request carrying several InputImage parts + // (with `detail` set) must translate to a chat Parts array that + // preserves image order and the `image_url.url` shape the chat + // vision path (`extract_images_from_request`) walks. The + // `detail` hint has no chat-completions analogue we forward, so + // it's dropped — but it must not break translation. + let req = ResponsesRequest { + model: "m".into(), + input: ResponsesInput::Items(vec![ResponsesInputItem::Message { + role: "user".into(), + content: ResponsesMessageContent::Parts(vec![ + ResponsesContentPart::InputText { + text: "compare these".into(), + }, + ResponsesContentPart::InputImage { + image_url: "data:image/png;base64,FIRST".into(), + detail: Some("high".into()), + }, + ResponsesContentPart::InputImage { + image_url: "data:image/png;base64,SECOND".into(), + detail: None, + }, + ]), + }]), + instructions: None, + stream: false, + max_output_tokens: None, + temperature: None, + top_p: None, + previous_response_id: None, + extra: Value::Object(Default::default()), + }; + let chat = request_to_chat(req).unwrap(); + let parts = match &chat.messages[0].content { + MessageContent::Parts(p) => p.clone(), + other => panic!("expected Parts, got {other:?}"), + }; + // text + two images, in input order. + assert_eq!(parts.len(), 3); + assert_eq!(parts[0]["type"], "text"); + assert_eq!(parts[1]["image_url"]["url"], "data:image/png;base64,FIRST"); + assert_eq!(parts[2]["image_url"]["url"], "data:image/png;base64,SECOND"); + // `detail` is not forwarded into the chat image_url object. + assert!(parts[1]["image_url"].get("detail").is_none()); + } + #[test] fn text_only_parts_collapse_to_string() { let req = ResponsesRequest {