diff --git a/gateway/run.py b/gateway/run.py index 9e4dc7c377ba..26cf8859ac42 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -13671,6 +13671,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if _show_reasoning_effective and response and not _intentional_silence: last_reasoning = agent_result.get("last_reasoning") if last_reasoning: + from gateway.stream_consumer import escape_code_fences_for_display # Collapse long reasoning to keep messages readable lines = last_reasoning.strip().splitlines() if len(lines) > 15: @@ -13702,6 +13703,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew ) response = f"> 💭 **Reasoning:**\n{_quoted}\n\n{response}" else: + # Escape ``` inside reasoning so inner fences don't + # break the outer code block used to render it. + display_reasoning = escape_code_fences_for_display(display_reasoning) response = f"💭 **Reasoning:**\n```\n{display_reasoning}\n```\n\n{response}" # Runtime-metadata footer — only on the FINAL message of the turn. diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index a269aa8198d3..dc4f5f956ac7 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -61,6 +61,24 @@ _COMMENTARY = object() _FLUSH = object() +def escape_code_fences_for_display(text: str) -> str: + """Escape triple-backtick markers so text can be safely wrapped + inside an outer ``` code block without breaking the fence. + + When reasoning content contains ``` (e.g. the model quotes code + in its thinking), wrapping it in an outer ``` for display causes + the inner fence to break the outer block. Solution: replace each + `` ``` `` with `` \\`\\`\\` `` before wrapping. + + Returns: + The input text with each `` ``` `` replaced by `` \\`\\`\\` ``, + or the input unchanged if no triple-backticks are present. + """ + if not isinstance(text, str) or "```" not in text: + return text + return text.replace("```", "\\`\\`\\`") + + @dataclass class StreamConsumerConfig: """Runtime config for a single stream consumer instance.""" diff --git a/tests/gateway/test_escape_reasoning_fences.py b/tests/gateway/test_escape_reasoning_fences.py new file mode 100644 index 000000000000..3a54ea09dcc3 --- /dev/null +++ b/tests/gateway/test_escape_reasoning_fences.py @@ -0,0 +1,45 @@ +""" +Tests for escape_code_fences_for_display. + +B1: Escape triple-backtick markers inside reasoning text before wrapping + in an outer ``` fence, so inner ``` doesn't break the outer block. +""" + +import pytest +from gateway.stream_consumer import escape_code_fences_for_display + + +class TestEscapeCodeFencesForDisplay: + """escape_code_fences_for_display prevents inner ``` from breaking + the outer code block used to render reasoning.""" + + def test_no_fence_passthrough(self): + text = "plain reasoning text" + assert escape_code_fences_for_display(text) == text + + def test_single_fence_escaped(self): + text = "model used ```python\nx = 1\n``` in its thinking" + result = escape_code_fences_for_display(text) + assert "```" not in result + assert "\\`\\`\\`" in result + + def test_multiple_fences_all_escaped(self): + text = "```\nblock1\n``` and ```python\nblock2\n```" + result = escape_code_fences_for_display(text) + assert result.count("```") == 0 + assert result.count("\\`\\`\\`") == 4 + + def test_empty_string(self): + assert escape_code_fences_for_display("") == "" + + def test_none_returns_none(self): + assert escape_code_fences_for_display(None) is None + + def test_integration_with_outer_fence(self): + """Simulates the gateway's reasoning wrapping logic.""" + raw = "thinking about:\n```python\nprint('hi')\n```\nok" + escaped = escape_code_fences_for_display(raw) + wrapped = f"💭 **Reasoning:**\n```\n{escaped}\n```\n\nHere's the answer." + # The outer ``` should not be broken by inner ``` + assert wrapped.count("```") == 2 # only outer open + close + assert "\\`\\`\\`" in wrapped