feat(reasoning): add inline think tag extraction and Anthropic thinking_blocks support

Add extract_think() and emit_incremental_think() helpers to extract thinking content from inline <think> and <thought> tags in the content field. This handles models served via Ollama, self-hosted vLLM, or other compatible endpoints that embed reasoning as inline tags instead of using the dedicated reasoning_content API field.

Also adds Anthropic thinking_blocks support for extended thinking via the thinking content blocks array.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Flinn Xie
2026-05-12 23:02:59 +08:00
co-authored by Sisyphus
parent 3a27af0018
commit 3a851f8f8d
5 changed files with 283 additions and 4 deletions
+82 -1
View File
@@ -1,4 +1,4 @@
from nanobot.utils.helpers import strip_think
from nanobot.utils.helpers import extract_think, strip_think
class TestStripThinkTag:
@@ -144,3 +144,84 @@ class TestStripThinkConservativePreserve:
def test_literal_channel_marker_in_code_block_preserved(self):
text = "Example:\n```\nif line.startswith('<channel|>'):\n skip()\n```"
assert strip_think(text) == text
class TestExtractThink:
def test_no_think_tags(self):
thinking, clean = extract_think("Hello World")
assert thinking is None
assert clean == "Hello World"
def test_single_think_block(self):
text = "Hello <think>reasoning content\nhere</think> World"
thinking, clean = extract_think(text)
assert thinking == "reasoning content\nhere"
assert clean == "Hello World"
def test_single_thought_block(self):
text = "Hello <thought>reasoning content</thought> World"
thinking, clean = extract_think(text)
assert thinking == "reasoning content"
assert clean == "Hello World"
def test_multiple_think_blocks(self):
text = "A<think>first</think>B<thought>second</thought>C"
thinking, clean = extract_think(text)
assert thinking == "first\n\nsecond"
assert clean == "ABC"
def test_think_only_no_content(self):
text = "<think>just thinking</think>"
thinking, clean = extract_think(text)
assert thinking == "just thinking"
assert clean == ""
def test_unclosed_think_not_extracted(self):
# Unclosed blocks at start are stripped but NOT extracted
text = "<think>unclosed thinking..."
thinking, clean = extract_think(text)
assert thinking is None
assert clean == ""
def test_empty_think_block(self):
text = "Hello <think></think> World"
thinking, clean = extract_think(text)
# Empty blocks result in empty string after strip
assert thinking == ""
assert clean == "Hello World"
def test_think_with_whitespace_only(self):
text = "Hello <think> \n World"
thinking, clean = extract_think(text)
assert thinking is None
assert clean == "Hello <think> \n World"
def test_mixed_think_and_thought(self):
text = "Start<think>first reasoning</think>middle<thought>second reasoning</thought>End"
thinking, clean = extract_think(text)
assert thinking == "first reasoning\n\nsecond reasoning"
assert clean == "StartmiddleEnd"
def test_real_world_ollama_response(self):
text = """<think>
The user is asking about Python list comprehensions.
Let me explain the syntax and give examples.
</think>
List comprehensions in Python provide a concise way to create lists. Here's the syntax:
```python
[expression for item in iterable if condition]
```
For example:
```python
squares = [x**2 for x in range(10)]
```"""
thinking, clean = extract_think(text)
assert "list comprehensions" in thinking.lower()
assert "Let me explain" in thinking
assert "List comprehensions in Python" in clean
assert "<think>" not in clean
assert "</think>" not in clean