fix(OpenAI Responses): gate replay when store is stripped

This commit is contained in:
snowzlm
2026-06-07 22:47:15 +08:00
committed by Ayaan Zaidi
parent 2e8a2d617d
commit 8ecf55b36a
4 changed files with 128 additions and 6 deletions
@@ -3100,6 +3100,33 @@ describe("applyExtraParamsToAgent", () => {
expect(capturedOptions?.replayResponsesItemIds).toBe(true);
});
it("disables Responses replay item ids when custom Responses routes strip store", () => {
let capturedOptions:
| (SimpleStreamOptions & {
replayResponsesItemIds?: boolean;
})
| undefined;
const baseStreamFn: StreamFn = (_model, _context, options) => {
capturedOptions = options;
return {} as ReturnType<StreamFn>;
};
const streamFn = createOpenAIResponsesContextManagementWrapper(baseStreamFn, undefined);
void streamFn(
{
api: "openai-responses",
provider: "custom-openai-responses",
id: "gpt-5.5",
baseUrl: "https://custom.example.invalid/v1",
compat: { supportsStore: false },
} as unknown as Model<"openai-responses">,
{ messages: [] },
{},
);
expect(capturedOptions?.replayResponsesItemIds).toBe(false);
});
it("forces store=true for azure-openai provider with openai-responses API (#42800)", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "azure-openai",
@@ -20,6 +20,17 @@ function buildModel(): Model<"openai-responses"> {
};
}
function buildStorelessCustomModel(): Model<"openai-responses"> {
return {
...buildModel(),
provider: "custom-openai-responses",
baseUrl: "https://custom.example.invalid/v1",
compat: {
supportsStore: false,
} as never,
};
}
function extractInput(payload: Record<string, unknown> | undefined) {
return Array.isArray(payload?.input) ? payload.input : [];
}
@@ -89,6 +100,8 @@ async function runAbortedOpenAIResponsesStream(params: {
parameters: ReturnType<typeof Type.Object>;
}>;
replayResponsesItemIds?: boolean;
usePolicyReplayDefault?: boolean;
model?: Model<"openai-responses">;
}) {
// Abort after payload capture so tests inspect serialization without network I/O.
const controller = new AbortController();
@@ -96,7 +109,7 @@ async function runAbortedOpenAIResponsesStream(params: {
let payload: Record<string, unknown> | undefined;
const responseStream = stream(
buildModel(),
params.model ?? buildModel(),
{
systemPrompt: "system",
messages: params.messages,
@@ -104,7 +117,9 @@ async function runAbortedOpenAIResponsesStream(params: {
},
{
apiKey: "test",
replayResponsesItemIds: params.replayResponsesItemIds ?? true,
...(params.usePolicyReplayDefault
? {}
: { replayResponsesItemIds: params.replayResponsesItemIds ?? true }),
signal: controller.signal,
onPayload: (nextPayload: unknown) => {
payload = nextPayload as Record<string, unknown>;
@@ -121,6 +136,83 @@ async function runAbortedOpenAIResponsesStream(params: {
}
describe("openai-responses reasoning replay", () => {
it("omits Responses item ids for storeless custom providers while preserving tool call ids", async () => {
const assistantToolOnly = buildAssistantMessage({
stopReason: "toolUse",
content: [
buildReasoningPart("rs_storeless"),
{
type: "text",
text: "Checking.",
textSignature: JSON.stringify({ v: 1, id: "msg_storeless", phase: "final_answer" }),
},
{
type: "toolCall",
id: "call_storeless|fc_storeless",
name: "noop",
arguments: {},
},
],
});
const toolResult: ToolResultMessage = {
role: "toolResult",
toolCallId: "call_storeless|fc_storeless",
toolName: "noop",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: Date.now(),
};
const { input, types } = await runAbortedOpenAIResponsesStream({
model: buildStorelessCustomModel(),
usePolicyReplayDefault: true,
messages: [
{ role: "user", content: "Call noop.", timestamp: Date.now() },
assistantToolOnly,
toolResult,
{ role: "user", content: "Now reply with ok.", timestamp: Date.now() },
],
tools: [
{
name: "noop",
description: "no-op",
parameters: Type.Object({}, { additionalProperties: false }),
},
],
});
expect(types).toContain("message");
expect(types).toContain("function_call");
expect(types).toContain("function_call_output");
const replayedItemIds = input.filter(
(item): item is Record<string, unknown> =>
Boolean(item) &&
typeof item === "object" &&
["reasoning", "message", "function_call"].includes(
String((item as Record<string, unknown>).type),
) &&
typeof (item as Record<string, unknown>).id === "string",
);
expect(replayedItemIds).toEqual([]);
const functionCall = input.find(
(item) =>
item &&
typeof item === "object" &&
(item as Record<string, unknown>).type === "function_call",
) as Record<string, unknown> | undefined;
const functionCallOutput = input.find(
(item) =>
item &&
typeof item === "object" &&
(item as Record<string, unknown>).type === "function_call_output",
) as Record<string, unknown> | undefined;
expect(functionCall?.call_id).toBeTruthy();
expect(functionCallOutput?.call_id).toBe(functionCall?.call_id);
});
it("replays reasoning for tool-call-only turns (OpenAI requires it)", async () => {
const assistantToolOnly = buildAssistantMessage({
stopReason: "toolUse",
+2 -1
View File
@@ -2208,7 +2208,8 @@ export function buildOpenAIResponsesParams(
const payloadPolicy = resolveOpenAIResponsesPayloadPolicy(model, {
storeMode: "disable",
});
const policyAllowsReplayIds = payloadPolicy.explicitStore !== false;
const policyAllowsReplayIds =
payloadPolicy.explicitStore !== false && !payloadPolicy.shouldStripStore;
const replayResponsesItemIds =
!isNativeCodexResponses && (options?.replayResponsesItemIds ?? policyAllowsReplayIds);
const messages = convertResponsesMessages(
+5 -3
View File
@@ -399,9 +399,11 @@ export function createOpenAIResponsesContextManagementWrapper(
const originalOnPayload = options?.onPayload;
const replayResponsesItemIds =
policy.explicitStore === undefined
? (options as OpenAIResponsesReplayOptions | undefined)?.replayResponsesItemIds
: policy.explicitStore;
policy.explicitStore === true
? true
: policy.explicitStore === false || policy.shouldStripStore
? false
: (options as OpenAIResponsesReplayOptions | undefined)?.replayResponsesItemIds;
const nextOptions: OpenAIResponsesReplayOptions = {
...options,
...(replayResponsesItemIds === undefined ? {} : { replayResponsesItemIds }),