Files
OmniRoute/open-sse/executors/deepseek-web-done-terminator.ts
Jon Bailey 91efacadd3 fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791)
* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(deepseek): extract done-terminator helper to keep frozen file under cap

Extracts the FINISHED-drain scheduler and finish-once guard added for
the [DONE] terminator fix (#6777) into a new
deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under
its frozen line cap (1148). Behavior is unchanged.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-10 18:37:05 -03:00

69 lines
2.2 KiB
TypeScript

// ── DeepSeek Web SSE "done terminator" helpers ──────────────────────────
//
// Extracted from deepseek-web.ts (frozen line-count) so the drain/guard
// state machine used to close the OpenAI-compatible SSE after DeepSeek's
// `response/status=FINISHED` event can grow without touching the frozen
// file. See #6777: upstreams that leave the HTTP body open hang OpenAI SDK
// clients that wait for `data: [DONE]` after `finish_reason: stop`.
/** How long to wait after DeepSeek `response/status=FINISHED` for trailing
* search_results before closing the OpenAI-compatible SSE. */
export const DEEPSEEK_FINISHED_DRAIN_MS = 750;
/** Wraps a stream-finishing callback so it runs at most once and never
* throws past a controller that the client already cancelled/closed. */
export function createFinishOnceGuard(finish: () => void): {
finishOnce: () => void;
hasFinished: () => boolean;
} {
let streamFinished = false;
return {
finishOnce: () => {
if (streamFinished) return;
streamFinished = true;
try {
finish();
} catch {
// Controller may already be closed if the client cancelled.
}
},
hasFinished: () => streamFinished,
};
}
/** Schedules `finishStream` after a short drain window following
* `response/status=FINISHED`, so late `search_results` payloads still get
* captured, while guaranteeing the stream always closes even if the
* upstream body stays open past that window. */
export function createFinishedDrainScheduler(
finishStream: () => void,
drainMs: number = DEEPSEEK_FINISHED_DRAIN_MS
): {
scheduleFinishAfterDrain: () => void;
clearFinishedDrain: () => void;
isDrainPending: () => boolean;
} {
let finishedDrainTimer: ReturnType<typeof setTimeout> | null = null;
const clearFinishedDrain = () => {
if (finishedDrainTimer) {
clearTimeout(finishedDrainTimer);
finishedDrainTimer = null;
}
};
const scheduleFinishAfterDrain = () => {
clearFinishedDrain();
finishedDrainTimer = setTimeout(() => {
finishedDrainTimer = null;
finishStream();
}, drainMs);
};
return {
scheduleFinishAfterDrain,
clearFinishedDrain,
isDrainPending: () => finishedDrainTimer !== null,
};
}