forked from boos/BoosAPI
[codex] fix empty stream success handling and unknown usage logs (#343)
* fix: preserve repeated short responses deltas * fix empty stream success handling and unknown usage logs * fix proxy usage metadata typing * fix stream bookkeeping and explicit zero usage source * fix sticky fast path and usage placeholder detection
This commit is contained in:
@@ -4,6 +4,7 @@ import { type DownstreamFormat, type ParsedSseEvent } from '../../shared/normali
|
||||
import { createOpenAiChatAggregateState, applyOpenAiChatStreamEvent, finalizeOpenAiChatAggregate } from './aggregator.js';
|
||||
import { openAiChatOutbound } from './outbound.js';
|
||||
import { openAiChatStream } from './stream.js';
|
||||
import { config } from '../../../config.js';
|
||||
|
||||
type StreamReader = {
|
||||
read(): Promise<{ done: boolean; value?: Uint8Array }>;
|
||||
@@ -49,6 +50,9 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
status: 'completed',
|
||||
errorMessage: null,
|
||||
};
|
||||
let terminalNormalizedFinal: ReturnType<typeof openAiChatOutbound.normalizeFinal> | null = null;
|
||||
let forwardedDownstreamOutput = false;
|
||||
const pendingWrites: string[] = [];
|
||||
|
||||
const extractFailureMessage = (payload: unknown, fallback = 'upstream stream failed'): string => {
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
@@ -76,10 +80,116 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
};
|
||||
};
|
||||
|
||||
const hasMeaningfulChatAggregateOutput = (): boolean => {
|
||||
if (input.downstreamFormat !== 'openai' || !chatAggregateState) return false;
|
||||
for (const choice of chatAggregateState.choices.values()) {
|
||||
if (choice.content.length > 0) return true;
|
||||
if (choice.reasoning.length > 0) return true;
|
||||
if (choice.toolCalls.some((item) => item.id || item.name || item.arguments)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasMeaningfulNormalizedFinalOutput = (): boolean => {
|
||||
if (!terminalNormalizedFinal) return false;
|
||||
const choices = Array.isArray(terminalNormalizedFinal.choices)
|
||||
? terminalNormalizedFinal.choices
|
||||
: [];
|
||||
if (choices.some((choice) => (
|
||||
choice.content.length > 0
|
||||
|| choice.reasoningContent.length > 0
|
||||
|| choice.toolCalls.some((toolCall) => toolCall.id || toolCall.name || toolCall.arguments)
|
||||
))) {
|
||||
return true;
|
||||
}
|
||||
if (terminalNormalizedFinal.content.length > 0) return true;
|
||||
if (terminalNormalizedFinal.reasoningContent.length > 0) return true;
|
||||
return terminalNormalizedFinal.toolCalls.some((toolCall) => toolCall.id || toolCall.name || toolCall.arguments);
|
||||
};
|
||||
|
||||
const flushPendingWrites = () => {
|
||||
if (pendingWrites.length <= 0) return;
|
||||
input.writeLines([...pendingWrites]);
|
||||
pendingWrites.length = 0;
|
||||
};
|
||||
|
||||
const emitLines = (lines: string[], options?: { meaningful?: boolean; force?: boolean }) => {
|
||||
if (lines.length <= 0) return;
|
||||
if (input.downstreamFormat !== 'openai') {
|
||||
input.writeLines(lines);
|
||||
return;
|
||||
}
|
||||
if (forwardedDownstreamOutput) {
|
||||
input.writeLines(lines);
|
||||
return;
|
||||
}
|
||||
if (options?.force) {
|
||||
pendingWrites.length = 0;
|
||||
forwardedDownstreamOutput = true;
|
||||
input.writeLines(lines);
|
||||
return;
|
||||
}
|
||||
if (options?.meaningful) {
|
||||
forwardedDownstreamOutput = true;
|
||||
flushPendingWrites();
|
||||
input.writeLines(lines);
|
||||
return;
|
||||
}
|
||||
pendingWrites.push(...lines);
|
||||
};
|
||||
|
||||
const emitRaw = (chunk: string, options?: { meaningful?: boolean; force?: boolean }) => {
|
||||
if (!chunk) return;
|
||||
if (input.downstreamFormat !== 'openai') {
|
||||
input.writeRaw(chunk);
|
||||
return;
|
||||
}
|
||||
if (forwardedDownstreamOutput) {
|
||||
input.writeRaw(chunk);
|
||||
return;
|
||||
}
|
||||
if (options?.force) {
|
||||
pendingWrites.length = 0;
|
||||
forwardedDownstreamOutput = true;
|
||||
input.writeRaw(chunk);
|
||||
return;
|
||||
}
|
||||
if (options?.meaningful) {
|
||||
forwardedDownstreamOutput = true;
|
||||
flushPendingWrites();
|
||||
input.writeRaw(chunk);
|
||||
return;
|
||||
}
|
||||
pendingWrites.push(chunk);
|
||||
};
|
||||
|
||||
const shouldFailEmptyChatCompletion = (): boolean => {
|
||||
if (!config.proxyEmptyContentFailEnabled) return false;
|
||||
if (input.downstreamFormat !== 'openai') return false;
|
||||
if (terminalResult.status === 'failed') return false;
|
||||
if (hasMeaningfulChatAggregateOutput()) return false;
|
||||
if (hasMeaningfulNormalizedFinalOutput()) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
|
||||
if (shouldFailEmptyChatCompletion()) {
|
||||
markFailed({
|
||||
error: {
|
||||
message: 'Upstream returned empty content',
|
||||
},
|
||||
}, 'Upstream returned empty content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.downstreamFormat === 'openai' && !forwardedDownstreamOutput) {
|
||||
forwardedDownstreamOutput = true;
|
||||
flushPendingWrites();
|
||||
}
|
||||
|
||||
// For native Anthropic streams, EOF without message_stop is not a clean
|
||||
// completion. Forward the partial stream as-is instead of fabricating an
|
||||
// end_turn/message_stop pair that makes clients think the run finished.
|
||||
@@ -108,12 +218,12 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
}),
|
||||
).slice(-1)[0];
|
||||
if (terminalChunk) {
|
||||
input.writeLines([`data: ${JSON.stringify(terminalChunk)}\n\n`]);
|
||||
emitLines([`data: ${JSON.stringify(terminalChunk)}\n\n`], { meaningful: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.writeLines(downstreamTransformer.serializeDone(streamContext, claudeContext));
|
||||
emitLines(downstreamTransformer.serializeDone(streamContext, claudeContext), { meaningful: true });
|
||||
};
|
||||
|
||||
const handleEventBlock = async (eventBlock: ParsedSseEvent): Promise<boolean> => {
|
||||
@@ -153,19 +263,26 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
const payloadType = typeof (parsedPayload as Record<string, unknown>).type === 'string'
|
||||
? String((parsedPayload as Record<string, unknown>).type)
|
||||
: '';
|
||||
if (payloadType === 'response.failed' || payloadType === 'error') {
|
||||
const isFailurePayload = payloadType === 'response.failed' || payloadType === 'error';
|
||||
if (isFailurePayload) {
|
||||
markFailed(parsedPayload);
|
||||
}
|
||||
const normalizedEvent = downstreamTransformer.transformStreamEvent(parsedPayload, streamContext, input.modelName);
|
||||
if (input.downstreamFormat === 'openai' && chatAggregateState) {
|
||||
applyOpenAiChatStreamEvent(chatAggregateState, normalizedEvent);
|
||||
}
|
||||
input.writeLines(downstreamTransformer.serializeStreamEvent(normalizedEvent, streamContext, claudeContext));
|
||||
emitLines(
|
||||
downstreamTransformer.serializeStreamEvent(normalizedEvent, streamContext, claudeContext),
|
||||
{
|
||||
meaningful: hasMeaningfulChatAggregateOutput(),
|
||||
force: isFailurePayload,
|
||||
},
|
||||
);
|
||||
return input.downstreamFormat === 'claude' && claudeContext.doneSent;
|
||||
}
|
||||
|
||||
if (input.downstreamFormat === 'openai') {
|
||||
input.writeRaw(`data: ${eventBlock.data}\n\n`);
|
||||
emitRaw(`data: ${eventBlock.data}\n\n`, { meaningful: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -190,16 +307,18 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
}
|
||||
if (input.downstreamFormat === 'openai') {
|
||||
const normalizedFinal = openAiChatOutbound.normalizeFinal(payload, input.modelName, fallbackText);
|
||||
terminalNormalizedFinal = normalizedFinal;
|
||||
streamContext.id = normalizedFinal.id;
|
||||
streamContext.model = normalizedFinal.model;
|
||||
streamContext.created = normalizedFinal.created;
|
||||
input.writeLines(
|
||||
emitLines(
|
||||
openAiChatOutbound
|
||||
.buildSyntheticChunks(normalizedFinal)
|
||||
.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`),
|
||||
{ meaningful: true },
|
||||
);
|
||||
} else {
|
||||
input.writeLines(
|
||||
emitLines(
|
||||
anthropicMessagesTransformer.serializeUpstreamFinalAsStream(
|
||||
payload,
|
||||
input.modelName,
|
||||
@@ -207,6 +326,7 @@ export function createChatProxyStreamSession(input: ChatProxyStreamSessionInput)
|
||||
streamContext,
|
||||
claudeContext,
|
||||
),
|
||||
{ meaningful: true },
|
||||
);
|
||||
}
|
||||
finalize();
|
||||
|
||||
@@ -98,7 +98,7 @@ function shouldFailEmptyResponsesCompletion(input: {
|
||||
: null;
|
||||
if (hasMeaningfulAggregateOutput(input.state)) return false;
|
||||
if (responsePayload && hasMeaningfulResponsesPayloadOutput(responsePayload)) return false;
|
||||
return input.usage.completionTokens <= 0 && input.usage.totalTokens <= 0;
|
||||
return input.usage.completionTokens <= 0;
|
||||
}
|
||||
|
||||
function getResponsesStreamFailureMessage(payload: unknown, fallback = 'upstream stream failed'): string {
|
||||
|
||||
Reference in New Issue
Block a user