fix: normalize Gemma 4 thought-channel output (#2224)

This commit is contained in:
RaresKeY
2026-06-04 20:26:58 +03:00
committed by GitHub
parent 7ce6ec7f50
commit c12c2aa233
7 changed files with 249 additions and 41 deletions
+42 -1
View File
@@ -1,5 +1,5 @@
import pytest
from routes.chat_helpers import needs_auto_name
from routes.chat_helpers import clean_thinking_for_save, needs_auto_name
@pytest.mark.parametrize("name,expected", [
@@ -27,3 +27,44 @@ from routes.chat_helpers import needs_auto_name
])
def test_needs_auto_name(name, expected):
assert needs_auto_name(name) == expected, f"needs_auto_name({name!r}) should be {expected}"
def test_clean_thinking_for_save_extracts_gemma4_thought_channel():
content, metadata = clean_thinking_for_save(
"<|channel>thought\ninternal reasoning<channel|>Final answer.",
{"model": "google/gemma-4-31B-it"},
)
assert content == "Final answer."
assert metadata["thinking"] == "internal reasoning"
assert metadata["model"] == "google/gemma-4-31B-it"
def test_clean_thinking_for_save_strips_empty_gemma4_thought_channel():
content, metadata = clean_thinking_for_save(
"<|channel>thought\n<channel|>Final answer.",
{"model": "google/gemma-4-31B-it"},
)
assert content == "Final answer."
assert "thinking" not in metadata
def test_clean_thinking_for_save_unwraps_gemma4_response_channel():
content, metadata = clean_thinking_for_save(
"<|channel>thought\ninternal reasoning<channel|><|channel>response\nFinal answer.<channel|>",
{"model": "google/gemma-4-31B-it"},
)
assert content == "Final answer."
assert metadata["thinking"] == "internal reasoning"
def test_clean_thinking_for_save_extracts_thought_tag():
content, metadata = clean_thinking_for_save(
"<thought>internal reasoning</thought>Final answer.",
{},
)
assert content == "Final answer."
assert metadata["thinking"] == "internal reasoning"
+51 -3
View File
@@ -18,7 +18,7 @@ def node_available():
pytest.skip("node binary not on PATH")
def _run_markdown_case(markdown: str) -> str:
def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)"):
script = textwrap.dedent(
r"""
import fs from 'node:fs';
@@ -54,9 +54,9 @@ def _run_markdown_case(markdown: str) -> str:
const moduleUrl = 'data:text/javascript;base64,' + Buffer.from(source).toString('base64');
const mod = await import(moduleUrl);
const input = JSON.parse(process.argv[1]);
console.log(JSON.stringify({ html: mod.mdToHtml(input) }));
console.log(JSON.stringify({ html: __RENDER_EXPR__ }));
"""
)
).replace("__RENDER_EXPR__", render_expr)
result = subprocess.run(
["node", "--input-type=module", "-e", script, json.dumps(markdown)],
cwd=_REPO,
@@ -99,3 +99,51 @@ def test_table_separator_row_not_rendered_as_data(node_available):
assert "<th" in html
assert "<td" in html
assert "---" not in html
def test_process_with_thinking_handles_gemma4_thought_channel(node_available):
html = _run_markdown_case(
"<|channel>thought\ninternal reasoning<channel|>Final answer.",
"mod.processWithThinking(input)",
)
assert "thinking-section" in html
assert "internal reasoning" in html
assert "Final answer." in html
assert "&lt;|channel&gt;" not in html
assert "<|channel>" not in html
def test_process_with_thinking_strips_empty_gemma4_thought_channel(node_available):
html = _run_markdown_case(
"<|channel>thought\n<channel|>Final answer.",
"mod.processWithThinking(input)",
)
assert "thinking-section" not in html
assert "Final answer." in html
assert "&lt;|channel&gt;" not in html
assert "<|channel>" not in html
def test_process_with_thinking_unwraps_gemma4_response_channel(node_available):
html = _run_markdown_case(
"<|channel>thought\ninternal reasoning<channel|><|channel>response\nFinal answer.<channel|>",
"mod.processWithThinking(input)",
)
assert "thinking-section" in html
assert "internal reasoning" in html
assert "Final answer." in html
assert "&lt;|channel&gt;" not in html
assert "<|channel>" not in html
def test_extract_thinking_blocks_handles_thought_tag(node_available):
result = _run_markdown_case(
"<thought>internal reasoning</thought>Final answer.",
"mod.extractThinkingBlocks(input)",
)
assert result["thinkingBlocks"] == ["internal reasoning"]
assert result["content"] == "Final answer."
+19
View File
@@ -23,3 +23,22 @@ def test_strip_think_cases():
# 6. Multiple blocks (closed + unclosed)
assert strip_think("Hello! <think> closed </think> Here is the answer. <think> unclosed") == "Hello! Here is the answer."
def test_strip_think_handles_thought_tags():
assert strip_think("<thought>internal reasoning</thought>Final answer.") == "Final answer."
def test_strip_think_handles_gemma4_thought_channel():
text = "<|channel>thought\ninternal reasoning<channel|>Final answer."
assert strip_think(text) == "Final answer."
def test_strip_think_handles_empty_gemma4_thought_channel():
text = "<|channel>thought\n<channel|>Final answer."
assert strip_think(text) == "Final answer."
def test_strip_think_unwraps_gemma4_response_channel():
text = "<|channel>thought\ninternal reasoning<channel|><|channel>response\nFinal answer.<channel|>"
assert strip_think(text) == "Final answer."