AIX: OpenAI Responses: non-fatal error if sealed

OpenAI sometimes emits a trailing 'error' event (e.g. rate-limit/TPM
advisory) AFTER 'response.completed'. The blanket error handler treated
it as fatal, calling setDialectTerminatingIssue which:
  - injected a red [Openai Issue] fragment into the finished message
  - overrode the prior setDialectEnded('done-dialect') with 'issue-dialect'
  - flipped the AIX outcome to 'failed', turning the Beam ray red

Track a #responseSealed flag set by the three terminal events
(response.completed/failed/incomplete) and short-circuit trailing 'error'
events with a server-log only - keeping mid-stream errors fatal as before.
This commit is contained in:
Enrico Ros
2026-05-02 17:53:43 -07:00
parent 31948a62f9
commit 13b928d68b
@@ -99,6 +99,7 @@ class ResponseParserStateMachine {
// streaming state tracking // streaming state tracking
#hasFunctionCalls: boolean = false; // tracks if we've seen function_call output items #hasFunctionCalls: boolean = false; // tracks if we've seen function_call output items
#responseSealed: boolean = false; // true once response.completed/failed/incomplete has been processed - trailing 'error' events are advisory only
// hosted tool configuration echo (captured at response.created) // hosted tool configuration echo (captured at response.created)
#imageGenToolCfg: TImageGenToolCfg | undefined; #imageGenToolCfg: TImageGenToolCfg | undefined;
@@ -264,6 +265,14 @@ class ResponseParserStateMachine {
return this.#hasFunctionCalls; return this.#hasFunctionCalls;
} }
markResponseSealed() {
this.#responseSealed = true;
}
get responseSealed() {
return this.#responseSealed;
}
// Hosted tool config capture // Hosted tool config capture
@@ -377,11 +386,13 @@ export function createOpenAIResponsesEventParser(vendor: 'openai' | 'xai'): Chat
} }
// -> End of the response // -> End of the response
R.markResponseSealed();
pt.setDialectEnded('done-dialect'); // OpenAI Responses: 'response.completed' pt.setDialectEnded('done-dialect'); // OpenAI Responses: 'response.completed'
break; break;
case 'response.failed': case 'response.failed':
R.setResponse(eventType, event.response); R.setResponse(eventType, event.response);
R.markResponseSealed();
pt.setTokenStopReason('cg-issue'); // generic issue? pt.setTokenStopReason('cg-issue'); // generic issue?
console.warn(`[DEV] AIX: FIXME: OpenAI-Response failed ${eventType}:`, event.response); console.warn(`[DEV] AIX: FIXME: OpenAI-Response failed ${eventType}:`, event.response);
// TODO: extract and forward error details // TODO: extract and forward error details
@@ -390,6 +401,7 @@ export function createOpenAIResponsesEventParser(vendor: 'openai' | 'xai'): Chat
case 'response.incomplete': case 'response.incomplete':
// TODO: We haven't seen one of those events yet; we need to see what happens and parse it! // TODO: We haven't seen one of those events yet; we need to see what happens and parse it!
R.setResponse(eventType, event.response); R.setResponse(eventType, event.response);
R.markResponseSealed();
// -> Status: handle incomplete response // -> Status: handle incomplete response
if (event.response.incomplete_details?.reason === 'max_output_tokens') if (event.response.incomplete_details?.reason === 'max_output_tokens')
@@ -729,6 +741,14 @@ export function createOpenAIResponsesEventParser(vendor: 'openai' | 'xai'): Chat
const errorMessage = safeErrorString(event.error?.message || event?.message) ?? undefined; const errorMessage = safeErrorString(event.error?.message || event?.message) ?? undefined;
const errorParam = safeErrorString(event.error?.param || event?.param) ?? undefined; const errorParam = safeErrorString(event.error?.param || event?.param) ?? undefined;
// Trailing-error guard: if the response already reached a terminal state (completed/failed/incomplete),
// an 'error' event arriving after is an upstream advisory (e.g. rate-limit headroom) and must NOT
// override the prior termination - otherwise it flips the message to red and the Beam ray to 'error'.
if (R.responseSealed) {
console.warn(`[DEV] AIX: OpenAI Responses: trailing 'error' after sealed response - ignored: ${errorCode || 'Error'}: ${errorMessage || 'unknown.'}${errorParam ? ` (param: ${errorParam})` : ''}`);
break;
}
// Transmit the error as text - note: throw if you want to transmit as 'error' // Transmit the error as text - note: throw if you want to transmit as 'error'
// FIXME: potential point for throwing OperationRetrySignal (using 'srv-warn' for now) // FIXME: potential point for throwing OperationRetrySignal (using 'srv-warn' for now)
pt.setDialectTerminatingIssue(`${errorCode || 'Error'}: ${errorMessage || 'unknown.'}${errorParam ? ` (param: ${errorParam})` : ''}`, IssueSymbols.Generic, 'srv-warn'); pt.setDialectTerminatingIssue(`${errorCode || 'Error'}: ${errorMessage || 'unknown.'}${errorParam ? ` (param: ${errorParam})` : ''}`, IssueSymbols.Generic, 'srv-warn');