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:
# 1. QWEN PARSER (Instruct & Coder Style)
# 1. QWEN PARSER (Instruct & Coder Style)
class QwenParser(BaseParser):
def __init__(self, tools: Dict[str, Any] = None):
super().__init__(tools)
self.reasoning_content = ""
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
# 1. IMMEDIATE REPETITION GUARD
......@@ -129,8 +133,32 @@ class QwenParser(BaseParser):
results = []
# 2. Pre-cleaning (Think tags, system markers, and special tokens)
clean_text = re.sub(r'<\|.*?\|>|<(?:thought|think)>.*?((?:</(?:thought|think)>)|$)', '', text, flags=re.DOTALL | re.IGNORECASE)
# 2. Pre-cleaning (Extract thinking/reasoning content, then remove for tool parsing)
# 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
# Matches <tool>, <tool_call>, or even just { "name": ... } if tags are missing
......@@ -480,7 +508,7 @@ class OpenAIFormatter:
self.model_name = model_name
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)"""
if LITELLM_AVAILABLE and all([ModelResponse, Choices, Message, Usage]):
try:
......@@ -508,6 +536,8 @@ class OpenAIFormatter:
"role": "assistant",
"content": text if not tool_calls else None,
}
if reasoning:
message["reasoning"] = reasoning
if tool_calls:
message["tool_calls"] = tool_calls
......
......@@ -3501,6 +3501,51 @@ model_manager = ModelManager()
# Global args for access in endpoints
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:
"""
......@@ -5692,6 +5737,15 @@ async def stream_chat_response(
print(f"--- END ESCAPED CONTENT ---")
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)
if tools:
# Convert tools back to Tool objects for parsing
......@@ -5854,9 +5908,14 @@ async def generate_chat_response(
response_message = {
"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"
# Check for tool calls
......@@ -6437,6 +6496,13 @@ def parse_args():
default=None,
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",
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