fix responses stream failure logging
This commit is contained in:
@@ -14,6 +14,7 @@ type StreamReader = {
|
||||
type ChatProxyStreamSessionInput = {
|
||||
downstreamFormat: DownstreamFormat;
|
||||
modelName: string;
|
||||
successfulUpstreamPath: string;
|
||||
onParsedPayload?: (payload: unknown) => void;
|
||||
writeLines: (lines: string[]) => void;
|
||||
writeRaw: (chunk: string) => void;
|
||||
@@ -23,6 +24,11 @@ type ResponseSink = {
|
||||
end(): void;
|
||||
};
|
||||
|
||||
type ChatProxyStreamResult = {
|
||||
status: 'completed' | 'failed';
|
||||
errorMessage: string | null;
|
||||
};
|
||||
|
||||
export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput) {
|
||||
const downstreamTransformer = input.downstreamFormat === 'claude'
|
||||
? anthropicMessagesTransformer
|
||||
@@ -39,6 +45,36 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
? createOpenAiChatAggregateState()
|
||||
: null;
|
||||
let finalized = false;
|
||||
let terminalResult: ChatProxyStreamResult = {
|
||||
status: 'completed',
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
const extractFailureMessage = (payload: unknown, fallback = 'upstream stream failed'): string => {
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const record = payload as Record<string, unknown>;
|
||||
if (record.error && typeof record.error === 'object' && !Array.isArray(record.error)) {
|
||||
const message = (record.error as Record<string, unknown>).message;
|
||||
if (typeof message === 'string' && message.trim()) return message.trim();
|
||||
}
|
||||
if (typeof record.message === 'string' && record.message.trim()) return record.message.trim();
|
||||
if (record.response && typeof record.response === 'object' && !Array.isArray(record.response)) {
|
||||
const responseError = (record.response as Record<string, unknown>).error;
|
||||
if (responseError && typeof responseError === 'object' && !Array.isArray(responseError)) {
|
||||
const message = (responseError as Record<string, unknown>).message;
|
||||
if (typeof message === 'string' && message.trim()) return message.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const markFailed = (payload: unknown, fallbackMessage?: string) => {
|
||||
terminalResult = {
|
||||
status: 'failed',
|
||||
errorMessage: extractFailureMessage(payload, fallbackMessage),
|
||||
};
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (finalized) return;
|
||||
@@ -51,7 +87,12 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.downstreamFormat === 'openai' && chatAggregateState && chatAggregateState.choices.size > 0) {
|
||||
if (
|
||||
input.downstreamFormat === 'openai'
|
||||
&& terminalResult.status !== 'failed'
|
||||
&& chatAggregateState
|
||||
&& chatAggregateState.choices.size > 0
|
||||
) {
|
||||
const needsTerminalFinishChunk = Array.from(chatAggregateState.choices.values())
|
||||
.some((choice) => !choice.finishReason);
|
||||
if (needsTerminalFinishChunk) {
|
||||
@@ -109,6 +150,12 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
}
|
||||
|
||||
if (parsedPayload && typeof parsedPayload === 'object') {
|
||||
const payloadType = typeof (parsedPayload as Record<string, unknown>).type === 'string'
|
||||
? String((parsedPayload as Record<string, unknown>).type)
|
||||
: '';
|
||||
if (payloadType === 'response.failed' || payloadType === 'error') {
|
||||
markFailed(parsedPayload);
|
||||
}
|
||||
const normalizedEvent = downstreamTransformer.transformStreamEvent(parsedPayload, streamContext, input.modelName);
|
||||
if (input.downstreamFormat === 'openai' && chatAggregateState) {
|
||||
applyOpenAiChatStreamEvent(chatAggregateState, normalizedEvent);
|
||||
@@ -129,10 +176,18 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
};
|
||||
|
||||
return {
|
||||
consumeUpstreamFinalPayload(payload: unknown, fallbackText: string, response?: ResponseSink) {
|
||||
consumeUpstreamFinalPayload(payload: unknown, fallbackText: string, response?: ResponseSink): ChatProxyStreamResult {
|
||||
if (payload && typeof payload === 'object') {
|
||||
input.onParsedPayload?.(payload);
|
||||
}
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const payloadType = typeof (payload as Record<string, unknown>).type === 'string'
|
||||
? String((payload as Record<string, unknown>).type)
|
||||
: '';
|
||||
if (payloadType === 'response.failed' || payloadType === 'error') {
|
||||
markFailed(payload);
|
||||
}
|
||||
}
|
||||
if (input.downstreamFormat === 'openai') {
|
||||
const normalizedFinal = openAiChatOutbound.normalizeFinal(payload, input.modelName, fallbackText);
|
||||
streamContext.id = normalizedFinal.id;
|
||||
@@ -156,8 +211,9 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
}
|
||||
finalize();
|
||||
response?.end();
|
||||
return terminalResult;
|
||||
},
|
||||
async run(reader: StreamReader | null | undefined, response: ResponseSink) {
|
||||
async run(reader: StreamReader | null | undefined, response: ResponseSink): Promise<ChatProxyStreamResult> {
|
||||
const lifecycle = createProxyStreamLifecycle<ParsedSseEvent>({
|
||||
reader,
|
||||
response,
|
||||
@@ -166,6 +222,7 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
onEof: finalize,
|
||||
});
|
||||
await lifecycle.run();
|
||||
return terminalResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ type ResponseSink = {
|
||||
end(): void;
|
||||
};
|
||||
|
||||
type ResponsesProxyStreamResult = {
|
||||
status: 'completed' | 'failed';
|
||||
errorMessage: string | null;
|
||||
};
|
||||
|
||||
type ResponsesProxyStreamSessionInput = {
|
||||
modelName: string;
|
||||
successfulUpstreamPath: string;
|
||||
@@ -33,20 +38,58 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object';
|
||||
}
|
||||
|
||||
function getResponsesStreamFailureMessage(payload: unknown, fallback = 'upstream stream failed'): string {
|
||||
if (isRecord(payload)) {
|
||||
if (isRecord(payload.error) && typeof payload.error.message === 'string' && payload.error.message.trim()) {
|
||||
return payload.error.message.trim();
|
||||
}
|
||||
if (typeof payload.message === 'string' && payload.message.trim()) {
|
||||
return payload.message.trim();
|
||||
}
|
||||
if (isRecord(payload.response) && isRecord(payload.response.error) && typeof payload.response.error.message === 'string' && payload.response.error.message.trim()) {
|
||||
return payload.response.error.message.trim();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSessionInput) {
|
||||
const streamContext = openAiResponsesStream.createContext(input.modelName);
|
||||
const responsesState = createOpenAiResponsesAggregateState(input.modelName);
|
||||
let finalized = false;
|
||||
let terminalResult: ResponsesProxyStreamResult = {
|
||||
status: 'completed',
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
terminalResult = {
|
||||
status: 'completed',
|
||||
errorMessage: null,
|
||||
};
|
||||
input.writeLines(completeResponsesStream(responsesState, streamContext, input.getUsage()));
|
||||
};
|
||||
|
||||
const fail = (payload: unknown, fallbackMessage?: string) => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
terminalResult = {
|
||||
status: 'failed',
|
||||
errorMessage: getResponsesStreamFailureMessage(payload, fallbackMessage),
|
||||
};
|
||||
input.writeLines(failResponsesStream(responsesState, streamContext, input.getUsage(), payload));
|
||||
};
|
||||
|
||||
const closeOut = () => {
|
||||
if (finalized) return;
|
||||
finalize();
|
||||
};
|
||||
|
||||
const handleEventBlock = (eventBlock: ParsedSseEvent): boolean => {
|
||||
if (eventBlock.data === '[DONE]') {
|
||||
finalize();
|
||||
closeOut();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -71,8 +114,7 @@ export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSes
|
||||
|| payloadType === 'response.failed'
|
||||
);
|
||||
if (isFailureEvent) {
|
||||
input.writeLines(failResponsesStream(responsesState, streamContext, input.getUsage(), parsedPayload));
|
||||
finalized = true;
|
||||
fail(parsedPayload);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -97,15 +139,16 @@ export function createResponsesProxyStreamSession(input: ResponsesProxyStreamSes
|
||||
};
|
||||
|
||||
return {
|
||||
async run(reader: StreamReader | null | undefined, response: ResponseSink) {
|
||||
async run(reader: StreamReader | null | undefined, response: ResponseSink): Promise<ResponsesProxyStreamResult> {
|
||||
const lifecycle = createProxyStreamLifecycle<ParsedSseEvent>({
|
||||
reader,
|
||||
response,
|
||||
pullEvents: (buffer) => openAiResponsesStream.pullSseEvents(buffer),
|
||||
handleEvent: handleEventBlock,
|
||||
onEof: finalize,
|
||||
onEof: closeOut,
|
||||
});
|
||||
await lifecycle.run();
|
||||
return terminalResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user