Bug
OpenAiCompatibleBackend::post_chat (crates/genie-core/src/llm/openai_compatible.rs) sets request.timeout(...) only on the non-streaming branch. For streaming it only adds Accept: text/event-stream, then awaits request.send() with no deadline.
The reqwest::Client is built with connect_timeout only. Once TCP is up, send().await waits forever for response headers. Per-chunk timeouts.read in chat_stream_once only applies after headers arrive.
A backend that accepts the connection but stalls before the first response byte hangs the stream task. POST /api/chat/stream holds the single-slot ChatTurnGate, so other chat requests stay busy until process restart.
The sibling raw-TCP local client (openai_compat.rs) already bounds the status-line wait; this reqwest optional-API path does not.
Expected
Waiting for streaming response headers should hit timeouts.read (or equivalent) and fail closed.
Actual
send().await parks until the peer closes or headers arrive — no idle deadline.
Suggested fix
Wrap streaming request.send() in tokio::time::timeout(self.timeouts.read, ...) (do not put a reqwest whole-request timeout on the stream — that would kill long SSE bodies). Map elapsed to a clear timeout error.
Bug
OpenAiCompatibleBackend::post_chat(crates/genie-core/src/llm/openai_compatible.rs) setsrequest.timeout(...)only on the non-streaming branch. For streaming it only addsAccept: text/event-stream, then awaitsrequest.send()with no deadline.The
reqwest::Clientis built withconnect_timeoutonly. Once TCP is up,send().awaitwaits forever for response headers. Per-chunktimeouts.readinchat_stream_onceonly applies after headers arrive.A backend that accepts the connection but stalls before the first response byte hangs the stream task.
POST /api/chat/streamholds the single-slotChatTurnGate, so other chat requests stay busy until process restart.The sibling raw-TCP local client (
openai_compat.rs) already bounds the status-line wait; this reqwest optional-API path does not.Expected
Waiting for streaming response headers should hit
timeouts.read(or equivalent) and fail closed.Actual
send().awaitparks until the peer closes or headers arrive — no idle deadline.Suggested fix
Wrap streaming
request.send()intokio::time::timeout(self.timeouts.read, ...)(do not put a reqwest whole-request timeout on the stream — that would kill long SSE bodies). Map elapsed to a clear timeout error.