Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions e2e/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
23 changes: 22 additions & 1 deletion src/protocol/iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
Loading