Skip to content

ollama-proxy: make response-header timeout configurable - #52

Open
pettersandvand wants to merge 5 commits into
NVIDIA:developfrom
pettersandvand:configurable-proxy-response-timeout
Open

pettersandvand wants to merge 5 commits into
NVIDIA:developfrom
pettersandvand:configurable-proxy-response-timeout

Conversation

@pettersandvand

Copy link
Copy Markdown

Summary

proxyResponseTimeout (services/ollama-proxy/proxy.go) is a hardcoded 120s constant used as the ResponseHeaderTimeout on the transport that forwards real client requests (e.g. /v1/chat/completions) to a chosen backend node. There's no flag or env var to change it.

In practice this means any backend response that legitimately takes longer than 120s to start — a cold model load on a large local model, a long prefill, or an engine (e.g. Ollama) that only flushes output once a tool-call has fully composed — fails with:

net/http: timeout awaiting response headers

even though the backend is healthy and would have answered given more time. This reproduces reliably with larger local models (e.g. 27B+) doing tool-calling generations.

Change

  • Added a --response-timeout duration flag to services/ollama-proxy/main.go (default 120s, matching current behavior exactly — no change unless explicitly set).
  • Threaded the value through Proxy via a new SetResponseTimeout method, applied to both the plain local forwarding transport and the per-peer cluster-mTLS transport (newProxyTransport), since both were previously governed by the same hardcoded constant.
  • Left the fast model-list probe client (modelListClient, fixed at proxyDialTimeout = 10s) untouched — it's intentionally a quick health/listing probe, not the request-forwarding path.
  • Updated the README flags table.
  • Added tests: default timeout applied, custom override applied to the transport, non-positive values ignored.

Testing

  • go build ./... — clean
  • go vet ./... — clean
  • go test ./... — all tests pass except one pre-existing, unrelated failure (a loopback-alias test requiring a 127.0.0.2 bind, which fails identically on unmodified main in this sandbox — confirmed via git stash).

Notes

Happy to adjust the flag name/shape (e.g. accept it in ms instead of a Go duration string, or expose it per-engine rather than globally) if maintainers have a preferred convention.

proxyResponseTimeout was a hardcoded 120s constant governing how long
the proxy waits for a backend engine (Ollama/LM Studio) to start
sending a response for a real forwarded request (e.g.
/v1/chat/completions). There was no flag or env var to change it, so
any backend response that takes longer than 120s to start (a cold
model load, a long prefill, or a tool-calling generation that an
engine only flushes once complete) fails with "net/http: timeout
awaiting response headers", even though the backend is healthy and
would have answered.

Add a --response-timeout duration flag (default 120s, matching prior
behavior exactly) and thread it through both the plain local transport
and the per-peer cluster-mTLS transport, since both were governed by
the same constant. Existing default behavior is unchanged unless the
flag is set.

Adds tests covering the default, a custom override applied to the
transport, and rejection of non-positive values. Updates the README
flags table.

Signed-off-by: crantz <petter.johan@live.no>
@pettersandvand

Copy link
Copy Markdown
Author

Related: #25 — same underlying pattern (a hardcoded response-header timeout with no config surface), but a different service/code path. #25 is about nvpair-engine-manager's internal action dispatcher (executor.go's engine-name-gated 30s/10m client selection, used for install/pull/local action calls). This PR is about ollama-proxy's separate proxyResponseTimeout constant, which governs the client-facing forwarding path (e.g. /v1/chat/completions) and is what actually causes real user-facing request failures on longer-running generations. Both stem from the same design gap (fixed timeouts, no way to override), so flagging the connection in case a maintainer wants a unified approach across both services.

pettersandvand and others added 4 commits September 9, 2026 08:49
The previous commit added ollama-proxy's --response-timeout flag, but
nothing passed it: the broker's spawnProxy() built ollama-proxy's args
itself and never included it, so a packaged install (broker spawned by
the desktop app, which has no knowledge of this flag either) saw no
behavior change at all.

Add --proxy-response-timeout to nvpair-ui-broker (default 5m, up from
ollama-proxy's own 120s default) and have spawnProxy() pass it through
as ollama-proxy's --response-timeout. This means the effective default
for anyone running through the broker (packaged app included, since
the desktop app doesn't need to change anything to benefit) becomes 5m
instead of 120s, while still being overridable via the new broker flag
for anyone who wants a different value.

Signed-off-by: crantz <petter.johan@live.no>
--response-timeout 0 now explicitly means "no timeout", matching
http.Transport's own ResponseHeaderTimeout semantics, rather than
being silently ignored. Some backends (a cold load on a very large
model, a long prefill on a large context, or an engine that only
flushes once a tool call finishes composing) can legitimately exceed
any fixed bound; this makes "wait indefinitely" an explicit, opt-in
choice rather than something inferred from an arbitrarily large
duration. Negative values remain invalid and are still ignored.
--proxy-response-timeout 0 on the broker threads the same choice
through to the packaged app.

This is a real tradeoff, not a free upgrade: with no timeout, a
backend that hangs outright (rather than eventually erroring) will
hang the forwarded request indefinitely too, with no automatic
failover to another candidate. Documented in both flags' help text.

To make that tradeoff visible either way, wrap each candidate's
transport in a loggingRoundTripper that logs "still awaiting response
headers" every 30s while a request is in flight. This fires whether or
not a timeout is configured, so a long wait shows up as periodic
progress in the logs instead of silence that looks identical to a
hang — most useful exactly when the timeout is raised or disabled,
since nothing else reports liveness until the request finally
succeeds or fails.

Signed-off-by: crantz <petter.johan@live.no>
Matches idleClientWriteTimeout's existing rationale in this file: a var
instead of a const only so a test can shorten it and observe a log
line without waiting 30 real seconds. Production never reassigns it.

Verified end-to-end against a genuinely slow/silent fake backend
(httptest server that writes zero bytes for N seconds before
answering, mirroring what Ollama actually does during tool-call
composition): a short --response-timeout still fails fast as before,
and --response-timeout 0 waits out the silence, delivers the real
response, and logs "still awaiting response headers" progress while
waiting.

Signed-off-by: crantz <petter.johan@live.no>
Adds a "Proxy response timeout" control to Service Settings, mirroring the
existing Log level setting's persistence pattern: a ui-config.json field
(getProxyResponseTimeoutMinutes/setProxyResponseTimeoutMinutes), a
service:get/set-proxy-response-timeout IPC pair, and a supervisor field
seeded by the connector before spawn. Unlike log level this has no live
JSON-RPC fan-out, so it applies on the next service restart, matching the
apply-by-restart pattern already used for engine port changes.

Stored as minutes (default 5, matching the broker's own hardcoded default)
and rendered to the broker's --proxy-response-timeout flag as a Go duration
string (e.g. "5m"). The UI explains that 0 means waiting indefinitely with
no automatic failover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sherief-nv

Copy link
Copy Markdown
Collaborator

Thanks - timeout units in minutes as a granularity seem too coarse, can we make them seconds?

Moving MR against development branch.

@sherief-nv
sherief-nv changed the base branch from main to develop September 10, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants