Skip to content

Commit fd0cfe9

Browse files
bmolodanclaude
andcommitted
feat: disable reasoning/thinking mode by default for hybrid-thinking models
Hybrid-thinking models such as Qwen3 emit <think>...</think> preambles that corrupt tool-call output and make agentic runs fail. This turns thinking off by default by injecting chat_template_kwargs.enable_thinking=false (plus a top-level enable_thinking for DashScope-style servers) via extra_body on both the agentic and single-shot LLM paths. - Config.disable_thinking (backend) + Configuration.disable_thinking (CLI), default True; DISABLE_THINKING env var (default true) for web-app mode - Provider guard: injection is skipped for first-party APIs that reject unknown fields and don't serve such models (OpenAI, Azure OpenAI, Bedrock, Anthropic); local/self-hosted OpenAI-compatible servers ignore or honor it - `codewiki config set --disable-thinking/--enable-thinking` and `codewiki generate --disable-thinking/--enable-thinking`; escape hatch is --enable-thinking / DISABLE_THINKING=false Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 599bfc3 commit fd0cfe9

7 files changed

Lines changed: 93 additions & 5 deletions

File tree

codewiki/cli/adapters/doc_generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def generate(self) -> DocumentationJob:
151151
use_gitignore=self.config.get('use_gitignore', True),
152152
prompt_caching=self.config.get('prompt_caching', True),
153153
max_retries=self.config.get('max_retries', 3),
154+
disable_thinking=self.config.get('disable_thinking', True),
154155
)
155156

156157
# Run backend documentation generation

