wrapper_generator.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. #!/usr/bin/env python3
  2. import argparse
  3. import json
  4. from datetime import datetime
  5. from pathlib import Path
  6. DEFAULT_OFFICIAL_MODEL_YAML = Path(
  7. r"G:\LMStudio\.lmstudio\hub\models\qwen\qwen3.6-35b-a3b\model.yaml"
  8. )
  9. DEFAULT_OFFICIAL_MANIFEST = Path(
  10. r"G:\LMStudio\.lmstudio\hub\models\qwen\qwen3.6-35b-a3b\manifest.json"
  11. )
  12. DEFAULT_OUTPUT_ROOT = Path("artifacts/lmstudio-model-wrappers")
  13. DEFAULT_TARGET_MODEL_KEY = "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive"
  14. DEFAULT_WRAPPER_OWNER = "local"
  15. DEFAULT_WRAPPER_NAME = "qwen3.6-35b-a3b-hauhaucs-aggressive"
  16. def build_wrapper_model_yaml(
  17. official_model_yaml_text: str,
  18. target_model_key: str,
  19. wrapper_owner: str,
  20. wrapper_name: str,
  21. ) -> str:
  22. lines = official_model_yaml_text.splitlines()
  23. output = []
  24. replaced_model = False
  25. inserted_base = False
  26. skipped_original_base = False
  27. for line in lines:
  28. stripped = line.strip()
  29. if not replaced_model and stripped.startswith("model: "):
  30. output.append(f"model: {wrapper_owner}/{wrapper_name}")
  31. replaced_model = True
  32. continue
  33. if not inserted_base and stripped.startswith("base:"):
  34. output.append(f'base: "{target_model_key}"')
  35. inserted_base = True
  36. skipped_original_base = True
  37. continue
  38. if skipped_original_base:
  39. if line.startswith("metadataOverrides:"):
  40. skipped_original_base = False
  41. output.append(line)
  42. continue
  43. output.append(line)
  44. if not replaced_model:
  45. raise ValueError("Official model.yaml is missing a model declaration")
  46. if not inserted_base:
  47. raise ValueError("Official model.yaml is missing a base declaration")
  48. return "\n".join(output).rstrip() + "\n"
  49. def build_wrapper_manifest(
  50. official_manifest: dict,
  51. target_model_key: str,
  52. wrapper_owner: str,
  53. wrapper_name: str,
  54. ) -> dict:
  55. manifest = {
  56. "type": "model",
  57. "owner": wrapper_owner,
  58. "name": wrapper_name,
  59. "dependencies": [
  60. {
  61. "type": "model",
  62. "purpose": "baseModel",
  63. "modelKeys": [target_model_key],
  64. }
  65. ],
  66. "revision": int(official_manifest.get("revision", 1)),
  67. }
  68. source_dep = (official_manifest.get("dependencies") or [{}])[0]
  69. if source_dep.get("sources"):
  70. manifest["dependencies"][0]["sources"] = source_dep["sources"]
  71. return manifest
  72. def build_readme(
  73. target_model_key: str,
  74. wrapper_owner: str,
  75. wrapper_name: str,
  76. ) -> str:
  77. return (
  78. f"# {wrapper_owner}/{wrapper_name}\n\n"
  79. "This wrapper exposes LM Studio reasoning metadata for a local HauhauCS GGUF.\n\n"
  80. "## Source Model\n\n"
  81. f"- Local model key: `{target_model_key}`\n\n"
  82. "## Intent\n\n"
  83. "- Preserve the original HauhauCS weights unchanged\n"
  84. "- Reuse the official Qwen3.6 reasoning-capable model wrapper behavior\n"
  85. "- Keep the wrapper portable and separate from LM Studio `.internal` state\n\n"
  86. "## Next Steps\n\n"
  87. "- Place or link this wrapper into an LM Studio-recognized local model package location\n"
  88. "- Reload LM Studio model metadata\n"
  89. "- Validate the wrapper via UI reasoning toggle and `/api/v1/chat`\n"
  90. )
  91. def build_wrapper_files(
  92. target_model_key: str,
  93. wrapper_owner: str,
  94. wrapper_name: str,
  95. official_model_yaml_text: str,
  96. official_manifest: dict,
  97. ) -> dict:
  98. model_yaml = build_wrapper_model_yaml(
  99. official_model_yaml_text=official_model_yaml_text,
  100. target_model_key=target_model_key,
  101. wrapper_owner=wrapper_owner,
  102. wrapper_name=wrapper_name,
  103. )
  104. manifest = build_wrapper_manifest(
  105. official_manifest=official_manifest,
  106. target_model_key=target_model_key,
  107. wrapper_owner=wrapper_owner,
  108. wrapper_name=wrapper_name,
  109. )
  110. readme = build_readme(
  111. target_model_key=target_model_key,
  112. wrapper_owner=wrapper_owner,
  113. wrapper_name=wrapper_name,
  114. )
  115. return {
  116. "model.yaml": model_yaml,
  117. "manifest.json": json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
  118. "README.md": readme,
  119. }
  120. def generate_wrapper(
  121. output_dir: Path,
  122. target_model_key: str,
  123. wrapper_owner: str,
  124. wrapper_name: str,
  125. official_model_yaml_text: str,
  126. official_manifest: dict,
  127. dry_run: bool,
  128. ) -> dict:
  129. files = build_wrapper_files(
  130. target_model_key=target_model_key,
  131. wrapper_owner=wrapper_owner,
  132. wrapper_name=wrapper_name,
  133. official_model_yaml_text=official_model_yaml_text,
  134. official_manifest=official_manifest,
  135. )
  136. summary = {
  137. "generated_at": datetime.now().isoformat(),
  138. "dry_run": dry_run,
  139. "output_dir": str(output_dir),
  140. "wrapper_model": f"{wrapper_owner}/{wrapper_name}",
  141. "target_model_key": target_model_key,
  142. "files": sorted(files.keys()),
  143. }
  144. if dry_run:
  145. return summary
  146. output_dir.mkdir(parents=True, exist_ok=True)
  147. for name, content in files.items():
  148. (output_dir / name).write_text(content, encoding="utf-8")
  149. (output_dir / "summary.json").write_text(
  150. json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
  151. encoding="utf-8",
  152. )
  153. return summary
  154. def load_inputs(model_yaml_path: Path, manifest_path: Path) -> tuple[str, dict]:
  155. return (
  156. model_yaml_path.read_text(encoding="utf-8"),
  157. json.loads(manifest_path.read_text(encoding="utf-8")),
  158. )
  159. def main() -> int:
  160. parser = argparse.ArgumentParser()
  161. parser.add_argument("--target-model-key", default=DEFAULT_TARGET_MODEL_KEY)
  162. parser.add_argument("--wrapper-owner", default=DEFAULT_WRAPPER_OWNER)
  163. parser.add_argument("--wrapper-name", default=DEFAULT_WRAPPER_NAME)
  164. parser.add_argument("--official-model-yaml", default=str(DEFAULT_OFFICIAL_MODEL_YAML))
  165. parser.add_argument("--official-manifest", default=str(DEFAULT_OFFICIAL_MANIFEST))
  166. parser.add_argument("--output-dir", default="")
  167. parser.add_argument("--output-root", default=str(DEFAULT_OUTPUT_ROOT))
  168. parser.add_argument("--dry-run", action="store_true")
  169. args = parser.parse_args()
  170. official_model_yaml_text, official_manifest = load_inputs(
  171. Path(args.official_model_yaml),
  172. Path(args.official_manifest),
  173. )
  174. if args.output_dir:
  175. output_dir = Path(args.output_dir)
  176. else:
  177. output_dir = Path(args.output_root) / args.wrapper_owner / args.wrapper_name
  178. summary = generate_wrapper(
  179. output_dir=output_dir,
  180. target_model_key=args.target_model_key,
  181. wrapper_owner=args.wrapper_owner,
  182. wrapper_name=args.wrapper_name,
  183. official_model_yaml_text=official_model_yaml_text,
  184. official_manifest=official_manifest,
  185. dry_run=args.dry_run,
  186. )
  187. print(json.dumps(summary, ensure_ascii=False, indent=2))
  188. return 0
  189. if __name__ == "__main__":
  190. raise SystemExit(main())