Files
2026-07-13 13:39:12 +08:00

300 lines
10 KiB
TypeScript

import { extractUsage } from "./usageTracking.ts";
import { parseSSEDataPayload } from "./streamHelpers.ts";
import {
backfillResponsesCompletedOutput,
normalizeResponsesSseIds,
pushUniqueResponsesOutputItems,
stringifyIdValue,
stripResponsesLifecycleEcho,
} from "./responsesStreamHelpers.ts";
import { getAnyReasoningValue } from "./reasoningFields.ts";
type JsonRecord = Record<string, unknown>;
type EventPrefixBuffer = {
eventType: () => string;
flush: () => string;
prefixData: (output: string, line: string) => string;
remember: (line: string) => void;
};
export type PassthroughTailProcessorContext = {
getSkipPassthroughEvent: () => boolean;
setSkipPassthroughEvent: (value: boolean) => void;
clearPendingPassthroughEvent: () => void;
shouldAbortOnClaudeLifecycle: (payload: unknown) => boolean;
emitClaudeEmptyStreamErrorAndAbort: () => void;
isClaudeEventPayload: (payload: unknown) => boolean;
updateClaudeEmptyResponseLifecycle: (payload: unknown) => void;
passthroughEventPrefix: EventPrefixBuffer;
emitConvertedOutput: (output: string) => void;
pushProviderPayload: (payload: unknown) => void;
pushClientPayload: (payload: unknown) => void;
setPassthroughResponsesId: (value: string) => void;
setUsage: (value: unknown) => void;
addTotalContentLength: (value: number) => void;
appendPassthroughContent: (value: string) => void;
appendPassthroughReasoning: (value: string) => void;
getResponsesReasoningKey: (payload: Record<string, unknown>) => string | null;
markResponsesReasoningSummarySeen: (key: string) => void;
ensureVisibleResponsesReasoningSummary: (payload: Record<string, unknown>) => boolean;
emitSyntheticResponsesReasoningSummary: (payload: Record<string, unknown>) => void;
passthroughResponsesOutputItems: unknown[];
passthroughResponsesPendingFunctionCalls: Map<string, JsonRecord>;
getPassthroughResponsesCurrentFunctionCallKey: () => string | null;
setPassthroughResponsesCurrentFunctionCallKey: (value: string | null) => void;
hasPassthroughToolCalls: () => boolean;
toResponsesCompletedWithToolCalls: (parsed: JsonRecord) => JsonRecord;
};
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function getFunctionCallPendingKey(item: JsonRecord | null): string | null {
if (!item) return null;
if (typeof item.id === "string") return item.id;
if (typeof item.call_id === "string") return item.call_id;
return null;
}
function handleResponsesTailPayload(
parsed: JsonRecord,
output: string,
context: PassthroughTailProcessorContext
): string {
const responsesIdsNormalized = normalizeResponsesSseIds(parsed);
const parsedResponse = asRecord(parsed.response);
const responseId =
(parsedResponse ? stringifyIdValue(parsedResponse.id) : null) ||
stringifyIdValue(parsed.response_id);
if (responseId) {
context.setPassthroughResponsesId(responseId);
}
const extracted = extractUsage(parsed);
if (extracted) {
context.setUsage(extracted);
}
if (typeof parsed.delta === "string") {
context.addTotalContentLength(parsed.delta.length);
}
if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
context.appendPassthroughContent(parsed.delta);
}
if (
parsed.type === "response.reasoning_summary_text.delta" ||
parsed.type === "response.reasoning_summary_text.done" ||
parsed.type === "response.reasoning_summary_part.done"
) {
const reasoningKey = context.getResponsesReasoningKey(parsed);
if (reasoningKey) {
context.markResponsesReasoningSummarySeen(reasoningKey);
}
}
if (
parsed.type === "response.output_item.added" &&
asRecord(parsed.item).type === "function_call"
) {
const item = { ...(parsed.item as JsonRecord) };
const pendingKey = getFunctionCallPendingKey(item);
if (pendingKey) {
if (typeof item.arguments !== "string") {
item.arguments = "";
}
context.passthroughResponsesPendingFunctionCalls.set(pendingKey, item);
context.setPassthroughResponsesCurrentFunctionCallKey(pendingKey);
}
}
if (parsed.type === "response.function_call_arguments.delta") {
const pendingKey =
typeof parsed.item_id === "string"
? parsed.item_id
: context.getPassthroughResponsesCurrentFunctionCallKey();
const pending = pendingKey
? context.passthroughResponsesPendingFunctionCalls.get(pendingKey)
: undefined;
if (pending && typeof parsed.delta === "string") {
const previousArgs = typeof pending.arguments === "string" ? pending.arguments : "";
pending.arguments = previousArgs + parsed.delta;
}
}
if (parsed.type === "response.function_call_arguments.done") {
const pendingKey =
typeof parsed.item_id === "string"
? parsed.item_id
: context.getPassthroughResponsesCurrentFunctionCallKey();
const pending = pendingKey
? context.passthroughResponsesPendingFunctionCalls.get(pendingKey)
: undefined;
if (pending) {
if (typeof parsed.arguments === "string") {
pending.arguments = parsed.arguments;
}
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [pending]);
}
}
if (parsed.type === "response.output_item.done" && parsed.item) {
const reasoningSummaryInjected = context.ensureVisibleResponsesReasoningSummary(parsed);
context.emitSyntheticResponsesReasoningSummary(parsed);
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [parsed.item]);
if (reasoningSummaryInjected) {
output = `data: ${JSON.stringify(parsed)}\n\n`;
}
const item = asRecord(parsed.item);
if (item.type === "function_call") {
const pendingKey = getFunctionCallPendingKey(item);
if (pendingKey) {
context.passthroughResponsesPendingFunctionCalls.delete(pendingKey);
if (context.getPassthroughResponsesCurrentFunctionCallKey() === pendingKey) {
context.setPassthroughResponsesCurrentFunctionCallKey(null);
}
}
}
}
if (
parsed.type === "response.completed" &&
Array.isArray(asRecord(parsed.response).output) &&
(asRecord(parsed.response).output as unknown[]).length > 0
) {
pushUniqueResponsesOutputItems(
context.passthroughResponsesOutputItems,
asRecord(parsed.response).output as unknown[]
);
}
if (
parsed.type === "response.completed" &&
context.passthroughResponsesPendingFunctionCalls.size > 0
) {
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [
...context.passthroughResponsesPendingFunctionCalls.values(),
]);
context.passthroughResponsesPendingFunctionCalls.clear();
context.setPassthroughResponsesCurrentFunctionCallKey(null);
}
const textualToolCallBackfilled =
parsed.type === "response.completed" && context.hasPassthroughToolCalls();
const outputPayload = textualToolCallBackfilled
? context.toResponsesCompletedWithToolCalls(parsed)
: parsed;
const stripped = stripResponsesLifecycleEcho(outputPayload);
const backfilled = backfillResponsesCompletedOutput(
outputPayload,
context.passthroughResponsesOutputItems
);
if (stripped || backfilled || textualToolCallBackfilled || responsesIdsNormalized) {
output = `data: ${JSON.stringify(outputPayload)}\n\n`;
}
return output;
}
function handleOpenAiTailPayload(parsed: JsonRecord, context: PassthroughTailProcessorContext) {
const firstChoice = Array.isArray(parsed.choices)
? (parsed.choices[0] as JsonRecord | undefined)
: undefined;
const delta = asRecord(firstChoice?.delta);
if (typeof delta.content === "string") {
context.appendPassthroughContent(delta.content);
context.addTotalContentLength(delta.content.length);
}
const reasoningDelta = getAnyReasoningValue(delta);
if (reasoningDelta) {
context.appendPassthroughReasoning(reasoningDelta);
}
}
export function processBufferedPassthroughLine(
line: string,
context: PassthroughTailProcessorContext
): boolean {
const trimmed = line.trim();
if (context.getSkipPassthroughEvent()) {
if (!trimmed) {
context.setSkipPassthroughEvent(false);
context.clearPendingPassthroughEvent();
}
return false;
}
if (/^event:\s*keepalive\b/i.test(trimmed)) {
context.setSkipPassthroughEvent(true);
context.clearPendingPassthroughEvent();
return false;
}
if (/^event:/i.test(trimmed)) {
const eventType = trimmed.replace(/^event:\s*/i, "");
if (context.shouldAbortOnClaudeLifecycle({ type: eventType })) {
context.emitClaudeEmptyStreamErrorAndAbort();
return true;
}
context.passthroughEventPrefix.remember(line);
return false;
}
if (/^(?::|id:|retry:)/i.test(trimmed)) {
context.passthroughEventPrefix.remember(line);
return false;
}
if (!trimmed) {
const pendingOutput = context.passthroughEventPrefix.flush();
if (pendingOutput) {
context.emitConvertedOutput(pendingOutput);
}
context.clearPendingPassthroughEvent();
return false;
}
if (!trimmed.startsWith("data:")) {
context.passthroughEventPrefix.remember(line);
return false;
}
const parsedPassthroughData = parseSSEDataPayload(trimmed.slice(5), {
eventType: context.passthroughEventPrefix.eventType(),
});
if (parsedPassthroughData?.done === true) {
return false;
}
let output =
line.startsWith("data:") && !line.startsWith("data: ")
? `data: ${line.slice(5)}\n\n`
: `${line}\n\n`;
if (parsedPassthroughData) {
context.pushProviderPayload(parsedPassthroughData);
if (context.shouldAbortOnClaudeLifecycle(parsedPassthroughData)) {
context.emitClaudeEmptyStreamErrorAndAbort();
return true;
}
if (context.isClaudeEventPayload(parsedPassthroughData)) {
context.updateClaudeEmptyResponseLifecycle(parsedPassthroughData);
}
const parsed = parsedPassthroughData as JsonRecord;
const parsedType = typeof parsed.type === "string" ? parsed.type : "";
const isResponses = parsedType.startsWith("response.");
const isClaude = context.isClaudeEventPayload(parsed);
if (isResponses) {
output = handleResponsesTailPayload(parsed, output, context);
} else if (!isClaude) {
handleOpenAiTailPayload(parsed, context);
}
context.pushClientPayload(parsed);
}
output = context.passthroughEventPrefix.prefixData(output, line);
context.emitConvertedOutput(output);
return false;
}