Skip to content

Commit e4c855b

Browse files
committed
fix: drop invalid previousResponseId on server errors
1 parent cb9bdaf commit e4c855b

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)