[Bug] llm-deepseek: SSE keep-alives re-arm the idle watchdog forever — a 300s idle timeout actually waited 17.7 min, and a chatty provider can hang a turn unboundedly #6959
Replies: 1 comment
|
A plugin now covers the two bounds this asks for, mountable without touching the adapter:
dsh plugin add @argszero/cordis-plugin-stream-ceilingIt hooks the public
When a bound elapses the call ends with one terminal Mapping to your three expectations:
- set:
- id: stream-ceiling
config:
payloadGraceMs: 60000Two things it deliberately does not do, so the adapter-side fix stays on the table:
One caveat worth knowing when choosing Verified with 18 tests ( |
Uh oh!
There was an error while loading. Please reload this page.
Summary
llm-deepseek(chat-completions protocol) can receive a complete assistantmessage — reasoning and tool-call blocks included — yet never receive a terminal
event from the provider. The connection stays open and keeps emitting SSE
comment lines. Because
onActivity→watchdog.pulse()re-arms the idle timeron every comment line, the configured
streamIdleTimeoutMsdoes not boundthe wait.
Two distinct consequences:
DeepSeek stream idle timeout after 300000ms, but the measured wait was10.0 / 17.3 / 17.7 minutes across three attempts.
keep-alives without ever terminating can hold one attempt open indefinitely;
no configuration value bounds it, in either direction.
Environment
@deepseek-ai/dsh@0.1.5-rc.1deepseek-official, modeldeepseek-v4-flashbaseURL: https://api.deepseek.com/beta, protocol left at its default (chat-completions)reasoningEffort: max,maxTokens: 256000,streamIdleTimeoutMs: 300000cacheReadTokens≈ 180k)Observed stream, in order
The trailing
finishis a local synthesis — its reason carries the timeouterror — not a provider event. In the same session, every successful request
instead ends with
finish {kind: "tool-calls"}or{kind: "stop"}at0.00 min after the last content record.
Three consecutive attempts on the same step, all
TIMEOUT, all with thepayload fully delivered before the hang:
retry 1/5— 10.02 min of silence after the last content recordretry 2/5— 17.34 minretry 1/5(next step) — 17.70 minUser-visible effect: a turn that produced no output for ~59 minutes, then failed.
The retry delay itself was only 475–999 ms, so essentially all of the wait was
the hang.
Why
streamIdleTimeoutMsdoes not bound itpackages/util/timeout/src/index.ts:packages/llm/llm-deepseek/src/protocols/chat-completions/adapter.ts:211passes() => { watchdog.pulse() }asonActivity, andsse.tsdocuments thatcallback as "optional transport-activity callback; comments never enter the
yielded payload stream".
So every SSE comment line resets the timer.
streamIdleTimeoutMsis therefore akeep-alive-silence timeout, not a ceiling on how long an attempt may hang:
Related
llm-pi-ai/openai-responses): final answer item complete,response.completedmissing,hangs until the idle timeout. This report adds an independent reproduction on
llm-deepseek/ chat-completions, where the reasoning text and the toolcalls are both complete.
bodyTimeout/headersTimeoutkilling asilent prefill before the first byte). Not this.
Expected behavior
separate
streamMaxDurationMs), reported with the value actually elapsed.only the configured threshold.
as [Bug] OpenAI Responses final item completes but missing response.completed hangs until idle timeout #6594 proposes (
terminalEventGraceMs).Raw session transcript (zstd jsonl) and the extraction script are available on
request.
All reactions