test_lmstudio_wrapper_generator.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import importlib.util
  2. import json
  3. import struct
  4. import tempfile
  5. import unittest
  6. from unittest import mock
  7. from pathlib import Path
  8. MODULE_PATH = Path(__file__).resolve().parent.parent / "tools" / "lmstudio" / "wrapper_generator.py"
  9. if MODULE_PATH.exists():
  10. spec = importlib.util.spec_from_file_location("wrapper_generator", MODULE_PATH)
  11. module = importlib.util.module_from_spec(spec)
  12. assert spec is not None and spec.loader is not None
  13. spec.loader.exec_module(module)
  14. gen = module
  15. else: # pragma: no cover
  16. gen = None
  17. class WrapperGeneratorTests(unittest.TestCase):
  18. def setUp(self):
  19. if gen is None:
  20. self.fail("tools.lmstudio.wrapper_generator is not available")
  21. def _write_gguf_metadata(self, path: Path, metadata: dict[str, object]) -> None:
  22. type_string = 8
  23. def write_string(value: str) -> bytes:
  24. raw = value.encode("utf-8")
  25. return struct.pack("<Q", len(raw)) + raw
  26. def write_value(value: object) -> tuple[int, bytes]:
  27. if isinstance(value, str):
  28. return type_string, write_string(value)
  29. raise TypeError(f"Unsupported GGUF test metadata value: {value!r}")
  30. payload = bytearray()
  31. payload.extend(b"GGUF")
  32. payload.extend(struct.pack("<I", 3))
  33. payload.extend(struct.pack("<Q", 0))
  34. payload.extend(struct.pack("<Q", len(metadata)))
  35. for key, value in metadata.items():
  36. value_type, raw_value = write_value(value)
  37. payload.extend(write_string(key))
  38. payload.extend(struct.pack("<I", value_type))
  39. payload.extend(raw_value)
  40. path.parent.mkdir(parents=True, exist_ok=True)
  41. path.write_bytes(payload)
  42. def test_detect_capabilities_from_cached_metadata(self):
  43. metadata = {
  44. "general.architecture": "qwen35moe",
  45. "general.name": "Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive",
  46. "general.basename": "Qwen3.6-35B-A3B",
  47. "tokenizer.chat_template": (
  48. "{%- if tools %}<tools>{%- endif %}"
  49. "{%- if add_generation_prompt %}"
  50. "{%- if enable_thinking is defined and enable_thinking is false %}"
  51. "{{- '<think>\\n\\n</think>\\n\\n' }}"
  52. "{%- else %}{{- '<think>\\n' }}{%- endif %}{%- endif %}"
  53. "<|vision_start|><|image_pad|><|vision_end|>"
  54. ),
  55. "qwen2.context_length": "262144",
  56. }
  57. profile = gen.build_virtual_model_profile(metadata)
  58. self.assertEqual(profile["architecture"], "qwen35moe")
  59. self.assertEqual(profile["basename_slug"], "qwen3.6-35b-a3b")
  60. self.assertEqual(profile["params_string"], "35B")
  61. self.assertEqual(profile["context_length"], 262144)
  62. self.assertTrue(profile["capabilities"]["reasoning"])
  63. self.assertTrue(profile["capabilities"]["vision"])
  64. self.assertTrue(profile["capabilities"]["tools"])
  65. self.assertTrue(profile["needs_enable_thinking"])
  66. self.assertTrue(profile["needs_preserve_thinking"])
  67. def test_prefers_internal_cache_before_touching_gguf(self):
  68. with tempfile.TemporaryDirectory() as tmpdir:
  69. root = Path(tmpdir)
  70. gguf_path = root / "models" / "publisher-a" / "Fancy-Model-GGUF" / "model.gguf"
  71. self._write_gguf_metadata(
  72. gguf_path,
  73. {
  74. "general.architecture": "qwen35",
  75. "general.name": "Wrong Fallback Name",
  76. "general.basename": "Wrong Fallback Name",
  77. "tokenizer.chat_template": "",
  78. "qwen2.context_length": "4096",
  79. },
  80. )
  81. cache_path = root / ".internal" / "gguf-metadata-cache.json"
  82. cache_path.parent.mkdir(parents=True, exist_ok=True)
  83. cache_payload = {
  84. "json": {
  85. "map": [
  86. [
  87. gguf_path.resolve().as_posix(),
  88. {
  89. "mtimeMs": gguf_path.stat().st_mtime * 1000,
  90. "metadata": {
  91. "arch": "qwen35moe",
  92. "name": "Cached Fancy Model",
  93. "chatTemplate": "<tools>{{ '<think>\\n' }}",
  94. "parameters": "35B-A3B",
  95. "contextLength": 262144,
  96. },
  97. },
  98. ]
  99. ]
  100. }
  101. }
  102. cache_path.write_text(json.dumps(cache_payload, ensure_ascii=False), encoding="utf-8")
  103. with mock.patch.object(gen, "read_gguf_metadata_streaming", side_effect=AssertionError("stream parser should not run on cache hit")):
  104. metadata = gen.resolve_gguf_metadata(
  105. gguf_path=gguf_path,
  106. cache_index=gen.load_gguf_metadata_cache(cache_path),
  107. )
  108. self.assertEqual(metadata["general.architecture"], "qwen35moe")
  109. self.assertEqual(metadata["general.name"], "Cached Fancy Model")
  110. self.assertEqual(metadata["general.parameter_count_label"], "35B-A3B")
  111. self.assertEqual(metadata["general.context_length"], "262144")
  112. def test_stream_parser_is_used_when_cache_missing(self):
  113. with tempfile.TemporaryDirectory() as tmpdir:
  114. gguf_path = Path(tmpdir) / "model.gguf"
  115. self._write_gguf_metadata(
  116. gguf_path,
  117. {
  118. "general.architecture": "gemma4",
  119. "general.name": "Streaming Model",
  120. "general.basename": "Streaming Model",
  121. "tokenizer.chat_template": "{{ '<think>\\n' }}",
  122. "qwen2.context_length": "8192",
  123. },
  124. )
  125. with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("full file read should not be used")):
  126. metadata = gen.resolve_gguf_metadata(
  127. gguf_path=gguf_path,
  128. cache_index={},
  129. )
  130. self.assertEqual(metadata["general.architecture"], "gemma4")
  131. self.assertEqual(metadata["general.name"], "Streaming Model")
  132. def test_skips_mmproj_when_filename_contains_mmproj_not_only_prefix(self):
  133. with tempfile.TemporaryDirectory() as tmpdir:
  134. root = Path(tmpdir)
  135. models_root = root / "models"
  136. output_root = root / "hub" / "models"
  137. gguf_path = (
  138. models_root
  139. / "publisher-a"
  140. / "Vision-Model-GGUF"
  141. / "Vision-Model-it.mmproj-Q8_0.gguf"
  142. )
  143. self._write_gguf_metadata(
  144. gguf_path,
  145. {
  146. "general.architecture": "qwen3vl",
  147. "general.name": "Vision Model",
  148. "general.basename": "Vision Model",
  149. "tokenizer.chat_template": "<|vision_start|><|image_pad|><|vision_end|>",
  150. },
  151. )
  152. summary = gen.generate_virtual_models_for_directory(
  153. models_root=models_root,
  154. output_root=output_root,
  155. dry_run=True,
  156. )
  157. self.assertEqual(summary["generated"], [])
  158. self.assertEqual(len(summary["skipped"]), 1)
  159. self.assertEqual(summary["skipped"][0]["reason"], "mmproj")
  160. def test_sibling_mmproj_marks_model_as_vision_capable(self):
  161. with tempfile.TemporaryDirectory() as tmpdir:
  162. root = Path(tmpdir)
  163. models_root = root / "models"
  164. output_root = root / "hub" / "models"
  165. model_dir = models_root / "publisher-a" / "Gemma4-Model-GGUF"
  166. self._write_gguf_metadata(
  167. model_dir / "Gemma4-Model-Q4_K_P.gguf",
  168. {
  169. "general.architecture": "gemma4",
  170. "general.name": "Gemma4 Model",
  171. "general.basename": "Gemma4 Model",
  172. "tokenizer.chat_template": "{% if enable_thinking is defined and enable_thinking is false %}{{ '' }}{% endif %}",
  173. "general.context_length": "262144",
  174. },
  175. )
  176. self._write_gguf_metadata(
  177. model_dir / "mmproj-Gemma4-Model-f16.gguf",
  178. {
  179. "general.architecture": "clip",
  180. "general.name": "Gemma4 mmproj",
  181. "general.basename": "Gemma4 mmproj",
  182. "tokenizer.chat_template": "",
  183. },
  184. )
  185. summary = gen.generate_virtual_models_for_directory(
  186. models_root=models_root,
  187. output_root=output_root,
  188. dry_run=True,
  189. )
  190. self.assertEqual(len(summary["generated"]), 1)
  191. generated = summary["generated"][0]
  192. self.assertTrue(generated["capabilities"]["vision"])
  193. def test_dry_run_scans_models_root_and_skips_lmstudio_community(self):
  194. with tempfile.TemporaryDirectory() as tmpdir:
  195. root = Path(tmpdir)
  196. models_root = root / "models"
  197. output_root = root / "hub" / "models"
  198. self._write_gguf_metadata(
  199. models_root / "publisher-a" / "Fancy-Model-GGUF" / "model.gguf",
  200. {
  201. "general.architecture": "qwen35",
  202. "general.name": "Fancy Model",
  203. "general.basename": "Fancy Model",
  204. "tokenizer.chat_template": "<tools>{% if enable_thinking is defined and enable_thinking is false %}{{ '<think>\\n\\n</think>\\n\\n' }}{% else %}{{ '<think>\\n' }}{% endif %}<|vision_start|><|image_pad|><|vision_end|>",
  205. "qwen2.context_length": "262144",
  206. },
  207. )
  208. self._write_gguf_metadata(
  209. models_root / "lmstudio-community" / "Qwen3.5-9B-GGUF" / "model.gguf",
  210. {
  211. "general.architecture": "qwen35",
  212. "general.name": "Qwen3.5-9B",
  213. "general.basename": "Qwen3.5-9B",
  214. "tokenizer.chat_template": "{{ '<think>\\n' }}",
  215. "qwen2.context_length": "262144",
  216. },
  217. )
  218. summary = gen.generate_virtual_models_for_directory(
  219. models_root=models_root,
  220. output_root=output_root,
  221. dry_run=True,
  222. )
  223. self.assertTrue(summary["dry_run"])
  224. self.assertEqual(len(summary["generated"]), 1)
  225. self.assertEqual(len(summary["skipped"]), 1)
  226. generated = summary["generated"][0]
  227. self.assertEqual(generated["publisher"], "publisher-a")
  228. self.assertEqual(generated["model_key"], "publisher-a/Fancy-Model-GGUF/model.gguf")
  229. self.assertEqual(generated["output_dir"], str(output_root / "publisher-a" / "fancy-model"))
  230. self.assertTrue(generated["capabilities"]["reasoning"])
  231. self.assertTrue(generated["capabilities"]["vision"])
  232. self.assertTrue(generated["capabilities"]["tools"])
  233. skipped = summary["skipped"][0]
  234. self.assertEqual(skipped["reason"], "lmstudio-community")
  235. def test_generate_virtual_model_writes_official_style_files(self):
  236. with tempfile.TemporaryDirectory() as tmpdir:
  237. root = Path(tmpdir)
  238. models_root = root / "models"
  239. output_root = root / "hub" / "models"
  240. model_index_cache = root / ".internal" / "model-index-cache.json"
  241. model_index_cache.parent.mkdir(parents=True, exist_ok=True)
  242. model_index_cache.write_text("cached", encoding="utf-8")
  243. self._write_gguf_metadata(
  244. models_root / "publisher-b" / "Fancy-Moe-Model-GGUF" / "Fancy-Moe-Model-Q4_K_M.gguf",
  245. {
  246. "general.architecture": "qwen35moe",
  247. "general.name": "Fancy Moe Model",
  248. "general.basename": "Fancy Moe Model",
  249. "tokenizer.chat_template": (
  250. "{%- if tools %}<tools>{%- endif %}"
  251. "{%- if add_generation_prompt %}"
  252. "{%- if enable_thinking is defined and enable_thinking is false %}"
  253. "{{- '<think>\\n\\n</think>\\n\\n' }}"
  254. "{%- else %}{{- '<think>\\n' }}{%- endif %}{%- endif %}"
  255. "<|vision_start|><|image_pad|><|vision_end|>"
  256. ),
  257. "qwen2.context_length": "262144",
  258. },
  259. )
  260. summary = gen.generate_virtual_models_for_directory(
  261. models_root=models_root,
  262. output_root=output_root,
  263. dry_run=False,
  264. model_index_cache_path=model_index_cache,
  265. )
  266. self.assertEqual(len(summary["generated"]), 1)
  267. output_dir = output_root / "publisher-b" / "fancy-moe-model"
  268. self.assertTrue(output_dir.exists())
  269. model_yaml = (output_dir / "model.yaml").read_text(encoding="utf-8")
  270. manifest = json.loads((output_dir / "manifest.json").read_text(encoding="utf-8"))
  271. self.assertIn("model: publisher-b/fancy-moe-model", model_yaml)
  272. self.assertIn("base:", model_yaml)
  273. self.assertIn("key: publisher-b/Fancy-Moe-Model-GGUF/Fancy-Moe-Model-Q4_K_M.gguf", model_yaml)
  274. self.assertIn("sources:", model_yaml)
  275. self.assertIn("reasoning: true", model_yaml)
  276. self.assertIn("vision: true", model_yaml)
  277. self.assertIn("trainedForToolUse: true", model_yaml)
  278. self.assertIn("key: enableThinking", model_yaml)
  279. self.assertIn("key: preserveThinking", model_yaml)
  280. self.assertEqual(manifest["owner"], "publisher-b")
  281. self.assertEqual(manifest["name"], "fancy-moe-model")
  282. self.assertEqual(
  283. manifest["dependencies"][0]["modelKeys"],
  284. ["publisher-b/Fancy-Moe-Model-GGUF/Fancy-Moe-Model-Q4_K_M.gguf"],
  285. )
  286. self.assertIn("sources", manifest["dependencies"][0])
  287. self.assertEqual(manifest["dependencies"][0]["sources"], [])
  288. def test_dry_run_does_not_delete_model_index_cache(self):
  289. with tempfile.TemporaryDirectory() as tmpdir:
  290. root = Path(tmpdir)
  291. models_root = root / "models"
  292. output_root = root / "hub" / "models"
  293. model_index_cache = root / ".internal" / "model-index-cache.json"
  294. model_index_cache.parent.mkdir(parents=True, exist_ok=True)
  295. model_index_cache.write_text("cached", encoding="utf-8")
  296. self._write_gguf_metadata(
  297. models_root / "publisher-a" / "Fancy-Model-GGUF" / "model.gguf",
  298. {
  299. "general.architecture": "qwen35",
  300. "general.name": "Fancy Model",
  301. "general.basename": "Fancy Model",
  302. "tokenizer.chat_template": "{% if enable_thinking is defined and enable_thinking is false %}{{ '' }}{% endif %}",
  303. "qwen2.context_length": "262144",
  304. },
  305. )
  306. summary = gen.generate_virtual_models_for_directory(
  307. models_root=models_root,
  308. output_root=output_root,
  309. dry_run=True,
  310. model_index_cache_path=model_index_cache,
  311. )
  312. self.assertFalse(summary["deleted_model_index_cache"])
  313. self.assertTrue(model_index_cache.exists())
  314. def test_generation_deletes_internal_model_index_cache_after_write(self):
  315. with tempfile.TemporaryDirectory() as tmpdir:
  316. root = Path(tmpdir)
  317. models_root = root / "models"
  318. output_root = root / "hub" / "models"
  319. model_index_cache = root / ".internal" / "model-index-cache.json"
  320. model_index_cache.parent.mkdir(parents=True, exist_ok=True)
  321. model_index_cache.write_text("cached", encoding="utf-8")
  322. self._write_gguf_metadata(
  323. models_root / "publisher-a" / "Fancy-Model-GGUF" / "model.gguf",
  324. {
  325. "general.architecture": "qwen35",
  326. "general.name": "Fancy Model",
  327. "general.basename": "Fancy Model",
  328. "tokenizer.chat_template": "{% if enable_thinking is defined and enable_thinking is false %}{{ '' }}{% endif %}",
  329. "qwen2.context_length": "262144",
  330. },
  331. )
  332. summary = gen.generate_virtual_models_for_directory(
  333. models_root=models_root,
  334. output_root=output_root,
  335. dry_run=False,
  336. model_index_cache_path=model_index_cache,
  337. )
  338. self.assertTrue(summary["deleted_model_index_cache"])
  339. self.assertEqual(summary["model_index_cache_path"], str(model_index_cache))
  340. self.assertFalse(model_index_cache.exists())
  341. def test_owner_is_normalized_for_lmstudio_manifest_and_model_id(self):
  342. metadata = {
  343. "general.architecture": "qwen35moe",
  344. "general.name": "Fancy Moe Model",
  345. "general.basename": "Fancy Moe Model",
  346. "tokenizer.chat_template": (
  347. "{% if enable_thinking is defined and enable_thinking is false %}"
  348. "{{ '<think>\\n\\n</think>\\n\\n' }}"
  349. "{% endif %}"
  350. ),
  351. "qwen2.context_length": "262144",
  352. }
  353. profile = gen.build_virtual_model_profile(metadata)
  354. files = gen.build_virtual_model_files(
  355. publisher="HauhauCS",
  356. slug="fancy-moe-model",
  357. model_key="HauhauCS/Fancy-Moe-Model-GGUF/Fancy-Moe-Model-Q4_K_M.gguf",
  358. profile=profile,
  359. )
  360. manifest = json.loads(files["manifest.json"])
  361. self.assertIn("model: hauhaucs/fancy-moe-model", files["model.yaml"])
  362. self.assertEqual(manifest["owner"], "hauhaucs")
  363. self.assertEqual(manifest["dependencies"][0]["sources"], [])
  364. def test_collect_job_uses_normalized_owner_for_output_dir_and_model_id(self):
  365. with tempfile.TemporaryDirectory() as tmpdir:
  366. root = Path(tmpdir)
  367. models_root = root / "models"
  368. output_root = root / "hub" / "models"
  369. gguf_path = (
  370. models_root
  371. / "HauhauCS"
  372. / "Fancy-Moe-Model-GGUF"
  373. / "Fancy-Moe-Model-Q4_K_M.gguf"
  374. )
  375. self._write_gguf_metadata(
  376. gguf_path,
  377. {
  378. "general.architecture": "qwen35moe",
  379. "general.name": "Fancy Moe Model",
  380. "general.basename": "Fancy Moe Model",
  381. "tokenizer.chat_template": (
  382. "{% if enable_thinking is defined and enable_thinking is false %}"
  383. "{{ '<think>\\n\\n</think>\\n\\n' }}"
  384. "{% endif %}"
  385. ),
  386. "qwen2.context_length": "262144",
  387. },
  388. )
  389. job = gen.collect_virtual_model_job(
  390. models_root=models_root,
  391. output_root=output_root,
  392. gguf_path=gguf_path,
  393. cache_index={},
  394. )
  395. self.assertEqual(job["publisher"], "hauhaucs")
  396. self.assertEqual(
  397. job["output_dir"],
  398. str(output_root / "hauhaucs" / "fancy-moe-model"),
  399. )
  400. def test_nothink_variant_disables_reasoning_and_custom_field(self):
  401. metadata = {
  402. "general.architecture": "qwen35",
  403. "general.name": "Generic Model NoThink",
  404. "general.basename": "Generic Model",
  405. "tokenizer.chat_template": "<tools><|vision_start|><|image_pad|><|vision_end|>",
  406. "qwen2.context_length": "262144",
  407. }
  408. profile = gen.build_virtual_model_profile(metadata)
  409. files = gen.build_virtual_model_files(
  410. publisher="publisher-c",
  411. slug="generic-model-nothink",
  412. model_key="publisher-c/Generic-Model-GGUF/Generic-Model-NoThink-Q4_K_M.gguf",
  413. profile=profile,
  414. )
  415. self.assertIn("reasoning: false", files["model.yaml"])
  416. self.assertNotIn("key: enableThinking", files["model.yaml"])
  417. self.assertNotIn("key: preserveThinking", files["model.yaml"])
  418. def test_nothink_directory_forces_reasoning_off_even_if_template_contains_think(self):
  419. with tempfile.TemporaryDirectory() as tmpdir:
  420. root = Path(tmpdir)
  421. models_root = root / "models"
  422. output_root = root / "hub" / "models"
  423. model_index_cache = root / ".internal" / "model-index-cache.json"
  424. model_index_cache.parent.mkdir(parents=True, exist_ok=True)
  425. model_index_cache.write_text("cached", encoding="utf-8")
  426. gguf_path = (
  427. models_root
  428. / "publisher-b"
  429. / "Generic-Model-NoThink-GGUF"
  430. / "Generic-Model-NoThink-Q4_K_M.gguf"
  431. )
  432. self._write_gguf_metadata(
  433. gguf_path,
  434. {
  435. "general.architecture": "qwen35",
  436. "general.name": "Generic Model NoThink",
  437. "general.basename": "Generic Model",
  438. "tokenizer.chat_template": "{{ '<think>\\n\\n</think>\\n\\n' }}<tools>",
  439. "qwen2.context_length": "262144",
  440. },
  441. )
  442. summary = gen.generate_virtual_models_for_directory(
  443. models_root=models_root,
  444. output_root=output_root,
  445. dry_run=False,
  446. model_index_cache_path=model_index_cache,
  447. )
  448. self.assertEqual(len(summary["generated"]), 1)
  449. output_dir = output_root / "publisher-b" / "generic-model-nothink"
  450. model_yaml = (output_dir / "model.yaml").read_text(encoding="utf-8")
  451. self.assertIn("reasoning: false", model_yaml)
  452. self.assertNotIn("key: enableThinking", model_yaml)
  453. def test_static_think_block_without_enable_thinking_is_not_reasoning_capable(self):
  454. metadata = {
  455. "general.architecture": "qwen35",
  456. "general.name": "Static Think Model",
  457. "general.basename": "Static Think Model",
  458. "tokenizer.chat_template": "{{ '<think>\\n\\n</think>\\n\\n' }}<tools>",
  459. "qwen2.context_length": "262144",
  460. }
  461. profile = gen.build_virtual_model_profile(metadata)
  462. self.assertFalse(profile["capabilities"]["reasoning"])
  463. self.assertFalse(profile["needs_enable_thinking"])
  464. def test_gemma4_pipe_style_tool_tokens_are_detected(self):
  465. metadata = {
  466. "general.architecture": "gemma4",
  467. "general.name": "Gemma4 Tool Model",
  468. "general.basename": "Gemma4 Tool Model",
  469. "tokenizer.chat_template": (
  470. "{% if tools %}{{ '<|tool>' }}{% endif %}"
  471. "{{ '<|tool_call>' }}"
  472. "{{ '<|tool_response>' }}"
  473. ),
  474. "general.context_length": "262144",
  475. }
  476. profile = gen.build_virtual_model_profile(metadata)
  477. self.assertTrue(profile["capabilities"]["tools"])
  478. def test_profile_inference_uses_metadata_and_directory_augments_vision(self):
  479. metadata = {
  480. "general.architecture": "gemma4",
  481. "general.name": "Augmented Vision Model",
  482. "general.basename": "Augmented Vision Model",
  483. "tokenizer.chat_template": "{% if enable_thinking is defined and enable_thinking is false %}{{ '' }}{% endif %}",
  484. "general.context_length": "262144",
  485. }
  486. profile_without_mmproj = gen.build_virtual_model_profile(metadata)
  487. profile_with_mmproj = gen.build_virtual_model_profile(
  488. metadata,
  489. has_sibling_mmproj=True,
  490. )
  491. self.assertFalse(profile_without_mmproj["capabilities"]["vision"])
  492. self.assertTrue(profile_with_mmproj["capabilities"]["vision"])
  493. self.assertEqual(
  494. profile_without_mmproj["capabilities"]["tools"],
  495. profile_with_mmproj["capabilities"]["tools"],
  496. )
  497. if __name__ == "__main__":
  498. unittest.main()