codewiki/cli/commands/config.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ def config_group():
128128
help="Tool-call retries allowed per agent before giving up. Raise this for "
129129
"weaker/local models that emit malformed tool arguments (default: 3)",
130130
)
131+
@click.option(
132+
"--disable-thinking/--enable-thinking",
133+
"disable_thinking",
134+
default=None,
135+
help="Turn off reasoning/thinking mode for hybrid-thinking models such as "
136+
"Qwen3 (sends chat_template_kwargs.enable_thinking=false; default: "
137+
"disabled). Use --enable-thinking to leave it under provider control.",
138+
)
131139
def config_set(
132140
api_key: Optional[str],
133141
base_url: Optional[str],
@@ -145,6 +153,7 @@ def config_set(
145153
use_gitignore: Optional[bool] = None,
146154
prompt_caching: Optional[bool] = None,
147155
max_retries: Optional[int] = None,
156+
disable_thinking: Optional[bool] = None,
148157
):
149158
"""
150159
Set configuration values for CodeWiki.
@@ -197,7 +206,7 @@ def config_set(
197206
"""
198207
try:
199208
# Check if at least one option is provided
200-
if not any([api_key, base_url, main_model, cluster_model, fallback_model, max_tokens, max_token_per_module, max_token_per_leaf_module, max_depth, provider, aws_region, api_version, azure_deployment, use_gitignore is not None, prompt_caching is not None, max_retries is not None]):
209+
if not any([api_key, base_url, main_model, cluster_model, fallback_model, max_tokens, max_token_per_module, max_token_per_leaf_module, max_depth, provider, aws_region, api_version, azure_deployment, use_gitignore is not None, prompt_caching is not None, max_retries is not None, disable_thinking is not None]):
201210
click.echo("No options provided. Use --help for usage information.")
202211
sys.exit(EXIT_CONFIG_ERROR)
203212

@@ -271,6 +280,8 @@ def config_set(
271280
if max_retries < 1:
272281
raise ConfigurationError("max_retries must be a positive integer")
273282
validated_data['max_retries'] = max_retries
283+
if disable_thinking is not None:
284+
validated_data['disable_thinking'] = disable_thinking
274285

275286
# Create config manager and save
276287
manager = ConfigManager()
@@ -293,6 +304,7 @@ def config_set(
293304
use_gitignore=validated_data.get('use_gitignore'),
294305
prompt_caching=validated_data.get('prompt_caching'),
295306
max_retries=validated_data.get('max_retries'),
307+
disable_thinking=validated_data.get('disable_thinking'),
296308
)
297309

298310
# Display success messages
@@ -361,6 +373,8 @@ def config_set(
361373

362374
if max_retries is not None:
363375
click.secho(f"✓ Max retries: {max_retries}", fg="green")
376+
if disable_thinking is not None:
377+
click.secho(f"✓ Disable thinking: {disable_thinking}", fg="green")
364378

365379
click.echo("\n" + click.style("Configuration updated successfully.", fg="green", bold=True))
366380

@@ -425,6 +439,7 @@ def config_show(output_json: bool):
425439
"use_gitignore": config.use_gitignore if config else True,
426440
"prompt_caching": config.prompt_caching if config else True,
427441
"max_retries": config.max_retries if config else 3,
442+
"disable_thinking": config.disable_thinking if config else True,
428443
"agent_instructions": config.agent_instructions.to_dict() if config and config.agent_instructions else {},
429444
"config_file": str(manager.config_file_path)
430445
}
@@ -482,6 +497,7 @@ def config_show(output_json: bool):
482497
click.echo(f" Max Token/Leaf Module: {config.max_token_per_leaf_module}")
483498
click.echo(f" Prompt Caching: {config.prompt_caching}")
484499
click.echo(f" Max Retries: {config.max_retries}")
500+
click.echo(f" Disable Thinking: {config.disable_thinking}")
485501

486502
click.echo()
487503
click.secho("Decomposition Settings", fg="cyan", bold=True)

codewiki/cli/commands/generate.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ def _find_affected(tree, parent_names=None):
313313
default=None,
314314
help="Tool-call retries allowed per agent before giving up (overrides config)",
315315
)
316+
@click.option(
317+
"--disable-thinking/--enable-thinking",
318+
"disable_thinking",
319+
default=None,
320+
help="Turn off reasoning/thinking mode for hybrid-thinking models such as "
321+
"Qwen3 (overrides config)",
322+
)
316323
@click.option(
317324
"--update",
318325
is_flag=True,
@@ -344,6 +351,7 @@ def generate_command(
344351
max_depth: Optional[int],
345352
prompt_caching: Optional[bool],
346353
max_retries: Optional[int],
354+
disable_thinking: Optional[bool],
347355
update: bool = False,
348356
compare_to: Optional[str] = None
349357
):
@@ -548,13 +556,15 @@ def generate_command(
548556
effective_use_gitignore = use_gitignore if use_gitignore is not None else config.use_gitignore
549557
effective_prompt_caching = prompt_caching if prompt_caching is not None else config.prompt_caching
550558
effective_max_retries = max_retries if max_retries is not None else config.max_retries
559+
effective_disable_thinking = disable_thinking if disable_thinking is not None else config.disable_thinking
551560
logger.debug(f"Max tokens: {effective_max_tokens}")
552561
logger.debug(f"Max token/module: {effective_max_token_per_module}")
553562
logger.debug(f"Max token/leaf module: {effective_max_token_per_leaf}")
554563
logger.debug(f"Max depth: {effective_max_depth}")
555564
logger.debug(f"Use gitignore: {effective_use_gitignore}")
556565
logger.debug(f"Prompt caching: {effective_prompt_caching}")
557566
logger.debug(f"Max retries: {effective_max_retries}")
567+
logger.debug(f"Disable thinking: {effective_disable_thinking}")
558568

559569
# Get agent instructions (merge runtime with persistent)
560570
agent_instructions_dict = None
@@ -598,6 +608,8 @@ def generate_command(
598608
'prompt_caching': prompt_caching if prompt_caching is not None else config.prompt_caching,
599609
# Tool-call retry setting (runtime override takes precedence)
600610
'max_retries': max_retries if max_retries is not None else config.max_retries,
611+
# Thinking-mode setting (runtime override takes precedence)
612+
'disable_thinking': disable_thinking if disable_thinking is not None else config.disable_thinking,
601613
},
602614
verbose=verbose,
603615
generate_html=github_pages,

codewiki/cli/config_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def save(
139139
use_gitignore: Optional[bool] = None,
140140
prompt_caching: Optional[bool] = None,
141141
max_retries: Optional[int] = None,
142+
disable_thinking: Optional[bool] = None,
142143
):
143144
"""
144145
Save configuration to file and keyring.
@@ -161,6 +162,7 @@ def save(
161162
use_gitignore: Apply Git ignore rules during repository analysis
162163
prompt_caching: Add prompt-cache breakpoints to agentic LLM calls
163164
max_retries: Tool-call retries allowed per agent before giving up
165+
disable_thinking: Turn off reasoning/thinking mode for the model
164166
"""
165167
# Ensure config directory exists
166168
try:
@@ -216,6 +218,8 @@ def save(
216218
self._config.prompt_caching = prompt_caching
217219
if max_retries is not None:
218220
self._config.max_retries = max_retries
221+
if disable_thinking is not None:
222+
self._config.disable_thinking = disable_thinking
219223

220224
# Validate configuration whenever the minimum required fields are set.
221225
# Caw providers only need main_model; API providers need base_url +

codewiki/cli/models/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class Configuration:
124124
use_gitignore: Apply Git ignore rules during repository analysis
125125
prompt_caching: Add prompt-cache breakpoints to agentic LLM calls (default: True)
126126
max_retries: Tool-call retries allowed per agent before giving up (default: 3)
127+
disable_thinking: Turn off reasoning/thinking mode for hybrid-thinking
128+
models such as Qwen3 (default: True)
127129
agent_instructions: Custom agent instructions for documentation generation
128130
"""
129131
base_url: str
@@ -142,6 +144,7 @@ class Configuration:
142144
use_gitignore: bool = True
143145
prompt_caching: bool = True
144146
max_retries: int = 3
147+
disable_thinking: bool = True
145148
agent_instructions: AgentInstructions = field(default_factory=AgentInstructions)
146149

147150
def validate(self):
@@ -181,6 +184,7 @@ def to_dict(self) -> dict:
181184
'use_gitignore': self.use_gitignore,
182185
'prompt_caching': self.prompt_caching,
183186
'max_retries': self.max_retries,
187+
'disable_thinking': self.disable_thinking,
184188
'fallback_model': self.fallback_model,
185189
}
186190
if self.agent_instructions and not self.agent_instructions.is_empty():
@@ -219,6 +223,7 @@ def from_dict(cls, data: dict) -> 'Configuration':
219223
use_gitignore=data.get('use_gitignore', True),
220224
prompt_caching=data.get('prompt_caching', True),
221225
max_retries=data.get('max_retries', 3),
226+
disable_thinking=data.get('disable_thinking', True),
222227
agent_instructions=agent_instructions,
223228
)
224229

@@ -289,4 +294,5 @@ def to_backend_config(self, repo_path: str, output_dir: str, api_key: str, runti
289294
use_gitignore=self.use_gitignore,
290295
prompt_caching=self.prompt_caching,
291296
max_retries=self.max_retries,
297+
disable_thinking=self.disable_thinking,
292298
)

codewiki/src/be/llm_services.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,37 @@ def _should_use_max_completion_tokens(model_name: str, base_url: str) -> bool:
7272
return False
7373

7474

75+
def _thinking_disabled_extra_body() -> dict:
76+
"""Request-body fields that ask the server to turn off reasoning/thinking.
77+
78+
``chat_template_kwargs.enable_thinking`` is the switch honored by hybrid
79+
thinking models (e.g. Qwen3) served via vLLM, SGLang, llama.cpp and
80+
LM Studio; the top-level ``enable_thinking`` covers DashScope-style
81+
endpoints. Servers that don't recognize these fields ignore them.
82+
"""
83+
return {
84+
"chat_template_kwargs": {"enable_thinking": False},
85+
"enable_thinking": False,
86+
}
87+
88+
89+
def _thinking_toggle_supported(config: Config) -> bool:
90+
"""Whether it's safe to send the thinking-off ``extra_body`` for this config.
91+
92+
First-party APIs (OpenAI, Azure OpenAI, Bedrock, Anthropic) reject unknown
93+
request fields, and none of them serve hybrid Qwen-style thinking models, so
94+
injection is skipped there. Local / self-hosted OpenAI-compatible servers
95+
and proxies (vLLM, SGLang, llama.cpp, LiteLLM, Ollama, etc.) either honor or
96+
ignore the fields.
97+
"""
98+
if config.provider in ("azure-openai", "bedrock", "anthropic"):
99+
return False
100+
base_url = (config.llm_base_url or "").lower()
101+
if "api.openai.com" in base_url or ".openai.azure.com" in base_url:
102+
return False
103+
return True
104+
105+
75106
def _build_model_settings(config: Config, model_name: str) -> OpenAIChatModelSettings:
76107
"""Build model settings with the correct token parameter.
77108
@@ -80,12 +111,16 @@ def _build_model_settings(config: Config, model_name: str) -> OpenAIChatModelSet
80111
provider default.
81112
"""
82113
if _should_use_max_completion_tokens(model_name, config.llm_base_url):
83-
return OpenAIChatModelSettings(
114+
settings = OpenAIChatModelSettings(
84115
max_completion_tokens=config.max_tokens
85116
)
86-
return OpenAIChatModelSettings(
87-
max_tokens=config.max_tokens
88-
)
117+
else:
118+
settings = OpenAIChatModelSettings(
119+
max_tokens=config.max_tokens
120+
)
121+
if getattr(config, "disable_thinking", False) and _thinking_toggle_supported(config):
122+
settings["extra_body"] = _thinking_disabled_extra_body()
123+
return settings
89124

90125

91126
def _get_litellm_model_name(model_name: str, provider: str) -> str:
@@ -346,6 +381,8 @@ def call_llm(
346381
"model": model,
347382
"messages": [{"role": "user", "content": prompt}],
348383
}
384+
if getattr(config, "disable_thinking", False) and _thinking_toggle_supported(config):
385+
base_kwargs["extra_body"] = _thinking_disabled_extra_body()
349386

350387
try:
351388
response = client.chat.completions.create(

codewiki/src/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def is_cli_context() -> bool:
5050
CLUSTER_MODEL = os.getenv('CLUSTER_MODEL', MAIN_MODEL)
5151
LLM_BASE_URL = os.getenv('LLM_BASE_URL', 'http://0.0.0.0:4000/')
5252
LLM_API_KEY = os.getenv('LLM_API_KEY', 'sk-1234')
53+
# Disable reasoning/thinking mode by default (hybrid-thinking models like Qwen3
54+
# emit <think> blocks that corrupt tool-call output). Set DISABLE_THINKING=false
55+
# to leave thinking under provider control.
56+
DISABLE_THINKING = os.getenv('DISABLE_THINKING', 'true').lower() not in ('0', 'false', 'no')
5357

5458
# Atlas Cloud default endpoint (OpenAI-compatible). Used to auto-fill the base URL
5559
# when the user selects the `atlas-cloud` provider without passing --base-url.
@@ -87,6 +91,10 @@ class Config:
8791
# validation before giving up. Higher values help weaker/local models that
8892
# frequently emit malformed tool arguments (default: 3).
8993
max_retries: int = 3
94+
# Ask the provider to disable reasoning/thinking mode (hybrid models like
95+
# Qwen3). Sent as chat_template_kwargs.enable_thinking=false via extra_body;
96+
# skipped for first-party APIs that reject unknown fields. Default: True.
97+
disable_thinking: bool = True
9098
# Agent instructions for customization
9199
agent_instructions: Optional[Dict[str, Any]] = None
92100
# Apply Git ignore rules before dependency analysis
@@ -172,6 +180,7 @@ def from_args(cls, args: argparse.Namespace) -> 'Config':
172180
cluster_model=CLUSTER_MODEL,
173181
fallback_model=FALLBACK_MODEL_1,
174182
use_gitignore=getattr(args, "use_gitignore", True),
183+
disable_thinking=DISABLE_THINKING,
175184
)
176185

177186
@classmethod
@@ -198,6 +207,7 @@ def from_cli(
198207
use_gitignore: bool = True,
199208
prompt_caching: bool = True,
200209
max_retries: int = 3,
210+
disable_thinking: bool = True,
201211
) -> 'Config':
202212
"""
203213
Create configuration for CLI context.
@@ -227,6 +237,7 @@ def from_cli(
227237
use_gitignore: Whether to apply Git ignore rules
228238
prompt_caching: Whether to add prompt-cache breakpoints to agentic calls
229239
max_retries: Tool-call retries allowed per agent before giving up
240+
disable_thinking: Ask the provider to turn off reasoning/thinking mode
230241
231242
Returns:
232243
Config instance
@@ -258,4 +269,5 @@ def from_cli(
258269
use_gitignore=use_gitignore,
259270
prompt_caching=prompt_caching,
260271
max_retries=max_retries,
272+
disable_thinking=disable_thinking,
261273
)

0 commit comments

Comments
 (0)