Commit 1a6467ca authored by Your Name's avatar Your Name

Add reasoning/thinking extraction and forced reasoning support

- Add --force-reasoning CLI flag to force thinking mode for models like qwen3 coder
- Add check_force_reasoning() function to determine if reasoning should be forced
- Modify QwenParser to extract thinking/reasoning content instead of stripping it
- Add reasoning field to response message in non-streaming chat completions
- Prepend reasoning content to generated text in streaming responses
- Update OpenAIFormatter to include reasoning in response when available
parent b1e402b9
...@@ -117,6 +117,10 @@ class BaseParser: ...@@ -117,6 +117,10 @@ class BaseParser:
# 1. QWEN PARSER (Instruct & Coder Style) # 1. QWEN PARSER (Instruct & Coder Style)
# 1. QWEN PARSER (Instruct & Coder Style) # 1. QWEN PARSER (Instruct & Coder Style)
class QwenParser(BaseParser): class QwenParser(BaseParser):
def __init__(self, tools: Dict[str, Any] = None):
super().__init__(tools)
self.reasoning_content = ""
@validate_tool_output @validate_tool_output
def parse(self, text: str) -> List[Dict]: def parse(self, text: str) -> List[Dict]:
# 1. IMMEDIATE REPETITION GUARD # 1. IMMEDIATE REPETITION GUARD
...@@ -129,8 +133,32 @@ class QwenParser(BaseParser): ...@@ -129,8 +133,32 @@ class QwenParser(BaseParser):
results = [] results = []
# 2. Pre-cleaning (Think tags, system markers, and special tokens) # 2. Pre-cleaning (Extract thinking/reasoning content, then remove for tool parsing)
clean_text = re.sub(r'<\|.*?\|>|<(?:thought|think)>.*?((?:</(?:thought|think)>)|$)', '', text, flags=re.DOTALL | re.IGNORECASE) # Extract thinking content before removing it
# Enhanced pattern to capture various thinking tag formats
thinking_pattern = r'<\|.*?\|>|<(?:thought|think)>.*?((?:</(?:thought|think)>)|$)|<\|begin.*?\|><\|end.*?\|>'
thinking_matches = re.findall(thinking_pattern, text, flags=re.DOTALL | re.IGNORECASE)
if thinking_matches:
# Extract the actual thinking content from matches
thinking_content_parts = []
for match in thinking_matches:
if isinstance(match, tuple):
# Handle tuple matches from regex groups
if match[0]: # First group contains the content
thinking_content_parts.append(match[0])
elif len(match) > 1 and match[1]: # Second group might contain content
thinking_content_parts.append(match[1])
else:
# Direct string match
thinking_content_parts.append(match)
# Join all thinking matches with newlines, filtering out empty strings
self.reasoning_content = '\n'.join([part.strip() for part in thinking_content_parts if part and part.strip()])
else:
self.reasoning_content = ""
# Remove thinking tags from text for tool call parsing
clean_text = re.sub(thinking_pattern, '', text, flags=re.DOTALL | re.IGNORECASE)
# 3. FLEXIBLE TAG MATCHING # 3. FLEXIBLE TAG MATCHING
# Matches <tool>, <tool_call>, or even just { "name": ... } if tags are missing # Matches <tool>, <tool_call>, or even just { "name": ... } if tags are missing
...@@ -480,7 +508,7 @@ class OpenAIFormatter: ...@@ -480,7 +508,7 @@ class OpenAIFormatter:
self.model_name = model_name self.model_name = model_name
self.id = f"chatcmpl-{uuid.uuid4()}" self.id = f"chatcmpl-{uuid.uuid4()}"
def format_full(self, text, prompt_tokens, completion_tokens, tool_calls=None): def format_full(self, text, prompt_tokens, completion_tokens, tool_calls=None, reasoning=None):
"""Standard Response (Non-Streaming)""" """Standard Response (Non-Streaming)"""
if LITELLM_AVAILABLE and all([ModelResponse, Choices, Message, Usage]): if LITELLM_AVAILABLE and all([ModelResponse, Choices, Message, Usage]):
try: try:
...@@ -508,6 +536,8 @@ class OpenAIFormatter: ...@@ -508,6 +536,8 @@ class OpenAIFormatter:
"role": "assistant", "role": "assistant",
"content": text if not tool_calls else None, "content": text if not tool_calls else None,
} }
if reasoning:
message["reasoning"] = reasoning
if tool_calls: if tool_calls:
message["tool_calls"] = tool_calls message["tool_calls"] = tool_calls
......
...@@ -3501,6 +3501,51 @@ model_manager = ModelManager() ...@@ -3501,6 +3501,51 @@ model_manager = ModelManager()
# Global args for access in endpoints # Global args for access in endpoints
global_args = None global_args = None
# Global force reasoning list - models to force thinking mode for
global_force_reasoning = []
def check_force_reasoning(model_name: str = None) -> bool:
"""
Check if thinking/reasoning should be forced for a specific model.
Args:
model_name: The model name to check (optional - uses global_args if not provided)
Returns:
True if reasoning should be forced for this model
Usage:
# From CLI: --force-reasoning qwen3 qwq deepseek-r1
# Will match if model name contains any of these strings (case-insensitive)
"""
global global_force_reasoning, global_args
# Get force_reasoning list from args if not already stored globally
if not global_force_reasoning and global_args:
global_force_reasoning = getattr(global_args, 'force_reasoning', []) or []
if not global_force_reasoning:
return False
# If model_name not provided, try to get from global_args
if model_name is None and global_args:
model_name = getattr(global_args, 'model', None)
if isinstance(model_name, list) and model_name:
model_name = model_name[0]
if not model_name:
return False
model_name_lower = model_name.lower()
# Check if any force_reasoning pattern matches the model name
for pattern in global_force_reasoning:
if pattern.lower() in model_name_lower:
return True
return False
def check_reply_filter(filter_type: str, model_type: str = "text", model_name: str = None) -> bool: def check_reply_filter(filter_type: str, model_type: str = "text", model_name: str = None) -> bool:
""" """
...@@ -5674,23 +5719,32 @@ async def stream_chat_response( ...@@ -5674,23 +5719,32 @@ async def stream_chat_response(
# Explicitly flush to ensure data is sent immediately # Explicitly flush to ensure data is sent immediately
await asyncio.sleep(0) await asyncio.sleep(0)
print(f"DEBUG: stream_chat_response completed, {chunk_count} chunks, generated_text length: {len(generated_text)}") print(f"DEBUG: stream_chat_response completed, {chunk_count} chunks, generated_text length: {len(generated_text)}")
if not generated_text.strip(): if not generated_text.strip():
print(f"DEBUG: Warning - no content generated!") print(f"DEBUG: Warning - no content generated!")
# In debug mode, dump the full generated text # In debug mode, dump the full generated text
if global_debug: if global_debug:
print(f"\n{'='*80}") print(f"\n{'='*80}")
print(f"=== FULL GENERATED TEXT (DEBUG) ===") print(f"=== FULL GENERATED TEXT (DEBUG) ===")
print(f"{'='*80}") print(f"{'='*80}")
# Show both raw (actual) content and escaped representation # Show both raw (actual) content and escaped representation
print(f"--- RAW CONTENT (actual newlines shown as lines) ---") print(f"--- RAW CONTENT (actual newlines shown as lines) ---")
print(generated_text) print(generated_text)
print(f"--- END RAW CONTENT ---") print(f"--- END RAW CONTENT ---")
print(f"--- ESCAPED CONTENT (repr() - shows \\n for newlines) ---") print(f"--- ESCAPED CONTENT (repr() - shows \\n for newlines) ---")
print(repr(generated_text)) print(repr(generated_text))
print(f"--- END ESCAPED CONTENT ---") print(f"--- END ESCAPED CONTENT ---")
print(f"{'='*80}\n") print(f"{'='*80}\n")
# Extract reasoning content if using QwenParser and reasoning is forced
reasoning_content = ""
if check_force_reasoning(model_name) and hasattr(tool_parser, 'reasoning_content'):
reasoning_content = tool_parser.reasoning_content
# Prepend reasoning content to generated text if we have reasoning content
if reasoning_content:
generated_text = reasoning_content + "\n\n" + generated_text
print(f"DEBUG: Prepended reasoning content (length: {len(reasoning_content)}) to generated text")
# Check for tool calls in complete output (for API response format) # Check for tool calls in complete output (for API response format)
if tools: if tools:
...@@ -5854,9 +5908,14 @@ async def generate_chat_response( ...@@ -5854,9 +5908,14 @@ async def generate_chat_response(
response_message = { response_message = {
"role": "assistant", "role": "assistant",
"content": generated_text,
} }
# Add reasoning content if available
if check_force_reasoning(model_name) and hasattr(tool_parser, 'reasoning_content'):
reasoning_content = tool_parser.reasoning_content
if reasoning_content:
response_message["reasoning"] = reasoning_content
finish_reason = "stop" finish_reason = "stop"
# Check for tool calls # Check for tool calls
...@@ -6437,6 +6496,13 @@ def parse_args(): ...@@ -6437,6 +6496,13 @@ def parse_args():
default=None, default=None,
help="Path to store generated files (images, audio). If specified, files will be saved here and served over web.", help="Path to store generated files (images, audio). If specified, files will be saved here and served over web.",
) )
parser.add_argument(
"--force-reasoning",
type=str,
nargs="+",
default=[],
help="Force thinking/reasoning mode for specific models. Usage: --force-reasoning qwen3 qwq deepseek-r1"
)
parser.add_argument( parser.add_argument(
"--parser", "--parser",
type=str, type=str,
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment