#!/usr/bin/env python3 import argparse import json import os import sys import time import urllib.error import urllib.request from datetime import datetime from pathlib import Path OPENAI_COMPAT_TESTS = [ { "name": "official_disable", "model": "qwen/qwen3.6-35b-a3b", "enable_thinking": False, }, { "name": "official_enable", "model": "qwen/qwen3.6-35b-a3b", "enable_thinking": True, }, { "name": "hauhau_disable", "model": "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive", "enable_thinking": False, }, { "name": "hauhau_enable", "model": "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive", "enable_thinking": True, }, ] NATIVE_TESTS = [ { "name": "official_reasoning_off", "model": "qwen/qwen3.6-35b-a3b", "reasoning": "off", }, { "name": "official_reasoning_on", "model": "qwen/qwen3.6-35b-a3b", "reasoning": "on", }, { "name": "hauhau_reasoning_off", "model": "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive", "reasoning": "off", }, { "name": "hauhau_reasoning_on", "model": "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive", "reasoning": "on", }, { "name": "wrapped_reasoning_off", "model": "local/qwen3.6-35b-a3b-hauhaucs-aggressive", "reasoning": "off", }, { "name": "wrapped_reasoning_on", "model": "local/qwen3.6-35b-a3b-hauhaucs-aggressive", "reasoning": "on", }, ] def post_json(url: str, payload: dict, timeout: int) -> tuple[int, str]: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") request = urllib.request.Request( url, data=body, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(request, timeout=timeout) as response: return response.status, response.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as exc: return exc.code, exc.read().decode("utf-8", errors="replace") def get_json(url: str, timeout: int) -> tuple[int, str]: request = urllib.request.Request(url, method="GET") try: with urllib.request.urlopen(request, timeout=timeout) as response: return response.status, response.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as exc: return exc.code, exc.read().decode("utf-8", errors="replace") def parse_json_maybe(raw: str): try: return json.loads(raw) except json.JSONDecodeError: return None def summarize_response(test: dict, status_code: int, raw_text: str) -> dict: parsed = parse_json_maybe(raw_text) summary = { "test": test["name"], "model": test["model"], "enable_thinking": test["enable_thinking"], "http_status": status_code, "json_valid": parsed is not None, "error": None, "finish_reason": None, "content": None, "reasoning_content": None, "contains_think_tag_in_content": None, "contains_think_tag_in_reasoning": None, "usage": None, } if parsed is None: summary["error"] = "response_not_json" return summary if "error" in parsed: summary["error"] = parsed["error"] choices = parsed.get("choices") or [] if not choices: return summary choice = choices[0] message = choice.get("message") or {} content = message.get("content") reasoning = message.get("reasoning_content") summary["finish_reason"] = choice.get("finish_reason") summary["content"] = content summary["reasoning_content"] = reasoning summary["contains_think_tag_in_content"] = ( isinstance(content, str) and ("" in content or "" in content) ) summary["contains_think_tag_in_reasoning"] = ( isinstance(reasoning, str) and ("" in reasoning or "" in reasoning) ) summary["usage"] = parsed.get("usage") return summary def summarize_native_response(test: dict, status_code: int, raw_text: str) -> dict: parsed = parse_json_maybe(raw_text) summary = { "test": test["name"], "model": test["model"], "reasoning": test["reasoning"], "http_status": status_code, "json_valid": parsed is not None, "error": None, "message_content": [], "reasoning_content": [], "output_types": [], "stats": None, } if parsed is None: summary["error"] = "response_not_json" return summary if "error" in parsed: summary["error"] = parsed["error"] output = parsed.get("output") or [] summary["output_types"] = [item.get("type") for item in output] for item in output: if item.get("type") == "message": summary["message_content"].append(item.get("content")) elif item.get("type") == "reasoning": summary["reasoning_content"].append(item.get("content")) summary["stats"] = parsed.get("stats") return summary def ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) def write_text(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8") def write_json(path: Path, obj) -> None: path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=7860) parser.add_argument("--timeout", type=int, default=1800) parser.add_argument("--max-tokens", type=int, default=256) parser.add_argument( "--prompt", default="Compute 317 * 29. Give the final answer only.", ) parser.add_argument( "--output-root", default="artifacts/lmstudio-thinking-probe", ) args = parser.parse_args() timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") run_dir = Path(args.output_root) / timestamp ensure_dir(run_dir) base_url = f"http://{args.host}:{args.port}/v1" metadata = { "base_url": base_url, "started_at": datetime.now().isoformat(), "cwd": os.getcwd(), "python": sys.version, "openai_compat_tests": OPENAI_COMPAT_TESTS, "native_tests": NATIVE_TESTS, "prompt": args.prompt, "timeout": args.timeout, "max_tokens": args.max_tokens, } write_json(run_dir / "run_metadata.json", metadata) status_code, models_raw = get_json(f"{base_url}/models", timeout=30) write_text(run_dir / "models.raw.json", models_raw) write_json( run_dir / "models.summary.json", { "http_status": status_code, "json_valid": parse_json_maybe(models_raw) is not None, }, ) all_summaries = [] for index, test in enumerate(OPENAI_COMPAT_TESTS, start=1): payload = { "model": test["model"], "messages": [{"role": "user", "content": args.prompt}], "temperature": 0.2, "max_tokens": args.max_tokens, "stream": False, "enable_thinking": test["enable_thinking"], } prefix = f"{index:02d}_{test['name']}" write_json(run_dir / f"{prefix}.request.json", payload) started = time.time() status_code, raw_text = post_json( f"{base_url}/chat/completions", payload, timeout=args.timeout, ) elapsed = time.time() - started write_text(run_dir / f"{prefix}.response.raw.json", raw_text) summary = summarize_response(test, status_code, raw_text) summary["elapsed_seconds"] = round(elapsed, 3) write_json(run_dir / f"{prefix}.summary.json", summary) all_summaries.append(summary) native_summaries = [] for index, test in enumerate(NATIVE_TESTS, start=1): payload = { "model": test["model"], "input": args.prompt, "temperature": 0.2, "max_output_tokens": args.max_tokens, "reasoning": test["reasoning"], "store": False, } prefix = f"native_{index:02d}_{test['name']}" write_json(run_dir / f"{prefix}.request.json", payload) started = time.time() status_code, raw_text = post_json( f"http://{args.host}:{args.port}/api/v1/chat", payload, timeout=args.timeout, ) elapsed = time.time() - started write_text(run_dir / f"{prefix}.response.raw.json", raw_text) summary = summarize_native_response(test, status_code, raw_text) summary["elapsed_seconds"] = round(elapsed, 3) write_json(run_dir / f"{prefix}.summary.json", summary) native_summaries.append(summary) report_lines = [ "# LM Studio Thinking Probe", "", f"- Base URL: `{base_url}`", f"- Prompt: `{args.prompt}`", f"- Run dir: `{run_dir}`", "", "| Test | HTTP | enable_thinking | finish | reasoning_content | think tags in content | content |", "| --- | --- | --- | --- | --- | --- | --- |", ] for summary in all_summaries: content = summary["content"] if isinstance(content, str): content = content.replace("\n", "\\n") content = content[:80] reasoning_present = summary["reasoning_content"] is not None report_lines.append( "| {test} | {http_status} | {enable_thinking} | {finish_reason} | {reasoning_present} | {contains_think_tag_in_content} | {content} |".format( test=summary["test"], http_status=summary["http_status"], enable_thinking=summary["enable_thinking"], finish_reason=summary["finish_reason"], reasoning_present=reasoning_present, contains_think_tag_in_content=summary["contains_think_tag_in_content"], content=content, ) ) report_lines.extend( [ "", "## Native API", "", "| Test | HTTP | reasoning | reasoning items | message items | stats |", "| --- | --- | --- | --- | --- | --- |", ] ) for summary in native_summaries: report_lines.append( "| {test} | {http_status} | {reasoning} | {reasoning_count} | {message_count} | {stats} |".format( test=summary["test"], http_status=summary["http_status"], reasoning=summary["reasoning"], reasoning_count=len(summary["reasoning_content"]), message_count=len(summary["message_content"]), stats=str(summary["stats"]).replace("\n", " "), ) ) write_text(run_dir / "report.md", "\n".join(report_lines) + "\n") write_json( run_dir / "all_summaries.json", { "completed_at": datetime.now().isoformat(), "openai_compat_results": all_summaries, "native_results": native_summaries, }, ) print(run_dir) return 0 if __name__ == "__main__": raise SystemExit(main())