diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 11fcbe86aa..a9fcca0331 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -112,6 +112,16 @@ export function openaiResponsesToOpenAIRequest( if (contentItem.type === "output_text") { return { type: "text", text: toString(contentItem.text) }; } + if (contentItem.type === "input_image") { + const imgResult: JsonRecord = { + type: "image_url", + image_url: { url: toString(contentItem.image_url) }, + }; + if (contentItem.detail !== undefined) { + (imgResult.image_url as JsonRecord).detail = contentItem.detail; + } + return imgResult; + } return contentValue; }) : item.content; @@ -288,11 +298,15 @@ export function openaiToOpenAIResponsesRequest( return { type: "input_text", text: toString(contentItem.text) }; } if (contentItem.type === "image_url") { - const imgUrl = contentItem.image_url as string | { url?: string }; - return { + const imgUrl = contentItem.image_url as string | { url?: string; detail?: string }; + const imgResult: JsonRecord = { type: "input_image", image_url: typeof imgUrl === "string" ? imgUrl : imgUrl?.url || "", }; + if (typeof imgUrl === "object" && imgUrl?.detail !== undefined) { + imgResult.detail = imgUrl.detail; + } + return imgResult; } return contentValue; }) diff --git a/tests/unit/responses-translation-fixes.test.mjs b/tests/unit/responses-translation-fixes.test.mjs index ea6a3ca7cd..fd0355d5db 100644 --- a/tests/unit/responses-translation-fixes.test.mjs +++ b/tests/unit/responses-translation-fixes.test.mjs @@ -4,6 +4,9 @@ import assert from "node:assert/strict"; const { convertResponsesApiFormat } = await import( "../../open-sse/translator/helpers/responsesApiHelper.ts" ); +const { openaiResponsesToOpenAIRequest, openaiToOpenAIResponsesRequest } = await import( + "../../open-sse/translator/request/openai-responses.ts" +); test("convertResponsesApiFormat filters orphaned function_call_output items", () => { const body = { @@ -33,3 +36,84 @@ test("convertResponsesApiFormat skips function_call items with empty names", () const assistantMsgs = result.messages.filter((m) => m.role === "assistant"); assert.equal(assistantMsgs.length, 0); }); + +test("Responses→Chat: input_image converted to image_url with detail", () => { + const body = { + model: "gpt-4", + input: [ + { + type: "message", + role: "user", + content: [ + { type: "input_text", text: "What is this?" }, + { type: "input_image", image_url: "https://example.com/img.png", detail: "high" }, + ], + }, + ], + }; + const result = openaiResponsesToOpenAIRequest(null, body, null, null); + const userMsg = result.messages.find((m) => m.role === "user"); + const imgPart = userMsg.content.find((c) => c.type === "image_url"); + assert.ok(imgPart, "should have image_url content part"); + assert.equal(imgPart.image_url.url, "https://example.com/img.png"); + assert.equal(imgPart.image_url.detail, "high"); +}); + +test("Responses→Chat: input_image without detail omits detail field", () => { + const body = { + model: "gpt-4", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_image", image_url: "https://example.com/img.png" }], + }, + ], + }; + const result = openaiResponsesToOpenAIRequest(null, body, null, null); + const userMsg = result.messages.find((m) => m.role === "user"); + const imgPart = userMsg.content.find((c) => c.type === "image_url"); + assert.ok(imgPart); + assert.equal(imgPart.image_url.url, "https://example.com/img.png"); + assert.equal(imgPart.image_url.detail, undefined); +}); + +test("Chat→Responses: image_url detail preserved as input_image", () => { + const body = { + model: "gpt-4", + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Describe" }, + { type: "image_url", image_url: { url: "https://example.com/img.png", detail: "low" } }, + ], + }, + ], + }; + const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, null); + const userItem = result.input.find((i) => i.type === "message" && i.role === "user"); + const imgPart = userItem.content.find((c) => c.type === "input_image"); + assert.ok(imgPart, "should have input_image content part"); + assert.equal(imgPart.image_url, "https://example.com/img.png"); + assert.equal(imgPart.detail, "low"); +}); + +test("Chat→Responses: image_url without detail omits detail", () => { + const body = { + model: "gpt-4", + messages: [ + { + role: "user", + content: [ + { type: "image_url", image_url: { url: "https://example.com/img.png" } }, + ], + }, + ], + }; + const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, null); + const userItem = result.input.find((i) => i.type === "message" && i.role === "user"); + const imgPart = userItem.content.find((c) => c.type === "input_image"); + assert.ok(imgPart); + assert.equal(imgPart.detail, undefined); +});