mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: preserve conversation context on retry after previous_response_id error
This commit is contained in:
parent
1f5ee4307e
commit
8093615b8d
2 changed files with 158 additions and 16 deletions
|
|
@ -859,6 +859,119 @@ describe("OpenAiNativeHandler", () => {
|
|||
expect(secondCallBody.previous_response_id).toBe("resp_789")
|
||||
})
|
||||
|
||||
it("should retry with full conversation when previous_response_id fails", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
// First call fails with invalid previous_response_id error
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: "Bad Request",
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Invalid previous_response_id: resp_old",
|
||||
code: "invalid_previous_response_id",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
// Second call (retry) succeeds
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Retry successful"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_new","usage":{"prompt_tokens":100,"completion_tokens":2}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
})
|
||||
|
||||
// Set up conversation with multiple messages
|
||||
const conversationMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "First response" },
|
||||
{ role: "user", content: "Second message" },
|
||||
{ role: "assistant", content: "Second response" },
|
||||
{ role: "user", content: "Latest message" },
|
||||
]
|
||||
|
||||
// Try to create message with a previous_response_id that will fail
|
||||
const stream = handler.createMessage(systemPrompt, conversationMessages, {
|
||||
taskId: "test-task",
|
||||
previousResponseId: "resp_old",
|
||||
})
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Should have retried successfully
|
||||
const textChunks = chunks.filter((c) => c.type === "text")
|
||||
expect(textChunks).toHaveLength(1)
|
||||
expect(textChunks[0].text).toBe("Retry successful")
|
||||
|
||||
// Verify two fetch calls were made
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First call should have previous_response_id and only latest message
|
||||
const firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body)
|
||||
expect(firstCallBody.previous_response_id).toBe("resp_old")
|
||||
expect(firstCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Latest message" }],
|
||||
},
|
||||
])
|
||||
|
||||
// Second call (retry) should NOT have previous_response_id and should have FULL conversation
|
||||
const secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
|
||||
expect(secondCallBody.previous_response_id).toBeUndefined()
|
||||
expect(secondCallBody.instructions).toBe(systemPrompt)
|
||||
expect(secondCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "First message" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "First response" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Second message" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Second response" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Latest message" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should only send latest message when using previous_response_id", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
)
|
||||
|
||||
// Make the request
|
||||
yield* this.executeRequest(requestBody, model, metadata)
|
||||
yield* this.executeRequest(requestBody, model, metadata, systemPrompt, messages)
|
||||
}
|
||||
|
||||
private buildRequestBody(
|
||||
|
|
@ -276,6 +276,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
requestBody: any,
|
||||
model: OpenAiNativeModel,
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
systemPrompt?: string,
|
||||
messages?: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
try {
|
||||
// Use the official SDK
|
||||
|
|
@ -297,18 +299,25 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
const errorMessage = sdkErr?.message || sdkErr?.error?.message || ""
|
||||
const is400Error = sdkErr?.status === 400 || sdkErr?.response?.status === 400
|
||||
const isPreviousResponseError =
|
||||
errorMessage.includes("Previous response") || errorMessage.includes("not found")
|
||||
errorMessage.includes("Previous response") ||
|
||||
errorMessage.includes("not found") ||
|
||||
errorMessage.includes("previous_response_id")
|
||||
|
||||
if (is400Error && requestBody.previous_response_id && isPreviousResponseError) {
|
||||
// Log the error and retry without the previous_response_id
|
||||
|
||||
// Remove the problematic previous_response_id and retry
|
||||
const retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// Clear the stored lastResponseId to prevent using it again
|
||||
this.lastResponseId = undefined
|
||||
|
||||
// Re-prepare the request body with full conversation (no previous_response_id)
|
||||
let retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// Re-prepare the input to send full conversation if we have the necessary data
|
||||
if (systemPrompt && messages) {
|
||||
// Re-prepare input without previous_response_id (will send full conversation)
|
||||
const { formattedInput } = this.prepareStructuredInput(systemPrompt, messages, undefined)
|
||||
retryRequestBody.input = formattedInput
|
||||
}
|
||||
|
||||
try {
|
||||
// Retry with the SDK
|
||||
const retryStream = (await (this.client as any).responses.create(
|
||||
|
|
@ -317,7 +326,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
if (typeof (retryStream as any)[Symbol.asyncIterator] !== "function") {
|
||||
// If SDK fails, fall back to SSE
|
||||
yield* this.makeGpt5ResponsesAPIRequest(retryRequestBody, model, metadata)
|
||||
yield* this.makeGpt5ResponsesAPIRequest(
|
||||
retryRequestBody,
|
||||
model,
|
||||
metadata,
|
||||
systemPrompt,
|
||||
messages,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -329,13 +344,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
return
|
||||
} catch (retryErr) {
|
||||
// If retry also fails, fall back to SSE
|
||||
yield* this.makeGpt5ResponsesAPIRequest(retryRequestBody, model, metadata)
|
||||
yield* this.makeGpt5ResponsesAPIRequest(retryRequestBody, model, metadata, systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For other errors, fallback to manual SSE via fetch
|
||||
yield* this.makeGpt5ResponsesAPIRequest(requestBody, model, metadata)
|
||||
yield* this.makeGpt5ResponsesAPIRequest(requestBody, model, metadata, systemPrompt, messages)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +439,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
requestBody: any,
|
||||
model: OpenAiNativeModel,
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
systemPrompt?: string,
|
||||
messages?: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
const apiKey = this.options.openAiNativeApiKey ?? "not-provided"
|
||||
const baseUrl = this.options.openAiNativeBaseUrl || "https://api.openai.com"
|
||||
|
|
@ -463,20 +480,32 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
// Check if this is a 400 error about previous_response_id not found
|
||||
const isPreviousResponseError =
|
||||
errorDetails.includes("Previous response") || errorDetails.includes("not found")
|
||||
errorDetails.includes("Previous response") ||
|
||||
errorDetails.includes("not found") ||
|
||||
errorDetails.includes("previous_response_id")
|
||||
|
||||
if (response.status === 400 && requestBody.previous_response_id && isPreviousResponseError) {
|
||||
// Log the error and retry without the previous_response_id
|
||||
|
||||
// Remove the problematic previous_response_id and retry
|
||||
const retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// Clear the stored lastResponseId to prevent using it again
|
||||
this.lastResponseId = undefined
|
||||
// Resolve the promise once to unblock any waiting requests
|
||||
this.resolveResponseId(undefined)
|
||||
|
||||
// Re-prepare the input without previous_response_id to send the full conversation
|
||||
let retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// If we have systemPrompt and messages, re-prepare the input to send full conversation
|
||||
if (systemPrompt && messages) {
|
||||
// Re-prepare input without previous_response_id (will send full conversation)
|
||||
// Note: We pass undefined metadata to prepareStructuredInput to ensure it doesn't use previousResponseId
|
||||
const { formattedInput } = this.prepareStructuredInput(systemPrompt, messages, undefined)
|
||||
|
||||
// Update the input in the retry request body to include full conversation
|
||||
retryRequestBody.input = formattedInput
|
||||
}
|
||||
|
||||
// Retry the request without the previous_response_id
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "POST",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue