فهرست منبع

feat: 添加 HauhauCS GGUF 工具和仓库指南

- 新增 tools/hauhaucs-gguf/remove_gguf_thinking.py 工具,
  用于修补 GGUF 文件中的 chat template,强制输出空 think 块
- 新增 tools/hauhaucs-gguf/README.md 文档,
  说明 GGUF 工具的用途和使用方法
- 新增 AGENTS.md 仓库指南,包含项目结构、构建测试命令、
  编码风格、测试规范、提交规范和文档协作规则
kekeZack 1 ماه پیش
والد
کامیت
e5b0fb8c51
3فایلهای تغییر یافته به همراه125 افزوده شده و 0 حذف شده
  1. 42 0
      AGENTS.md
  2. 32 0
      tools/hauhaucs-gguf/README.md
  3. 51 0
      tools/hauhaucs-gguf/remove_gguf_thinking.py

+ 42 - 0
AGENTS.md

@@ -0,0 +1,42 @@
+# Repository Guidelines
+
+## 项目结构与模块组织
+
+本仓库围绕 `LM Studio` reasoning/toggle 问题组织为三块:
+
+- `tools/lmstudio/`:一次性工具、wrapper 生成器、协议探针。
+- `services/litellm-lmstudio-adapter/`:长期运行的 FastAPI 适配服务。
+- `tests/`:顶层工具测试;服务自身测试位于 `services/litellm-lmstudio-adapter/tests/`。
+- `docs/`:规格、分析与交接文档。
+- `artifacts/`:实验输出、日志与探针结果,不放业务源码。
+
+新增代码时,优先延续现有分层,不要把服务逻辑混入 `tools/`。
+
+## 构建、测试与开发命令
+
+- `python -m unittest tests.test_lmstudio_wrapper_generator`
+  - 运行 wrapper 生成器测试。
+- `python -m unittest tests.test_openai_reasoning_proxy`
+  - 运行早期 reasoning 代理测试。
+- `python -m unittest discover -s services/litellm-lmstudio-adapter/tests -p "test_*.py"`
+  - 运行适配服务测试。
+- `python services/litellm-lmstudio-adapter/litellm_lmstudio_adapter.py`
+  - 本地直接启动适配服务,适合快速验证。
+
+提交前至少运行受影响模块对应的测试;修改服务时,优先补跑完整服务测试集。
+
+## 编码风格与命名约定
+
+仓库以 Python 为主,使用 4 空格缩进,遵循标准库优先和清晰胜过技巧的原则。文件、函数、变量使用 `snake_case`;测试文件命名为 `test_*.py`。目录名沿用现状,例如 `litellm-lmstudio-adapter`,避免无必要重命名。仅在复杂协议转换处添加简短注释。
+
+## 测试规范
+
+测试框架为 `unittest`。新增功能或修复缺陷时,应同时补充回归测试。顶层工具测试放在 `tests/`,服务内部行为测试放在 `services/litellm-lmstudio-adapter/tests/`。优先覆盖请求转换、流式 SSE 处理、以及 LM Studio 原生透传路径。
+
+## 提交与合并请求规范
+
+当前历史很短,现有提交风格为简短前缀式摘要,例如 `init: 初始化项目`。后续建议保持 `type: 简述` 格式,如 `fix: 修复 SSE block 解析`。PR 应包含:变更目的、影响范围、测试命令与结果;如果修改接口行为,补充示例请求或响应片段。
+
+## 文档与协作规则
+
+本仓库新增文档、说明、注释性使用示例,以及协作回复默认使用中文;仅在外部接口字段、协议名、或第三方术语必须保留英文时例外。编写代理说明或交接文档时,也应优先使用中文,并引用具体路径与命令,避免空泛描述。

+ 32 - 0
tools/hauhaucs-gguf/README.md

@@ -0,0 +1,32 @@
+# HauhauCS GGUF 工具
+
+直接修补 HauhauCS GGUF 文件中的 chat template,去掉 `enable_thinking` 条件分支,强制输出空 think 块。
+
+## 文件说明
+
+### [remove_gguf_thinking.py](remove_gguf_thinking.py)
+
+**用途:**
+
+- 读取 GGUF 文件中的 `tokenizer.chat_template`
+- 将原本根据 `enable_thinking` 切换的模板替换为始终输出 `<think>\n\n</think>\n\n`
+- 原地写回,不改变文件大小
+
+**适合场景:**
+
+- 你有一个 HauhauCS 的 GGUF 模型
+- 外部调用无法或不想传 `enable_thinking` 参数
+- 想让模型始终以 "thinking off" 模式运行
+
+**用法:**
+
+```powershell
+python remove_gguf_thinking.py 模型文件.gguf
+```
+
+不传参数时默认路径为 `Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-NoThink-Q4_K_M.gguf`。
+
+**注意:**
+
+- 新模板长度不能超过原模板,否则报错退出
+- 直接修改原文件,建议先备份

+ 51 - 0
tools/hauhaucs-gguf/remove_gguf_thinking.py

@@ -0,0 +1,51 @@
+import struct
+import sys
+
+src = r'Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-NoThink-Q4_K_M.gguf'
+if len(sys.argv) > 1:
+    src = sys.argv[1]
+
+with open(src, 'rb') as f:
+    data = bytearray(f.read())
+
+key = b'tokenizer.chat_template'
+kp = data.find(key)
+kl = struct.unpack('<Q', data[kp-8:kp])[0]
+sl = struct.unpack('<Q', data[kp+kl+4:kp+kl+12])[0]
+sp = kp + kl + 12
+template = data[sp:sp+sl].decode('utf-8').rstrip(' ')
+
+old = (
+    "{%- if add_generation_prompt %}\n"
+    "    {{- '<|im_start|>assistant\\n' }}\n"
+    "    {%- if enable_thinking is defined and enable_thinking is false %}\n"
+    "        {{- '<think>\\n\\n</think>\\n\\n' }}\n"
+    "    {%- else %}\n"
+    "        {{- '<think>\\n' }}\n"
+    "    {%- endif %}\n"
+    "{%- endif %}"
+)
+new = (
+    "{%- if add_generation_prompt %}\n"
+    "    {{- '<|im_start|>assistant\\n' }}\n"
+    "    {{- '<think>\\n\\n</think>\\n\\n' }}\n"
+    "{%- endif %}"
+)
+
+if old in template:
+    template = template.replace(old, new)
+    new_bytes = template.encode('utf-8')
+    if len(new_bytes) > sl:
+        print("ERROR: New template longer than original")
+        exit(1)
+    padded = new_bytes + b' ' * (sl - len(new_bytes))
+    data[sp:sp+sl] = padded
+    with open(src, 'wb') as f:
+        f.write(data)
+    print("Done: enable_thinking conditional removed, forced empty think block")
+else:
+    print("No enable_thinking conditional found in template")
+    idx = template.find('enable_thinking')
+    if idx >= 0:
+        print("Found at position", idx)
+        print(template[idx-50:idx+200])