Skip to content

Commit 35be03b

Browse files
sanil-23claude
andcommitted
fix(inference): break on [DONE] so a lingering socket doesn't trip the watchdog
CodeRabbit review on #4393: after the terminal [DONE] sentinel the loop re-armed the idle watchdog, so a provider that sends [DONE] but holds the socket open would fail an already-complete response as a retryable stall. Break on [DONE]; add a regression that sends [DONE] then keeps the connection open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 98bc84f commit 35be03b

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

src/openhuman/inference/provider/compatible_stream_native.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,13 @@ impl OpenAiCompatibleProvider {
326326
};
327327
let data = data.trim();
328328
if data == "[DONE]" {
329-
continue;
329+
// `[DONE]` is the terminal SSE sentinel — the response is
330+
// complete. Stop reading immediately rather than looping
331+
// back to another watchdog-armed read: a provider that
332+
// sends `[DONE]` but lingers the socket would otherwise
333+
// trip the inactivity watchdog and fail a finished
334+
// response as a retryable stall (#4269 watchdog review).
335+
break 'stream;
330336
}
331337

332338
let chunk: StreamChunkResponse = match serde_json::from_str(data) {

src/openhuman/inference/provider/compatible_tests.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,3 +4818,34 @@ async fn stream_watchdog_tolerates_dropped_delta_receiver() {
48184818
.expect("a dropped delta receiver must not fail the stream");
48194819
assert_eq!(resp.text.as_deref(), Some("xy"));
48204820
}
4821+
4822+
#[tokio::test]
4823+
async fn stream_stops_on_done_even_if_socket_lingers() {
4824+
// A provider that sends `[DONE]` then holds the socket open must finalize the
4825+
// (complete) response immediately — the watchdog must NOT re-arm and fail it
4826+
// as a stall. Regression for the watchdog + terminal-sentinel interaction
4827+
// (CodeRabbit review, PR #4393). With the pre-fix `continue`, this stream
4828+
// would trip the 300ms idle watchdog instead of completing.
4829+
let script = vec![
4830+
(
4831+
std::time::Duration::from_millis(0),
4832+
"data: {\"choices\":[{\"delta\":{\"content\":\"done-content\"}}]}\n\n",
4833+
),
4834+
(std::time::Duration::from_millis(0), "data: [DONE]\n\n"),
4835+
];
4836+
// close_after = false: hold the socket open for 30s AFTER [DONE].
4837+
let url = spawn_scripted_sse(script, false, std::time::Duration::from_secs(30)).await;
4838+
let provider = OpenAiCompatibleProvider::new("donetest", &url, None, AuthStyle::None)
4839+
.with_stream_idle_timeout(std::time::Duration::from_millis(300));
4840+
let request = watchdog_request();
4841+
let (delta_tx, _delta_rx) = tokio::sync::mpsc::channel(8);
4842+
// Must return well under the 30s socket hold (and without a stall error).
4843+
let resp = tokio::time::timeout(
4844+
std::time::Duration::from_secs(5),
4845+
provider.stream_native_chat(None, &request, &delta_tx, 0),
4846+
)
4847+
.await
4848+
.expect("must complete on [DONE], not wait for the socket to close")
4849+
.expect("[DONE] must finalize the response, not trip the watchdog");
4850+
assert_eq!(resp.text.as_deref(), Some("done-content"));
4851+
}

0 commit comments

Comments
 (0)