diff --git a/e2e/responses.test.ts b/e2e/responses.test.ts index 968b002..b13d210 100644 --- a/e2e/responses.test.ts +++ b/e2e/responses.test.ts @@ -239,6 +239,30 @@ describe("openai-responses adapter through runInference", () => { expect(error.data.error.message).toContain("backend exploded"); }); + test("response.incomplete surfaces its reason as a protocol_mismatch inference.error", async () => { + harness = setupHarness({ adapters: registry }); + const stream = harness.scenario.createStream(); + harness.scenario.whenRequestMatches(() => true, stream); + stream.enqueueAll( + [ + sse({ + type: "response.incomplete", + response: { + status: "incomplete", + incomplete_details: { reason: "max_output_tokens" }, + usage: { input_tokens: 10, output_tokens: 100 }, + }, + }), + ], + { startAt: 1 }, + ); + const events = await collect(harness, [userTurn("hi")]); + expect(events.some((e) => e.type === "inference.done")).toBe(false); + const error = errorEvent(events); + expect(error.data.error.category).toBe("protocol_mismatch"); + expect(error.data.error.message).toContain("max_output_tokens"); + }); + test("a prior signed reasoning turn replays as a reasoning item ahead of its function_call", async () => { harness = setupHarness({ adapters: registry }); const stream = harness.scenario.createStream(); diff --git a/src/protocol/iterator.ts b/src/protocol/iterator.ts index 19246ec..7e607c3 100644 --- a/src/protocol/iterator.ts +++ b/src/protocol/iterator.ts @@ -70,6 +70,9 @@ const CompletedEvent = type({ response: { "usage?": ResponsesUsage } }); const FailedEvent = type({ "response?": { "error?": { "message?": "string" } }, }); +const IncompleteEvent = type({ + response: { "incomplete_details?": { "reason?": "string" } }, +}); const ErrorEvent = type({ "message?": "string" }); // Maps the Responses API's usage object onto the internal TokenUsage, @@ -459,6 +462,24 @@ export function parseResponse( const message = validated.response?.error?.message ?? "response failed"; throw new ProtocolMismatchError(`${provider}: ${message}`, parsed); } + // Throws like parseJSONResponse does for status "incomplete". The + // harness drops the events of a batch that throws, so this event's usage + // cannot also be reported. + case "response.incomplete": { + const validated = IncompleteEvent(parsed); + if (validated instanceof type.errors) { + throw protocolMismatch( + provider, + `response.incomplete failed schema validation: ${validated.summary}`, + parsed, + ); + } + throw protocolMismatch( + provider, + `response status is "incomplete": ${validated.response.incomplete_details?.reason ?? "no reason given"}`, + parsed, + ); + } case "error": { const validated = ErrorEvent(parsed); if (validated instanceof type.errors) { @@ -475,7 +496,7 @@ export function parseResponse( } default: // Lifecycle envelopes (response.created, response.in_progress, - // content_part.*, *_text.done, response.incomplete) carry no + // content_part.*, *_text.done) carry no // incremental payload a caller needs; ignore them. Unknown event // types are protocol-legal — the vocabulary is expected to grow. return events;