mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(OpenAI Responses): gate replay when store is stripped
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user