align vercel stream finalization with go

This commit is contained in:
CJACK
2026-04-26 08:29:23 +08:00
parent 7bff2c1bab
commit 40c61949e8
4 changed files with 207 additions and 3 deletions

View File

@@ -10,7 +10,7 @@ const {
formatOpenAIStreamToolCalls,
} = require('../helpers/stream-tool-sieve');
const { BASE_HEADERS } = require('../shared/deepseek-constants');
const { writeOpenAIError } = require('./error_shape');
const { writeOpenAIError, openAIErrorType } = require('./error_shape');
const { parseChunkForContent, isCitation } = require('./sse_parse');
const { buildUsage } = require('./token_usage');
const {
@@ -129,6 +129,7 @@ async function handleVercelStream(req, res, rawBody, payload) {
const toolSieveEnabled = toolPolicy.toolSieveEnabled;
const toolSieveState = createToolSieveState();
let toolCallsEmitted = false;
let toolCallsDoneEmitted = false;
const streamToolCallIDs = new Map();
const streamToolNames = new Map();
const decoder = new TextDecoder();
@@ -153,14 +154,16 @@ async function handleVercelStream(req, res, rawBody, payload) {
return;
}
const detected = parseStandaloneToolCalls(outputText, toolNames);
if (detected.length > 0 && !toolCallsEmitted) {
if (detected.length > 0 && !toolCallsDoneEmitted) {
toolCallsEmitted = true;
toolCallsDoneEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(detected, streamToolCallIDs) });
} else if (toolSieveEnabled) {
const tailEvents = flushToolSieve(toolSieveState, toolNames);
for (const evt of tailEvents) {
if (evt.type === 'tool_calls' && Array.isArray(evt.calls) && evt.calls.length > 0) {
toolCallsEmitted = true;
toolCallsDoneEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) });
resetStreamToolCallState(streamToolCallIDs, streamToolNames);
continue;
@@ -173,6 +176,15 @@ async function handleVercelStream(req, res, rawBody, payload) {
if (detected.length > 0 || toolCallsEmitted) {
reason = 'tool_calls';
}
if (detected.length === 0 && !toolCallsEmitted && outputText.trim() === '') {
const detail = upstreamEmptyOutputDetail(reason === 'content_filter', outputText, thinkingText);
sendFailedChunk(res, detail.status, detail.message, detail.code);
await releaseLease();
if (!res.writableEnded && !res.destroyed) {
res.end();
}
return;
}
sendFrame({
id: sessionID,
object: 'chat.completion.chunk',
@@ -234,7 +246,7 @@ async function handleVercelStream(req, res, rawBody, payload) {
return;
}
if (parsed.contentFilter) {
await finish('stop');
await finish(outputText.trim() === '' ? 'content_filter' : 'stop');
return;
}
if (parsed.finished) {
@@ -284,6 +296,7 @@ async function handleVercelStream(req, res, rawBody, payload) {
}
if (evt.type === 'tool_calls') {
toolCallsEmitted = true;
toolCallsDoneEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) });
resetStreamToolCallState(streamToolCallIDs, streamToolNames);
continue;
@@ -315,6 +328,46 @@ function toBool(v) {
return v === true;
}
function upstreamEmptyOutputDetail(contentFilter, _text, thinking) {
if (contentFilter) {
return {
status: 400,
message: 'Upstream content filtered the response and returned no output.',
code: 'content_filter',
};
}
if (thinking !== '') {
return {
status: 429,
message: 'Upstream account hit a rate limit and returned reasoning without visible output.',
code: 'upstream_empty_output',
};
}
return {
status: 429,
message: 'Upstream account hit a rate limit and returned empty output.',
code: 'upstream_empty_output',
};
}
function sendFailedChunk(res, status, message, code) {
res.write(`data: ${JSON.stringify({
status_code: status,
error: {
message,
type: openAIErrorType(status),
code,
param: null,
},
})}\n\n`);
if (!res.writableEnded && !res.destroyed) {
res.write('data: [DONE]\n\n');
}
if (typeof res.flush === 'function') {
res.flush();
}
}
module.exports = {
handleVercelStream,
};