Skip to content

Commit ea67459

Browse files
committed
Add top_k parameter and smart reasoning extraction
- Add top_k sampling parameter (0=disabled, 20-40 recommended for thinking models) - Implement auto-detect reasoning extraction supporting multiple tag formats: - <think>/<thinking>/<reasoning>/<reason> tags (DeepSeek, Qwen3, QwQ, GLM) - GPT-OSS analysis channel format - Add reasoning_mode dropdown: Auto-detect, Disabled, Custom tags - Add custom_open_tag/custom_close_tag inputs for custom reasoning formats - Update README with reasoning extraction documentation
1 parent 008a4b0 commit ea67459

2 files changed

Lines changed: 133 additions & 27 deletions

File tree

LMStudio.py

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Provides text generation using local LLM/VLM models via LM Studio server.
44
"""
55
import logging
6+
import re
67
from typing import Optional, Tuple, List
78
from tempfile import NamedTemporaryFile
89
import numpy as np
@@ -58,11 +59,36 @@
5859
"Ultra (1536px)": 1536,
5960
}
6061

62+
# Reasoning extraction modes
63+
REASONING_MODE_OPTIONS = [
64+
"Auto-detect (recommended)",
65+
"Disabled",
66+
"Custom tags",
67+
]
68+
69+
# Common reasoning tag patterns used by different models
70+
# Order matters - most common first for efficiency
71+
COMMON_REASONING_PATTERNS = [
72+
# DeepSeek R1, Qwen3, QwQ, GLM-4/Z1 - most common
73+
(r"<think>(.*?)</think>", "<think>", "</think>"),
74+
# Alternative spelling
75+
(r"<thinking>(.*?)</thinking>", "<thinking>", "</thinking>"),
76+
# Some models use this
77+
(r"<reasoning>(.*?)</reasoning>", "<reasoning>", "</reasoning>"),
78+
# Occasionally seen
79+
(r"<reason>(.*?)</reason>", "<reason>", "</reason>"),
80+
# GPT-OSS style (simplified pattern for the analysis channel)
81+
(r"<\|start\|>assistant<\|channel\|>analysis<\|message\|>(.*?)<\|end\|>",
82+
"<|start|>assistant<|channel|>analysis<|message|>", "<|end|>"),
83+
]
84+
6185

6286
class YANCLMStudio:
6387
"""
6488
LM Studio integration node for ComfyUI.
6589
Queries local LM Studio server for text generation with LLM/VLM models.
90+
91+
Note: model.respond() automatically applies the model's chat template.
6692
"""
6793

6894
CATEGORY = "YANC/LMStudio"
@@ -153,7 +179,14 @@ def INPUT_TYPES(cls):
153179
"min": 0.0,
154180
"max": 1.0,
155181
"step": 0.05,
156-
"tooltip": "Nucleus sampling threshold. Lower values (0.1-0.9) = more focused. 1.0 = disabled."
182+
"tooltip": "Nucleus sampling: only consider tokens with cumulative probability >= top_p. Lower = more focused. 1.0 = disabled."
183+
}),
184+
"top_k": ("INT", {
185+
"default": 0,
186+
"min": 0,
187+
"max": 500,
188+
"step": 1,
189+
"tooltip": "Top-K sampling: only consider the K most likely tokens. Lower = more focused. 0 = disabled. Recommended: 20-40 for thinking models."
157190
}),
158191
"repeat_penalty": ("FLOAT", {
159192
"default": 1.0,
@@ -162,11 +195,20 @@ def INPUT_TYPES(cls):
162195
"step": 0.05,
163196
"tooltip": "Penalizes repeated tokens. Higher values (1.1-1.3) reduce repetition. 1.0 = disabled."
164197
}),
165-
# --- Extraction and management ---
166-
"reasoning_tag": ("STRING", {
198+
# --- Reasoning extraction ---
199+
"reasoning_mode": (REASONING_MODE_OPTIONS, {
200+
"default": "Auto-detect (recommended)",
201+
"tooltip": "How to extract reasoning/thinking from model output. Auto-detect works with DeepSeek, Qwen, QwQ, GLM, GPT-OSS and similar models."
202+
}),
203+
"custom_open_tag": ("STRING", {
167204
"default": "<think>",
168-
"tooltip": "Opening tag to identify reasoning sections (e.g., '<think>' for DeepSeek R1). Reasoning is extracted to separate output."
205+
"tooltip": "Custom opening tag for reasoning extraction. Only used when reasoning_mode is 'Custom tags'."
169206
}),
207+
"custom_close_tag": ("STRING", {
208+
"default": "</think>",
209+
"tooltip": "Custom closing tag for reasoning extraction. Only used when reasoning_mode is 'Custom tags'."
210+
}),
211+
# --- Management ---
170212
"unload_llm": ("BOOLEAN", {
171213
"default": True,
172214
"tooltip": "Unload the LLM from LM Studio after generation. Recommended to free VRAM for image generation."
@@ -285,48 +327,64 @@ def _convert_image_to_pil(self, image_tensor, resize_option: str = "No Resize")
285327
logger.error(f"Failed to convert image: {e}")
286328
return None
287329

288-
def _extract_reasoning(self, text: str, opening_tag: str) -> Tuple[str, str]:
330+
def _extract_reasoning_auto(self, text: str) -> Tuple[str, str, Optional[str]]:
331+
"""
332+
Auto-detect and extract reasoning using common patterns.
333+
334+
Args:
335+
text: Full response text
336+
337+
Returns:
338+
Tuple of (response_without_reasoning, reasoning_content, detected_pattern)
339+
detected_pattern is None if no pattern matched
340+
"""
341+
for pattern, open_tag, close_tag in COMMON_REASONING_PATTERNS:
342+
# Use DOTALL to match across newlines
343+
matches = list(re.finditer(pattern, text, re.DOTALL))
344+
if matches:
345+
reasoning_parts = [m.group(1) for m in matches]
346+
# Remove all matched reasoning blocks from text
347+
clean_text = re.sub(pattern, "", text, flags=re.DOTALL)
348+
return clean_text.strip(), "\n---\n".join(reasoning_parts).strip(), open_tag
349+
350+
# No pattern matched
351+
return text, "", None
352+
353+
def _extract_reasoning_custom(self, text: str, open_tag: str, close_tag: str) -> Tuple[str, str]:
289354
"""
290-
Extract reasoning content from response.
355+
Extract reasoning using custom tags.
291356
292357
Args:
293358
text: Full response text
294-
opening_tag: Opening tag like "<think>"
359+
open_tag: Opening tag
360+
close_tag: Closing tag
295361
296362
Returns:
297363
Tuple of (response_without_reasoning, reasoning_content)
298364
"""
299-
if not opening_tag or opening_tag not in text:
365+
if not open_tag or open_tag not in text:
300366
return text, ""
301367

302-
# Derive closing tag
303-
if opening_tag.startswith("<") and opening_tag.endswith(">"):
304-
tag_name = opening_tag[1:-1]
305-
closing_tag = f"</{tag_name}>"
306-
else:
307-
# Non-XML style tag - just use same tag as closer
308-
closing_tag = opening_tag
309-
310368
reasoning_parts = []
311369
response_text = text
312370

313371
# Extract all reasoning blocks
314-
while opening_tag in response_text:
315-
start_idx = response_text.find(opening_tag)
316-
end_idx = response_text.find(closing_tag, start_idx)
372+
while open_tag in response_text:
373+
start_idx = response_text.find(open_tag)
374+
end_idx = response_text.find(close_tag, start_idx + len(open_tag))
317375

318376
if end_idx == -1:
319377
# No closing tag - take rest as reasoning
320-
reasoning_parts.append(response_text[start_idx + len(opening_tag):])
378+
reasoning_parts.append(response_text[start_idx + len(open_tag):])
321379
response_text = response_text[:start_idx]
322380
break
323381

324382
# Extract reasoning content
325-
reasoning_content = response_text[start_idx + len(opening_tag):end_idx]
383+
reasoning_content = response_text[start_idx + len(open_tag):end_idx]
326384
reasoning_parts.append(reasoning_content)
327385

328386
# Remove from response
329-
response_text = response_text[:start_idx] + response_text[end_idx + len(closing_tag):]
387+
response_text = response_text[:start_idx] + response_text[end_idx + len(close_tag):]
330388

331389
return response_text.strip(), "\n---\n".join(reasoning_parts).strip()
332390

@@ -347,8 +405,11 @@ def generate(
347405
draft_model_selection: str = CUSTOM_MODEL_OPTION,
348406
custom_draft_model: str = "",
349407
top_p: float = 1.0,
408+
top_k: int = 0,
350409
repeat_penalty: float = 1.0,
351-
reasoning_tag: str = "<think>",
410+
reasoning_mode: str = "Auto-detect (recommended)",
411+
custom_open_tag: str = "<think>",
412+
custom_close_tag: str = "</think>",
352413
unload_llm: bool = True,
353414
unload_comfy_models: bool = False,
354415
refresh_models: bool = False
@@ -475,28 +536,48 @@ def generate(
475536
}
476537

477538
# Add optional parameters if not at default
478-
# Note: Parameter names per LM Studio SDK docs (topPSampling, not topP)
539+
# Note: Parameter names per LM Studio SDK docs
479540
if top_p < 1.0:
480541
gen_config["topPSampling"] = top_p
542+
if top_k > 0:
543+
gen_config["topKSampling"] = top_k
481544
if repeat_penalty != 1.0:
482545
gen_config["repeatPenalty"] = repeat_penalty
483546
# Note: seed is not a valid inference-time parameter in LM Studio SDK
484547
if draft_model:
485548
gen_config["draftModel"] = draft_model
486549

487550
troubleshooting_lines.append(f"[INFO] Config: maxTokens={max_tokens}, temp={temperature}")
551+
if top_k > 0:
552+
troubleshooting_lines.append(f"[INFO] Sampling: top_k={top_k}, top_p={top_p}")
488553
troubleshooting_lines.append("[INFO] Generating...")
489554

490555
# Generate response
491556
response = model.respond(chat, config=gen_config)
492557
response_text = str(response)
493558

494559
troubleshooting_lines.append("[INFO] Generation complete")
560+
troubleshooting_lines.append(f"[INFO] Raw response length: {len(response_text)} chars")
561+
562+
# Extract reasoning based on mode
563+
final_response = response_text
564+
reasoning = ""
565+
566+
if reasoning_mode == "Auto-detect (recommended)":
567+
final_response, reasoning, detected_pattern = self._extract_reasoning_auto(response_text)
568+
if detected_pattern:
569+
troubleshooting_lines.append(f"[INFO] Auto-detected reasoning format: {detected_pattern}")
570+
elif response_text != final_response:
571+
troubleshooting_lines.append("[INFO] Reasoning extracted")
572+
elif reasoning_mode == "Custom tags":
573+
final_response, reasoning = self._extract_reasoning_custom(
574+
response_text, custom_open_tag, custom_close_tag
575+
)
576+
# else: "Disabled" - no extraction
495577

496-
# Extract reasoning if tag specified
497-
final_response, reasoning = self._extract_reasoning(response_text, reasoning_tag)
498578
if reasoning:
499-
troubleshooting_lines.append(f"[INFO] Extracted reasoning ({len(reasoning)} chars)")
579+
troubleshooting_lines.append(f"[INFO] Extracted reasoning: {len(reasoning)} chars")
580+
troubleshooting_lines.append(f"[INFO] Clean response: {len(final_response)} chars")
500581

501582
# Unload LLM if requested
502583
if unload_llm:

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ The node supports up to 4 image inputs for vision-language models:
5656

5757
**Note:** Not all VLMs support multiple images. If you get errors with multiple images, try using only `image1`.
5858

59+
## Reasoning Extraction
60+
61+
Many reasoning models (DeepSeek R1, Qwen3, QwQ, GLM-Z1) wrap their "thinking" process in special tags. The node can extract this separately from the final response.
62+
63+
### Modes
64+
65+
- **Auto-detect (recommended)**: Automatically detects common reasoning patterns:
66+
- `<think>...</think>` - DeepSeek R1, Qwen3, QwQ, GLM-Z1
67+
- `<thinking>...</thinking>` - Alternative format
68+
- `<reasoning>...</reasoning>` - Some models
69+
- GPT-OSS analysis channel format
70+
71+
- **Disabled**: Returns the full response as-is (no extraction)
72+
73+
- **Custom tags**: Specify your own open/close tags for models with unique formats
74+
75+
### Output
76+
77+
- **response**: Final answer with reasoning tags removed (if extracted)
78+
- **reasoning**: Extracted thinking/reasoning content
79+
80+
This allows you to route reasoning to a separate display or log while keeping the final response clean.
81+
5982
## Custom Server Address
6083

6184
Default: `http://127.0.0.1:1234`
@@ -86,7 +109,9 @@ This file survives git updates.
86109
|-----------|---------|-------------|
87110
| image_resize | Medium (768px) | Resize images before VLM processing |
88111
| top_p | 1.0 | Nucleus sampling (lower=more focused) |
112+
| top_k | 0 | Limits vocabulary (0=disabled, 20-40 recommended for thinking models) |
89113
| repeat_penalty | 1.0 | Reduce repetition (1.1-1.3 recommended) |
114+
| reasoning_mode | Auto-detect | How to extract reasoning from response |
90115
| unload_llm | True | Unload LLM after generation (recommended) |
91116

92117
### Understanding max_tokens vs Context Length

0 commit comments

Comments
 (0)