From 8a7e96ef6cd49a27ec022fb6f6cf74d4a90c2a0a Mon Sep 17 00:00:00 2001 From: glatinone <93207632+glatinone@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:47:16 +0800 Subject: [PATCH] fix(proxy): add --no-ccr-response-handling CLI flag and env var (fixes #3082) Signed-off-by: glatinone <93207632+glatinone@users.noreply.github.com> --- headroom/cli/proxy.py | 17 ++++++++ tests/test_cli_proxy_env.py | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/headroom/cli/proxy.py b/headroom/cli/proxy.py index bbdba0fed..94fc7837a 100644 --- a/headroom/cli/proxy.py +++ b/headroom/cli/proxy.py @@ -418,6 +418,18 @@ def dashboard(port: int, no_open: bool) -> None: "Env: HEADROOM_NO_CCR_PROACTIVE_EXPANSION." ), ) +@click.option( + "--no-ccr-response-handling", + is_flag=True, + envvar="HEADROOM_NO_CCR_RESPONSE_HANDLING", + help=( + "Disable server-side buffering of streaming responses for CCR response " + "handling, independent of --no-ccr. A client that offers the " + "headroom_retrieve tool itself (e.g. the bundled OpenCode plugin) " + "otherwise always takes the buffered-stream path with no opt-out " + "(issue #3082). Env: HEADROOM_NO_CCR_RESPONSE_HANDLING." + ), +) @click.option( "--proxy-extension", "proxy_extension", @@ -1033,6 +1045,7 @@ def proxy( lossless: bool, ccr_inline_resolve: bool, no_ccr_proactive_expansion: bool, + no_ccr_response_handling: bool, proxy_extension: tuple[str, ...], compressor: tuple[str, ...], no_subscription_tracking: bool, @@ -1339,6 +1352,10 @@ def proxy( ccr_resolve_markers_inline=ccr_inline_resolve, lossless=lossless, ccr_proactive_expansion=not no_ccr_proactive_expansion, + # Independent of --no-ccr: a client can offer headroom_retrieve itself + # (see the --no-ccr-response-handling help text), which unconditionally + # takes the buffered-stream path with no other opt-out (issue #3082). + ccr_handle_responses=not no_ccr_response_handling, # Flatten repeat-flag tuple AND any comma-separated values inside it. # `--proxy-extension a,b --proxy-extension c` and `HEADROOM_PROXY_EXTENSIONS=a,b,c` # both yield ["a", "b", "c"]. None when nothing was supplied. diff --git a/tests/test_cli_proxy_env.py b/tests/test_cli_proxy_env.py index 4dcd5fbb9..ae7985758 100644 --- a/tests/test_cli_proxy_env.py +++ b/tests/test_cli_proxy_env.py @@ -960,6 +960,89 @@ class TestCLICompressionOnlyFlags: assert cfg.ccr_inject_tool is False +class TestCLINoCcrResponseHandlingFlag: + """--no-ccr-response-handling / HEADROOM_NO_CCR_RESPONSE_HANDLING must flip + ccr_handle_responses, independent of --no-ccr (issue #3082). + + Before this flag existed, ccr_handle_responses had no CLI/env wiring at + all — a client that offers the headroom_retrieve tool itself (e.g. the + bundled OpenCode plugin) always took the buffered-stream path with no + supported opt-out, even with --no-ccr set. + """ + + def test_ccr_handle_responses_defaults_on(self, runner): + """Without the flag, ccr_handle_responses stays enabled (no behavior change).""" + captured_config = {} + + def mock_run_server(config, **kwargs): + captured_config["config"] = config + + with patch("headroom.proxy.server.run_server", mock_run_server): + result = runner.invoke(main, ["proxy"], catch_exceptions=False) + + assert result.exit_code == 0, result.output + assert captured_config["config"].ccr_handle_responses is True + + def test_no_ccr_response_handling_flag(self, runner): + """--no-ccr-response-handling disables ccr_handle_responses only.""" + captured_config = {} + + def mock_run_server(config, **kwargs): + captured_config["config"] = config + + with patch("headroom.proxy.server.run_server", mock_run_server): + result = runner.invoke( + main, ["proxy", "--no-ccr-response-handling"], catch_exceptions=False + ) + + assert result.exit_code == 0, result.output + cfg = captured_config["config"] + assert cfg.ccr_handle_responses is False + # Unrelated CCR knobs stay on: this flag is independent of --no-ccr. + assert cfg.ccr_inject_tool is True + assert cfg.ccr_inject_marker is True + + def test_no_ccr_response_handling_from_env(self, runner): + """HEADROOM_NO_CCR_RESPONSE_HANDLING env var disables ccr_handle_responses.""" + captured_config = {} + + def mock_run_server(config, **kwargs): + captured_config["config"] = config + + with patch("headroom.proxy.server.run_server", mock_run_server): + result = runner.invoke( + main, + ["proxy"], + env={"HEADROOM_NO_CCR_RESPONSE_HANDLING": "1"}, + catch_exceptions=False, + ) + + assert result.exit_code == 0, result.output + assert captured_config["config"].ccr_handle_responses is False + + def test_no_ccr_response_handling_combines_with_no_ccr(self, runner): + """--no-ccr-response-handling composes with --no-ccr instead of being + implied by it: both can be set together for a fully unbuffered, + no-retrieval-path configuration.""" + captured_config = {} + + def mock_run_server(config, **kwargs): + captured_config["config"] = config + + with patch("headroom.proxy.server.run_server", mock_run_server): + result = runner.invoke( + main, + ["proxy", "--no-ccr", "--no-ccr-response-handling"], + catch_exceptions=False, + ) + + assert result.exit_code == 0, result.output + cfg = captured_config["config"] + assert cfg.ccr_inject_tool is False + assert cfg.ccr_inject_marker is False + assert cfg.ccr_handle_responses is False + + class TestNoCcrMarkerCompressors: """Verify --no-ccr actually suppresses <> markers from every compressor, not just SmartCrusher (#1022)."""