Skip to content

fix(agent): stream LLM completions to avoid gateway idle-timeout - #236

Open
sebastianbraun25 wants to merge 3 commits into
VectifyAI:mainfrom
sebastianbraun25:fix/issue-235-stream-llm-completions
Open

fix(agent): stream LLM completions to avoid gateway idle-timeout#236
sebastianbraun25 wants to merge 3 commits into
VectifyAI:mainfrom
sebastianbraun25:fix/issue-235-stream-llm-completions

Conversation

@sebastianbraun25

@sebastianbraun25 sebastianbraun25 commented Aug 28, 2026

Copy link
Copy Markdown

Note

This PR was created in collaboration between a human and AI: implementation, tests, and
PR text were created by an AI assistant under the guidance and review of the human author.

Problem

openkb's wiki compiler (openkb/agent/compiler.py) calls litellm.completion() /
litellm.acompletion() in buffered (non-streaming) mode. On a long-running compile
step (large documents, many concepts, verbose models), some corporate LLM gateways
sitting in front of the actual provider enforce an idle timeout on buffered
requests: if no bytes are sent back to the client for N seconds while the gateway
waits for the upstream provider to finish generating the full response, the
gateway kills the connection with a Gateway Timeout, even though the underlying
provider call would have eventually succeeded.

This was observed against a corporate AI.proxy gateway (OpenAI-compatible,
deployed at AWS): any step whose completion took longer than the gateway's idle
window failed with a timeout purely because of buffering, not because the model
was slow to start responding.

Root Cause

_llm_call() and _llm_call_async() in openkb/agent/compiler.py call
litellm.completion() / litellm.acompletion() without stream=True. A buffered
request produces zero bytes on the wire until the entire response is ready, so an
idle-timeout gateway watching for connection activity can't distinguish "still
generating" from "connection died" and kills it.

Solution / Changes

  • openkb/agent/compiler.py:
    • _llm_call() and _llm_call_async() now call litellm.completion() /
      litellm.acompletion() with stream=True, so bytes keep flowing over the
      connection as tokens arrive.
    • New _merge_stream_chunks() helper merges the streamed chunks back into the
      same non-streaming response shape the rest of the compiler already expects
      (response.choices[0].message.content, response.usage,
      response.choices[0].finish_reason), using LiteLLM's own
      litellm.stream_chunk_builder() for genuine multi-chunk streams.
    • An exception raised mid-stream (e.g. a dropped connection) propagates as a
      complete failure — list(stream) never returns a partial buffer, matching
      the prior all-or-nothing behavior of a failed litellm.completion() call.
    • This is a straight swap to streaming-only; there is no stream=True/False
      config toggle.
  • tests/test_compiler.py: _mock_completion()/_mock_acompletion() (and the
    handful of inline mocks that built their own fake response) now return a
    single-chunk fake stream ([mock_resp]) instead of a bare response object,
    matching the new stream=True call signature.
  • tests/test_llm_timeout.py: same adjustment for its litellm.completion/
    acompletion mocks.

Backward compatible: no config/CLI surface changes, callers of _llm_call/
_llm_call_async see the same return type and response shape as before.

Issues

Resolves #235.

Sebastian Braun and others added 3 commits August 28, 2026 17:23
Corporate LLM gateways (e.g. AI.proxy on AWS) enforce an idle timeout on
buffered (non-streaming) requests, so a long-running compile step can hit
a Gateway Timeout even though the provider would have eventually finished.

Switch _llm_call() and _llm_call_async() in openkb/agent/compiler.py to
litellm.completion()/acompletion() with stream=True: streaming keeps
bytes flowing over the connection, so idle-timeout gateways never see a
silent connection. Chunks are merged back into the existing response
shape via a new _merge_stream_chunks() helper, using LiteLLM's own
litellm.stream_chunk_builder() for genuine multi-chunk streams. An
exception raised mid-stream propagates as a complete failure (list()
never returns a partial buffer), matching prior all-or-nothing behavior.

Adapts the compiler test mocks (_mock_completion/_mock_acompletion and a
handful of inline mocks) to return a single-chunk fake stream, plus the
litellm.completion/acompletion mocks in test_llm_timeout.py.

Resolves VectifyAI#235.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add per-chunk debug logging around streamed LiteLLM completion consumption in `openkb.agent.compiler`.

This diagnostic instrumentation is enabled via the existing `openkb -v` flag and helps narrow the still-observed ~60s production cutoff to either a no-first-byte case (for example slow time-to-first-token) or a proxy/gateway path that buffers or drops streamed bytes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per-chunk DEBUG logging (_log_chunk_timing) drowned out the rest of
a log on a long response — hundreds of LLM stream chunk [...] #N
lines for a single LLM call, e.g. one concepts-plan request logging
205 individual chunk lines.

Replaces it with exactly two log lines per LLM call:
- _log_stream_start: logged once, when the first chunk arrives
  (time-to-first-token).
- _log_stream_end: logged once, when the stream finishes cleanly
  (total chunk count + elapsed time for the last chunk).
- _log_stream_interrupted: logged once instead of _log_stream_end
  if the stream raises mid-iteration — reports how many chunks were
  successfully received and when, right before the exception is
  re-raised (still a complete failure, no partial buffer). Special-cases
  zero chunks (failure before any byte arrived) with dedicated wording
  instead of an inapplicable chunk number.

Updated tests/test_compiler.py::TestLLMStreamTimingDebugLogging to
match: asserts exactly one start + one end/interrupted line, and the
explicit absence of the old per-chunk lines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant