diff --git a/src/protocol/body.ts b/src/protocol/body.ts index 5228c79..0962003 100644 --- a/src/protocol/body.ts +++ b/src/protocol/body.ts @@ -24,6 +24,7 @@ const RESPONSES_TOOL_NAME_LIMIT: ToolNameLimit = { type ResponsesContentPart = | { type: "input_text"; text: string } | { type: "output_text"; text: string } + | { type: "refusal"; refusal: string } | { type: "input_image"; image_url: string } | { type: "input_image"; file_id: string } | { type: "input_file"; filename: string; file_data: string } @@ -127,9 +128,11 @@ function toResponsesContentPart( `@corbits/openai-responses: adapter does not handle ${block.type} content blocks.`, ); case "refusal": - throw new Error( - "@corbits/openai-responses: adapter does not handle refusal content blocks.", - ); + // Only a typed assistant message takes output parts; everywhere else + // the reason is replayed as plain text. + return textKind === "output_text" + ? { type: "refusal", refusal: block.reason } + : { type: textKind, text: block.reason }; case "thinking": case "tool_call": case "tool_result": diff --git a/src/responses.test.ts b/src/responses.test.ts index 52f90ca..e631290 100644 --- a/src/responses.test.ts +++ b/src/responses.test.ts @@ -271,6 +271,48 @@ describe("Responses request builder — quirk matrix", () => { { type: "output_text", text: "hi" }, ]); }); + + const refusalHistory: ConversationTurn[] = [ + { role: "user", timestamp: 0, content: [{ type: "text", text: "hi" }] }, + { + role: "assistant", + timestamp: 0, + content: [{ type: "refusal", reason: "I can't help with that." }], + }, + { + role: "user", + timestamp: 0, + content: [{ type: "refusal", reason: "quoted refusal" }], + }, + ]; + + test.each([ + { + shape: "typed", + assistant: [{ type: "refusal", refusal: "I can't help with that." }], + user: [{ type: "input_text", text: "quoted refusal" }], + }, + { + shape: "flat", + assistant: "I can't help with that.", + user: "quoted refusal", + }, + ] as const)( + "$shape shape replays a refusal in history instead of throwing", + ({ shape, assistant, user }) => { + const adapter = createOpenAIResponsesAdapter(source, { + contentShape: shape, + }); + const request = adapter.buildRequest(refusalHistory, "model", {}); + const items = inputItemsOf(request); + expect(items[1]).toEqual({ + type: "message", + role: "assistant", + content: assistant, + }); + expect(items[2]?.["content"]).toEqual(user); + }, + ); }); describe("Responses parser — event schema validation", () => {