Skip to content

Commit 2e16937

Browse files
authored
🤖 fix: drop stale OpenAI previousResponseId on 500s (#640)
I'm seeing persistent 500 errors from OpenAI occasionally on active chats, suggesting there's something malformed in the history or stored response object. Thinking that sending everything from scratch will most likely resolve the issue.
1 parent 944d419 commit 2e16937

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

src/node/services/streamManager.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,4 +455,58 @@ describe("StreamManager - previousResponseId recovery", () => {
455455
const errorWithoutId = new Error("Some other error");
456456
expect(extractMethod.call(streamManager, errorWithoutId)).toBeUndefined();
457457
});
458+
459+
test("recordLostResponseIdIfApplicable records IDs for explicit OpenAI errors", () => {
460+
const mockHistoryService = createMockHistoryService();
461+
const mockPartialService = createMockPartialService();
462+
const streamManager = new StreamManager(mockHistoryService, mockPartialService);
463+
464+
const recordMethod = Reflect.get(streamManager, "recordLostResponseIdIfApplicable") as (
465+
error: unknown,
466+
streamInfo: unknown
467+
) => void;
468+
expect(typeof recordMethod).toBe("function");
469+
470+
const apiError = new APICallError({
471+
message: "Previous response with id 'resp_deadbeef' not found.",
472+
url: "https://api.openai.com/v1/responses",
473+
requestBodyValues: {},
474+
statusCode: 400,
475+
responseHeaders: {},
476+
responseBody: "Previous response with id 'resp_deadbeef' not found.",
477+
isRetryable: false,
478+
data: { error: { code: "previous_response_not_found" } },
479+
});
480+
481+
recordMethod.call(streamManager, apiError, { messageId: "msg-1", model: "openai:gpt-mini" });
482+
483+
expect(streamManager.isResponseIdLost("resp_deadbeef")).toBe(true);
484+
});
485+
486+
test("recordLostResponseIdIfApplicable records IDs for 500 errors referencing previous responses", () => {
487+
const mockHistoryService = createMockHistoryService();
488+
const mockPartialService = createMockPartialService();
489+
const streamManager = new StreamManager(mockHistoryService, mockPartialService);
490+
491+
const recordMethod = Reflect.get(streamManager, "recordLostResponseIdIfApplicable") as (
492+
error: unknown,
493+
streamInfo: unknown
494+
) => void;
495+
expect(typeof recordMethod).toBe("function");
496+
497+
const apiError = new APICallError({
498+
message: "Internal error: Previous response with id 'resp_cafebabe' not found.",
499+
url: "https://api.openai.com/v1/responses",
500+
requestBodyValues: {},
501+
statusCode: 500,
502+
responseHeaders: {},
503+
responseBody: "Internal error: Previous response with id 'resp_cafebabe' not found.",
504+
isRetryable: false,
505+
data: { error: { code: "server_error" } },
506+
});
507+
508+
recordMethod.call(streamManager, apiError, { messageId: "msg-2", model: "openai:gpt-mini" });
509+
510+
expect(streamManager.isResponseIdLost("resp_cafebabe")).toBe(true);
511+
});
458512
});

src/node/services/streamManager.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,23 +1207,29 @@ export class StreamManager extends EventEmitter {
12071207
* Frontend will automatically retry, and buildProviderOptions will filter it out
12081208
*/
12091209
private recordLostResponseIdIfApplicable(error: unknown, streamInfo: WorkspaceStreamInfo): void {
1210-
const errorCode = this.extractErrorCode(error);
1211-
if (errorCode !== "previous_response_not_found") {
1210+
const responseId = this.extractPreviousResponseIdFromError(error);
1211+
if (!responseId) {
12121212
return;
12131213
}
12141214

1215-
// Extract previousResponseId from the stream's initial provider options
1216-
// We need to check streamInfo.streamResult.providerOptions, but that's not exposed
1217-
// Instead, we can extract it from the error response body if it contains it
1218-
const responseId = this.extractPreviousResponseIdFromError(error);
1219-
if (responseId) {
1220-
log.info("Recording lost previousResponseId for future filtering", {
1221-
previousResponseId: responseId,
1222-
workspaceId: streamInfo.messageId,
1223-
model: streamInfo.model,
1224-
});
1225-
this.lostResponseIds.add(responseId);
1215+
const errorCode = this.extractErrorCode(error);
1216+
const statusCode = this.extractStatusCode(error);
1217+
const shouldRecord =
1218+
errorCode === "previous_response_not_found" || statusCode === 404 || statusCode === 500;
1219+
1220+
if (!shouldRecord || this.lostResponseIds.has(responseId)) {
1221+
return;
12261222
}
1223+
1224+
log.info("Recording lost previousResponseId for future filtering", {
1225+
previousResponseId: responseId,
1226+
messageId: streamInfo.messageId,
1227+
model: streamInfo.model,
1228+
statusCode,
1229+
errorCode,
1230+
});
1231+
1232+
this.lostResponseIds.add(responseId);
12271233
}
12281234

12291235
/**
@@ -1280,6 +1286,21 @@ export class StreamManager extends EventEmitter {
12801286
return undefined;
12811287
}
12821288

1289+
private extractStatusCode(error: unknown): number | undefined {
1290+
if (APICallError.isInstance(error) && typeof error.statusCode === "number") {
1291+
return error.statusCode;
1292+
}
1293+
1294+
if (typeof error === "object" && error !== null && "statusCode" in error) {
1295+
const candidate = (error as { statusCode?: unknown }).statusCode;
1296+
if (typeof candidate === "number") {
1297+
return candidate;
1298+
}
1299+
}
1300+
1301+
return undefined;
1302+
}
1303+
12831304
private getStructuredErrorCode(candidate: unknown): string | undefined {
12841305
if (typeof candidate === "object" && candidate !== null && "error" in candidate) {
12851306
const withError = candidate as { error?: unknown };

0 commit comments

Comments
 (0)