Merge PR #6513: fix(ai): strip output-only statuses from Responses replay (@Ant39140)
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses native history replay sending output-only `status` fields back as input, preventing `input[N].status` failures in long-running sessions. ([#6513](https://github.com/can1357/oh-my-pi/pull/6513) by [@Ant39140](https://github.com/Ant39140))
|
||||
|
||||
### Added
|
||||
|
||||
- OAuth logins now stamp `authorizedAt` (epoch ms of the interactive login) on the stored credential, and every refresh-persist path preserves it. Anthropic expires the whole OAuth grant family ~30 days after authorization regardless of refresh-token rotation (observed as `invalid_grant: "Refresh token expired"` on the latest rotated token, exactly 30 days after login, across four production accounts), so the login anchor is what makes re-login deadlines computable. Exported `ANTHROPIC_OAUTH_GRANT_TTL_MS` alongside the anthropic OAuth flow.
|
||||
|
||||
@@ -203,9 +203,12 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
|
||||
if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item);
|
||||
if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item);
|
||||
|
||||
// Provider payload stores raw output items. Computer calls retain their stable
|
||||
// provider item ID; other replay items strip IDs and normalize call_id.
|
||||
// Strip status only from item types whose replay input rejects output
|
||||
// lifecycle metadata. Hosted built-in tool items require status for replay.
|
||||
const { id: _id, ...sanitizedItem } = item;
|
||||
if (item.type === "message" || item.type === "function_call" || item.type === "custom_tool_call") {
|
||||
delete sanitizedItem.status;
|
||||
}
|
||||
if (item.type === "computer_call" && typeof item.id === "string") sanitizedItem.id = item.id;
|
||||
if (typeof item.call_id === "string") {
|
||||
sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
|
||||
@@ -224,9 +227,6 @@ function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record<string, unkn
|
||||
if (typeof item.encrypted_content === "string" || item.encrypted_content === null) {
|
||||
sanitizedItem.encrypted_content = item.encrypted_content;
|
||||
}
|
||||
if (item.status === "in_progress" || item.status === "completed" || item.status === "incomplete") {
|
||||
sanitizedItem.status = item.status;
|
||||
}
|
||||
return sanitizedItem as unknown as OpenAIResponsesReplayItem;
|
||||
}
|
||||
|
||||
|
||||
@@ -944,9 +944,9 @@ describe("OpenAI responses history payload", () => {
|
||||
const payload = (await captureResponsesPayload(model, incrementalContext)) as { input?: unknown[] };
|
||||
expect(payload.input).toEqual([
|
||||
{ role: "user", content: [{ type: "input_text", text: "first question" }] },
|
||||
...incrementalItems1.map(({ id: _id, ...item }) => item),
|
||||
...incrementalItems1.map(({ id: _id, status: _status, ...item }) => item),
|
||||
{ role: "user", content: [{ type: "input_text", text: "second question" }] },
|
||||
...incrementalItems2.map(({ id: _id, ...item }) => item),
|
||||
...incrementalItems2.map(({ id: _id, status: _status, ...item }) => item),
|
||||
{ role: "user", content: [{ type: "input_text", text: "third question" }] },
|
||||
]);
|
||||
});
|
||||
@@ -1125,13 +1125,15 @@ describe("OpenAI responses history payload", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips replay-only ids and item references while preserving paired call_id values", async () => {
|
||||
it("strips output-only replay metadata while preserving paired call_id values", async () => {
|
||||
const opaqueReasoningId = `item_${"copilot/reasoning+token=".repeat(8)}`;
|
||||
const opaqueMessageId = `item_${"copilot/message+opaque=".repeat(8)}`;
|
||||
const opaqueCallId = `call_${"copilot/tool-call+opaque/=".repeat(8)}`;
|
||||
const opaqueFunctionItemId = `item_${"copilot/function-item+opaque/=".repeat(8)}`;
|
||||
const opaqueCustomCallId = `call_${"copilot/custom-call+opaque/=".repeat(8)}`;
|
||||
const opaqueCustomItemId = `item_${"copilot/custom-item+opaque/=".repeat(8)}`;
|
||||
const replayHistoryItems: Array<Record<string, unknown>> = [
|
||||
{ type: "reasoning", id: opaqueReasoningId, encrypted_content: "enc_opaque" },
|
||||
{ type: "reasoning", id: opaqueReasoningId, encrypted_content: "enc_opaque", status: "completed" },
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
@@ -1148,6 +1150,19 @@ describe("OpenAI responses history payload", () => {
|
||||
status: "completed",
|
||||
},
|
||||
{ type: "function_call_output", id: "fco_should_be_removed", call_id: opaqueCallId, output: "72F" },
|
||||
{
|
||||
type: "custom_tool_call",
|
||||
id: opaqueCustomItemId,
|
||||
call_id: opaqueCustomCallId,
|
||||
name: "apply_patch",
|
||||
input: "*** Begin Patch\n*** End Patch\n",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
type: "custom_tool_call_output",
|
||||
call_id: opaqueCustomCallId,
|
||||
output: "patch applied",
|
||||
},
|
||||
{ type: "item_reference", id: opaqueMessageId },
|
||||
];
|
||||
const context: Context = {
|
||||
@@ -1163,6 +1178,7 @@ describe("OpenAI responses history payload", () => {
|
||||
const messageItem = findResponsesInputItem(payload.input, "message");
|
||||
const functionCallItem = findResponsesInputItem(payload.input, "function_call");
|
||||
const functionCallOutputItem = findResponsesInputItem(payload.input, "function_call_output");
|
||||
const customToolCallItem = findResponsesInputItem(payload.input, "custom_tool_call");
|
||||
const itemReference = findResponsesInputItem(payload.input, "item_reference");
|
||||
const expectedCallId = truncateResponseItemId(opaqueCallId, "call");
|
||||
|
||||
@@ -1170,11 +1186,16 @@ describe("OpenAI responses history payload", () => {
|
||||
expect(messageItem).toBeDefined();
|
||||
expect(functionCallItem).toBeDefined();
|
||||
expect(functionCallOutputItem).toBeDefined();
|
||||
expect(customToolCallItem).toBeDefined();
|
||||
expect(reasoningItem?.id).toBeUndefined();
|
||||
expect(messageItem?.id).toBeUndefined();
|
||||
expect(functionCallItem?.id).toBeUndefined();
|
||||
expect(functionCallOutputItem?.id).toBeUndefined();
|
||||
expect(itemReference).toBeUndefined();
|
||||
expect(reasoningItem).not.toHaveProperty("status");
|
||||
expect(messageItem).not.toHaveProperty("status");
|
||||
expect(functionCallItem).not.toHaveProperty("status");
|
||||
expect(customToolCallItem).not.toHaveProperty("status");
|
||||
expect(
|
||||
(payload.input ?? []).some(
|
||||
item => item && typeof item === "object" && "id" in (item as Record<string, unknown>),
|
||||
@@ -1184,15 +1205,19 @@ describe("OpenAI responses history payload", () => {
|
||||
expect(functionCallItem).toBeDefined();
|
||||
expect(functionCallItem!.call_id).toBe(expectedCallId);
|
||||
expect(functionCallOutputItem?.call_id).toBe(expectedCallId);
|
||||
expect(customToolCallItem?.call_id).toBe(truncateResponseItemId(opaqueCustomCallId, "call"));
|
||||
expect((functionCallItem!.call_id as string).length).toBeLessThanOrEqual(64);
|
||||
expect(containsAssistantOutputText(payload.input, "Sanitized assistant answer")).toBe(true);
|
||||
expect(replayHistoryItems[0]?.id).toBe(opaqueReasoningId);
|
||||
expect(replayHistoryItems[1]?.id).toBe(opaqueMessageId);
|
||||
expect(replayHistoryItems[2]?.id).toBe(opaqueFunctionItemId);
|
||||
expect(replayHistoryItems[2]?.call_id).toBe(opaqueCallId);
|
||||
expect(replayHistoryItems[1]?.status).toBe("completed");
|
||||
expect(replayHistoryItems[2]?.status).toBe("completed");
|
||||
expect(replayHistoryItems[3]?.id).toBe("fco_should_be_removed");
|
||||
expect(replayHistoryItems[3]?.call_id).toBe(opaqueCallId);
|
||||
expect(replayHistoryItems[4]?.id).toBe(opaqueMessageId);
|
||||
expect(replayHistoryItems[4]?.status).toBe("completed");
|
||||
expect(replayHistoryItems[6]?.id).toBe(opaqueMessageId);
|
||||
});
|
||||
|
||||
it("backward compat: old full-snapshot payloads still replace history for legacy same-provider assistant turns", async () => {
|
||||
|
||||
Reference in New Issue
Block a user