Conversation
Some native model backends (llama.cpp, gpt4all) write log lines directly to file descriptor 1, bypassing sys.stdout and contextlib.redirect_stdout. Expose a reusable context manager that redirects fd 1 to stderr so plugins can keep that noise off stdout. Assisted-by: OpenAI Codex (GPT-5)
7c0bc28 to
8f305ab
Compare
|
I’ve corrected the description to clarify that this is only the shared-helper part of #196 and to credit @vanshtaneja23’s fd-level diagnosis and proposed approach. I should have included that credit when I opened the PR. If you were still planning to implement it yourself, I’m happy to step back. |
|
Thanks for the credit, and no need to step back — this is your PR, please carry it. One thing worth fixing before it lands, though: the redirect needs a flush on either side of the
Repro against this branch: import os, sys
from llm.utils import redirect_stdout_to_stderr
print("MODEL-OUTPUT-LINE-1") # still in Python's buffer
with redirect_stdout_to_stderr():
sys.stdout.flush() # anything that drains the buffer in here
os.write(1, b"NATIVE-NOISE\n")
print("MODEL-OUTPUT-LINE-2")$ python repro.py > out.txt 2> err.txt
$ cat out.txt
MODEL-OUTPUT-LINE-2
$ cat err.txt
MODEL-OUTPUT-LINE-1
NATIVE-NOISE
Adding a flush in two places fixes it: stdout_fd = stdout.fileno()
stderr_fd = stderr.fileno()
stdout.flush() # drain the buffer while fd 1 is still stdout
saved_stdout_fd = os.dup(stdout_fd)
try:
os.dup2(stderr_fd, stdout_fd)
yield
finally:
stdout.flush() # and again before fd 1 points back
os.dup2(saved_stdout_fd, stdout_fd)
os.close(saved_stdout_fd)With that, both model lines land in stdout and only the native noise goes to stderr. One smaller note for the docstring: |
Flush buffered Python output before redirecting file descriptor 1 and before restoring it so output cannot cross stream boundaries. Document the process-wide scope and cover buffered output in a subprocess regression test. Assisted-by: OpenAI Codex (GPT-5)
|
Thanks for catching this. Fixed in The helper now flushes stdout before redirecting fd 1 and again before restoring it. The restoration and descriptor close use nested I also replaced the fd-only test with a captured subprocess regression. It verifies that model output before and after the context remains on stdout, while buffered Python output and native
|
Remove PYTHONUNBUFFERED from the child process environment so the regression test always exercises block-buffered stdout, regardless of the parent test environment. Assisted-by: OpenAI Codex (GPT-5)
|
Thanks for the credit, and no need to step back — this is your PR, please carry it. One thing worth fixing before it lands, though: the redirect needs a flush on either side of the
Repro against this branch: import os, sys
from llm.utils import redirect_stdout_to_stderr
print("MODEL-OUTPUT-LINE-1") # still in Python's buffer
with redirect_stdout_to_stderr():
sys.stdout.flush() # anything that drains the buffer in here
os.write(1, b"NATIVE-NOISE\n")
print("MODEL-OUTPUT-LINE-2")$ python repro.py > out.txt 2> err.txt
$ cat out.txt
MODEL-OUTPUT-LINE-2
$ cat err.txt
MODEL-OUTPUT-LINE-1
NATIVE-NOISE
Adding a flush in two places fixes it: stdout_fd = stdout.fileno()
stderr_fd = stderr.fileno()
stdout.flush() # drain the buffer while fd 1 is still stdout
saved_stdout_fd = os.dup(stdout_fd)
try:
os.dup2(stderr_fd, stdout_fd)
yield
finally:
stdout.flush() # and again before fd 1 points back
os.dup2(saved_stdout_fd, stdout_fd)
os.close(saved_stdout_fd)With that, both model lines land in stdout and only the native noise goes to stderr. One smaller note for the docstring: |
Summary
Related to #196.
This implements the shared-helper option proposed by @vanshtaneja23 in their issue comment: a file-descriptor-level
redirect_stdout_to_stderr()context manager inllm.utils.The helper flushes buffered Python stdout before file descriptor 1 is redirected and again before it is restored. This prevents model output buffered outside the context from crossing stream boundaries. The docstring also notes that changing file descriptor 1 is process-wide and affects every thread while the context is active.
The regression test runs the helper in a subprocess with captured streams. It verifies that model output written before and after the context stays on stdout, while buffered Python output and a native
os.write()inside the context go to stderr. The child process explicitly uses normal buffered output even if the parent test environment setsPYTHONUNBUFFERED.This PR does not change
llm-llama-cpporllm-gpt4all, so it does not by itself fix #196. Those plugins would still need to wrap their native calls, or use library logging callbacks, before their diagnostics stop leaking to stdout.Verification
uv run pytest tests/test_utils.py -q:92 passedPYTHONUNBUFFERED=1 uv run pytest tests/test_utils.py::test_redirect_stdout_to_stderr -q:1 passeduv run pytest -q:1125 passeduv run ruff check llm/utils.py tests/test_utils.py: passeduv run black --check llm/utils.py tests/test_utils.py: passedgit diff --check: passedPrepared with OpenAI Codex (GPT-5) assistance.