Summary
A large non-streaming request to a reasoning model fails with HTTP 504 Gateway Timeout at ~600 seconds. The user sees a blank result and no error.
stream: False is hardcoded at providers/openai_compatible.py:589. A reasoning model must generate its whole response, reasoning tokens included, before any bytes go out. At ~100k tokens of input that takes longer than the edge proxy's idle timeout, so the proxy cuts the connection.
I am not asking you to switch to streaming. #134 asked for the opposite, and I would rather hand you the evidence than a patch that breaks someone else.
Environment
- Installed via
uvx --from git+https://github.com/BeehiveInnovations/pal-mcp-server.git
- Windows 11, Python 3.13
- Custom provider (
CUSTOM_API_URL) against an OpenAI-compatible endpoint behind Cloudflare
- A reasoning model that defaults to maximum thinking effort and emits
reasoning_content
What happens
codereview inlines file contents into one prompt and makes a single buffered request. From logs/mcp_server.log:
22:51:42 POST /chat/completions (Expert analysis embedding: 6 files, 407,176 characters)
23:01:43 HTTP/1.1 504 Gateway Timeout 601 seconds
Server: cloudflare, CF-RAY: ..., Retry-After: 120
23:01:44 openai._base_client Retrying due to status code 504
Fifteen 504s across three runs, all between 600 and 602 seconds.
Why it reads as a hang
Three things stack up:
- The OpenAI SDK retries twice on its own (
max_retries=2, not overridden). Each retry re-uploads the 420KB prompt and spends another 600s. Those retries log at DEBUG.
_run_with_retries wraps that with 4 more attempts, so an exception can take about two hours to surface.
- Nothing is logged between sending the request and getting a response.
The MCP client gives up long before those loops finish, so TOOL_COMPLETED never fires and the user gets nothing back. No error, no partial output, no log line pointing anywhere. I found the 504s by reading the whole log; they had been sitting there since the first failure.
Reproduction
- Configure a custom OpenAI-compatible provider backed by a reasoning model.
- Call
codereview with relevant_files totalling ~100k tokens. Mine was 5-6 files, ~420,000 characters.
- No
TOOL_COMPLETED, blank result. logs/mcp_server.log shows the 504 at ~601s and the SDK retries after it.
Same endpoint and payload, tested outside PAL:
| request |
result |
stream: false, ~113k tokens |
HTTP 504 at ~601s |
stream: true, ~113k tokens |
first byte 12.5s, done 450s, finish_reason: stop |
Streaming reached first byte in 4.6 to 12.5 seconds whatever the payload size. Every byte resets the proxy's idle timer.
The limit is time, not context
113k tokens against a 1M window. Nothing overflowed. The only question is how long a buffered body takes to build.
Suggested resolution
Streaming fixes it, but not as a global default. #134 ("O3 streaming=False for non-validated organizations") is the case that breaks: O3 through OpenRouter needs a verified organisation to stream at all.
An opt-in per provider or per model covers both:
- a
supports_streaming capability in the model config (custom_models.json and friends), defaulting off, or
- an env flag like
CUSTOM_STREAM=1 scoped to the custom provider
The streaming path is small: set stream: True, add stream_options: {"include_usage": True} so usage still arrives in the final chunk, accumulate delta.content, and take finish_reason from the last chunk that carries one. I run that locally as a patch and it has held up.
Two smaller findings, happy to split them out:
providers/openai_compatible.py:643 returns content unchecked, and tools/workflow/workflow_mixin.py:1502 treats "" as falsy. A reasoning model can return HTTP 200 with content: "" and finish_reason: "length" when reasoning eats the output budget. That path produces a silent blank instead of an error or a retry.
- One log line either side of the API call turns this from a day of log archaeology into a glance.
Where the opt-in belongs is your call. I have the reproduction if you want more detail on any part of it.
Summary
A large non-streaming request to a reasoning model fails with HTTP 504 Gateway Timeout at ~600 seconds. The user sees a blank result and no error.
stream: Falseis hardcoded atproviders/openai_compatible.py:589. A reasoning model must generate its whole response, reasoning tokens included, before any bytes go out. At ~100k tokens of input that takes longer than the edge proxy's idle timeout, so the proxy cuts the connection.I am not asking you to switch to streaming. #134 asked for the opposite, and I would rather hand you the evidence than a patch that breaks someone else.
Environment
uvx --from git+https://github.com/BeehiveInnovations/pal-mcp-server.gitCUSTOM_API_URL) against an OpenAI-compatible endpoint behind Cloudflarereasoning_contentWhat happens
codereviewinlines file contents into one prompt and makes a single buffered request. Fromlogs/mcp_server.log:Fifteen 504s across three runs, all between 600 and 602 seconds.
Why it reads as a hang
Three things stack up:
max_retries=2, not overridden). Each retry re-uploads the 420KB prompt and spends another 600s. Those retries log at DEBUG._run_with_retrieswraps that with 4 more attempts, so an exception can take about two hours to surface.The MCP client gives up long before those loops finish, so
TOOL_COMPLETEDnever fires and the user gets nothing back. No error, no partial output, no log line pointing anywhere. I found the 504s by reading the whole log; they had been sitting there since the first failure.Reproduction
codereviewwithrelevant_filestotalling ~100k tokens. Mine was 5-6 files, ~420,000 characters.TOOL_COMPLETED, blank result.logs/mcp_server.logshows the 504 at ~601s and the SDK retries after it.Same endpoint and payload, tested outside PAL:
stream: false, ~113k tokensstream: true, ~113k tokensfinish_reason: stopStreaming reached first byte in 4.6 to 12.5 seconds whatever the payload size. Every byte resets the proxy's idle timer.
The limit is time, not context
113k tokens against a 1M window. Nothing overflowed. The only question is how long a buffered body takes to build.
Suggested resolution
Streaming fixes it, but not as a global default. #134 ("O3 streaming=False for non-validated organizations") is the case that breaks: O3 through OpenRouter needs a verified organisation to stream at all.
An opt-in per provider or per model covers both:
supports_streamingcapability in the model config (custom_models.jsonand friends), defaulting off, orCUSTOM_STREAM=1scoped to the custom providerThe streaming path is small: set
stream: True, addstream_options: {"include_usage": True}so usage still arrives in the final chunk, accumulatedelta.content, and takefinish_reasonfrom the last chunk that carries one. I run that locally as a patch and it has held up.Two smaller findings, happy to split them out:
providers/openai_compatible.py:643returnscontentunchecked, andtools/workflow/workflow_mixin.py:1502treats""as falsy. A reasoning model can return HTTP 200 withcontent: ""andfinish_reason: "length"when reasoning eats the output budget. That path produces a silent blank instead of an error or a retry.Where the opt-in belongs is your call. I have the reproduction if you want more detail on any part of it.