annotuneagent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. annotuneagent/__init__.py +68 -0
  2. annotuneagent/__main__.py +6 -0
  3. annotuneagent/_version.py +19 -0
  4. annotuneagent/annotate.py +93 -0
  5. annotuneagent/arbitrate.py +141 -0
  6. annotuneagent/cli.py +366 -0
  7. annotuneagent/decide.py +725 -0
  8. annotuneagent/evaluate.py +121 -0
  9. annotuneagent/evidence.py +606 -0
  10. annotuneagent/granularity/__init__.py +28 -0
  11. annotuneagent/granularity/cl_dag.py +179 -0
  12. annotuneagent/granularity/ontology_map.py +108 -0
  13. annotuneagent/granularity/relations.py +96 -0
  14. annotuneagent/hypotheses.py +38 -0
  15. annotuneagent/local_lora.py +706 -0
  16. annotuneagent/marker_plot.py +172 -0
  17. annotuneagent/markers.py +206 -0
  18. annotuneagent/markers.tsv +378 -0
  19. annotuneagent/mltools.py +128 -0
  20. annotuneagent/ontology/cl_isa.json +1 -0
  21. annotuneagent/ontology/granularity_policy.yaml +8 -0
  22. annotuneagent/ontology/ontology_map.jsonl +198 -0
  23. annotuneagent/prepare.py +1385 -0
  24. annotuneagent/py.typed +0 -0
  25. annotuneagent/read.py +76 -0
  26. annotuneagent/tokenizer/LICENSE +202 -0
  27. annotuneagent/tokenizer/merges.txt +247587 -0
  28. annotuneagent/tokenizer/tokenizer.json +495920 -0
  29. annotuneagent/tokenizer/tokenizer_config.json +305 -0
  30. annotuneagent/tokenizer/vocab.json +248046 -0
  31. annotuneagent/votes.py +83 -0
  32. annotuneagent/workflow.py +2037 -0
  33. annotuneagent-0.1.0.dist-info/METADATA +208 -0
  34. annotuneagent-0.1.0.dist-info/RECORD +38 -0
  35. annotuneagent-0.1.0.dist-info/WHEEL +5 -0
  36. annotuneagent-0.1.0.dist-info/entry_points.txt +2 -0
  37. annotuneagent-0.1.0.dist-info/licenses/LICENSE +201 -0
  38. annotuneagent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,706 @@
1
+ # Copyright 2026 Zhao Li (李钊)
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ """One Q6_K base, two LoRA adapters, one enabled per request.
5
+
6
+ llama-server loads name and ml together and leaves them off.
7
+ A request sets that task's scale to 1 and the other to 0. Sequences
8
+ past ``maxlen`` raise. They are not truncated.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import getpass
14
+ import json
15
+ import os
16
+ import shutil
17
+ import stat
18
+ import subprocess
19
+ import tempfile
20
+ import time
21
+ import urllib.error
22
+ import urllib.parse
23
+ import urllib.request
24
+ from pathlib import Path
25
+
26
+ _TASKS = ("name", "ml")
27
+ # One private directory per user, so two users never share a state file or a log.
28
+ # os.getuid does not exist on Windows. The package must still import there.
29
+ _USER = str(os.getuid()) if hasattr(os, "getuid") else getpass.getuser()
30
+ _RUN_DIR = (
31
+ Path(os.environ["XDG_RUNTIME_DIR"]) / "annotune"
32
+ if os.environ.get("XDG_RUNTIME_DIR")
33
+ else Path(tempfile.gettempdir()) / f"annotune-{_USER}"
34
+ )
35
+ _STATE = _RUN_DIR / "llama-server.json"
36
+ _LOG = _RUN_DIR / "llama-server.log"
37
+ _DEFAULT_SERVICE = "http://127.0.0.1:8080"
38
+ _SERVER: dict | None = None
39
+ _LAST: dict = {}
40
+
41
+
42
+ class _Running:
43
+ """A llama-server started by another process. ``poll`` is None while it lives."""
44
+
45
+ def __init__(self, pid: int) -> None:
46
+ self.pid = pid
47
+
48
+ def poll(self) -> int | None:
49
+ return None if _alive(self.pid) else 1
50
+
51
+ def wait(self, timeout: float | None = None) -> int:
52
+ deadline = None if timeout is None else time.time() + timeout
53
+ while _alive(self.pid):
54
+ if deadline is not None and time.time() > deadline:
55
+ raise subprocess.TimeoutExpired(cmd="llama-server", timeout=timeout)
56
+ time.sleep(0.2)
57
+ return 0
58
+
59
+
60
+ class _Remote:
61
+ """An HTTP server this process did not start."""
62
+
63
+ def __init__(self, url: str) -> None:
64
+ self.url = url
65
+ self.pid = -1
66
+
67
+ def poll(self) -> int | None:
68
+ return None if _probe(self.url) is not None else 1
69
+
70
+ def wait(self, timeout: float | None = None) -> int:
71
+ raise subprocess.TimeoutExpired(cmd=self.url, timeout=timeout)
72
+
73
+
74
+ def _alive(pid: int) -> bool:
75
+ try:
76
+ os.kill(pid, 0)
77
+ except ProcessLookupError:
78
+ return False
79
+ except PermissionError:
80
+ return True
81
+ return True
82
+
83
+
84
+ class SequenceTooLong(RuntimeError):
85
+ def __init__(self, tokens: int, maxlen: int):
86
+ super().__init__(f"sequence is {tokens} tokens, MAXLEN={maxlen}; do not truncate")
87
+ self.tokens = tokens
88
+ self.maxlen = maxlen
89
+
90
+
91
+ def scales_for(task: str) -> list[dict[str, float]]:
92
+ """Enable one adapter. The other stays at scale 0 for this request."""
93
+ if task not in _TASKS:
94
+ raise ValueError(f"unknown task {task}")
95
+ return [
96
+ {"id": index, "scale": 1.0 if name == task else 0.0} for index, name in enumerate(_TASKS)
97
+ ]
98
+
99
+
100
+ def last_call() -> dict:
101
+ return dict(_LAST)
102
+
103
+
104
+ def packaged_tokenizer() -> str:
105
+ """Qwen tokenizer shipped with the package."""
106
+ return str(Path(__file__).resolve().parent / "tokenizer")
107
+
108
+
109
+ def _tokenizer_dir(path: str | None) -> str:
110
+ return packaged_tokenizer() if path is None else str(path)
111
+
112
+
113
+ def service_url() -> str:
114
+ """Server root. ``ANNOTUNE_URL`` overrides ``http://127.0.0.1:8080``."""
115
+ return _base_url(os.environ.get("ANNOTUNE_URL", _DEFAULT_SERVICE))
116
+
117
+
118
+ def attach(url: str | None = None, *, tokenizer: str | None = None) -> str:
119
+ """Use a server that is already running. Does not load weights."""
120
+ target = _base_url(url) if url else service_url()
121
+ tokenizer = _tokenizer_dir(tokenizer)
122
+ if _server_live() and _SERVER is not None and _SERVER.get("url") == target:
123
+ _SERVER["tokenizer"] = tokenizer
124
+ _SERVER["tok"] = None
125
+ return target
126
+ if _server_live() and _SERVER is not None and _SERVER.get("owned"):
127
+ raise RuntimeError(
128
+ f"this process already started {_SERVER['url']}; release() before using {target}"
129
+ )
130
+ _clear_client()
131
+ if not _use_existing(target, tokenizer):
132
+ raise RuntimeError(f"server is not ready: {target}")
133
+ return target
134
+
135
+
136
+ def open_adapters(
137
+ adapters: dict[str, str],
138
+ *,
139
+ gguf: str,
140
+ tokenizer: str | None = None,
141
+ ctx: int = 8192,
142
+ url: str | None = None,
143
+ ) -> None:
144
+ """Use the server at ``url``, or load one base and two adapters there.
145
+
146
+ A server that is already up is reused. This does not start a second one.
147
+ """
148
+ global _SERVER
149
+ target = _base_url(url) if url else service_url()
150
+ tokenizer = _tokenizer_dir(tokenizer)
151
+ missing = [name for name in _TASKS if not adapters.get(name)]
152
+ if missing:
153
+ raise ValueError(f"shared base needs adapters for {', '.join(missing)}")
154
+ paths = {name: str(Path(adapters[name]).resolve()) for name in _TASKS}
155
+ base = str(Path(gguf).resolve())
156
+ if _server_live() and _SERVER.get("url") == target:
157
+ _require_matching_model(_SERVER, base, paths, ctx)
158
+ if _SERVER["tokenizer"] != tokenizer:
159
+ raise RuntimeError("server already uses a different tokenizer")
160
+ return
161
+ if _server_live() and _SERVER.get("owned"):
162
+ raise RuntimeError(
163
+ f"this process already started {_SERVER['url']}; release() before using {target}"
164
+ )
165
+ _clear_client()
166
+ if _use_existing(target, tokenizer, gguf=base, adapters=paths, ctx=ctx):
167
+ return
168
+ other = _other_local(target)
169
+ if other is not None:
170
+ raise RuntimeError(
171
+ f"a model is already loaded at {other}; not loading another. "
172
+ f"Service address is {target}."
173
+ )
174
+ host, port = _split_host(target)
175
+ if host not in {"127.0.0.1", "localhost"}:
176
+ raise RuntimeError(f"server is not ready: {target}")
177
+ base = _gguf_path(base)
178
+ binary = _binary()
179
+ _private_dir(_LOG.parent)
180
+ log = _LOG.open("w")
181
+ proc = subprocess.Popen(
182
+ [
183
+ binary,
184
+ "-m",
185
+ base,
186
+ "--lora",
187
+ ",".join(paths[name] for name in _TASKS),
188
+ "--lora-init-without-apply",
189
+ "-c",
190
+ str(ctx),
191
+ "-ngl",
192
+ "99",
193
+ "-np",
194
+ "1",
195
+ "--no-cont-batching",
196
+ "--no-cache-prompt",
197
+ "--slot-prompt-similarity",
198
+ "0",
199
+ "--no-jinja",
200
+ "--host",
201
+ host,
202
+ "--port",
203
+ str(port),
204
+ ],
205
+ stdout=log,
206
+ stderr=subprocess.STDOUT,
207
+ env=_child_env(binary),
208
+ start_new_session=True,
209
+ )
210
+ _SERVER = _runtime(
211
+ proc,
212
+ url=target,
213
+ port=port,
214
+ gguf=base,
215
+ adapters=paths,
216
+ ctx=ctx,
217
+ tokenizer=tokenizer,
218
+ mode="llama",
219
+ owned=True,
220
+ log=log,
221
+ )
222
+ try:
223
+ _wait_ready(proc, target, _LOG)
224
+ except Exception:
225
+ release()
226
+ raise
227
+ _write_state()
228
+
229
+
230
+ def complete(task: str, system: str, payload: dict | str, *, maxlen: int) -> str:
231
+ """Run one greedy request with only ``task`` applied."""
232
+ if task not in _TASKS:
233
+ raise ValueError(f"unknown task {task}")
234
+ if not _server_live():
235
+ raise RuntimeError("shared base is not loaded")
236
+ from .decide import user_text
237
+
238
+ user = user_text(task, payload)
239
+ prompt = _render(system, user)
240
+ tokens = len(_tokenizer()(prompt, add_special_tokens=False).input_ids)
241
+ if tokens + 32 > maxlen:
242
+ raise SequenceTooLong(tokens, maxlen)
243
+ runtime = _SERVER
244
+ if runtime.get("mode") == "chat":
245
+ from .decide import post_chat
246
+
247
+ text = str(post_chat(runtime["url"], system, payload, 180, task=task)).strip()
248
+ scales: list[dict[str, float]] = []
249
+ evaluated = None
250
+ predicted = None
251
+ else:
252
+ scales = scales_for(task)
253
+ raw = _post(
254
+ "/completion",
255
+ {
256
+ "prompt": prompt,
257
+ "n_predict": 32,
258
+ "temperature": 0,
259
+ "top_k": 1,
260
+ "top_p": 1,
261
+ "min_p": 0,
262
+ "cache_prompt": False,
263
+ "lora": scales,
264
+ },
265
+ )
266
+ text = str(raw.get("content") or "").strip()
267
+ evaluated = raw.get("tokens_evaluated")
268
+ predicted = raw.get("tokens_predicted")
269
+ global _LAST
270
+ _LAST = {
271
+ "task": task,
272
+ "lora": scales,
273
+ "tokens": tokens,
274
+ "tokens_evaluated": evaluated,
275
+ "tokens_predicted": predicted,
276
+ }
277
+ return text
278
+
279
+
280
+ def local_server() -> str | None:
281
+ """Base URL of a loaded server. None when nothing is running."""
282
+ if _server_live():
283
+ return str(_SERVER.get("url") or "")
284
+ record = _read_state()
285
+ if record is not None and _alive(int(record.get("pid", -1))):
286
+ url = str(record.get("url") or f"http://127.0.0.1:{int(record['port'])}")
287
+ if _health_ok(url):
288
+ return url
289
+ found = _loaded_servers()
290
+ if len(found) == 1:
291
+ return str(found[0]["url"])
292
+ return None
293
+
294
+
295
+ def release() -> None:
296
+ """Stop a server this process started."""
297
+ global _SERVER
298
+ runtime = _SERVER
299
+ _SERVER = None
300
+ if runtime is None:
301
+ return
302
+ proc = runtime["proc"]
303
+ log = runtime.get("log")
304
+ if runtime.get("owned") and proc.poll() is None and getattr(proc, "pid", 0) > 0:
305
+ try:
306
+ os.killpg(proc.pid, 15)
307
+ except (ProcessLookupError, PermissionError):
308
+ pass
309
+ try:
310
+ proc.wait(timeout=10)
311
+ except subprocess.TimeoutExpired:
312
+ try:
313
+ os.killpg(proc.pid, 9)
314
+ except ProcessLookupError:
315
+ pass
316
+ proc.wait(timeout=5)
317
+ if log is not None:
318
+ log.close()
319
+ if not runtime.get("owned"):
320
+ return
321
+ record = _read_state()
322
+ pid = getattr(proc, "pid", None)
323
+ if record is None or int(record.get("pid", -2)) == pid:
324
+ _STATE.unlink(missing_ok=True)
325
+
326
+
327
+ def _private_dir(path: Path) -> None:
328
+ """Create ``path`` for this user alone. Refuse one that someone else made or can use."""
329
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
330
+ info = path.lstat()
331
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
332
+ raise RuntimeError(f"{path} must be a plain directory")
333
+ if hasattr(os, "getuid") and (info.st_uid != os.getuid() or info.st_mode & 0o077):
334
+ raise RuntimeError(f"{path} is not private to this user; remove it or set XDG_RUNTIME_DIR")
335
+
336
+
337
+ def _read_state() -> dict | None:
338
+ if _STATE.parent.exists():
339
+ _private_dir(_STATE.parent)
340
+ try:
341
+ record = json.loads(_STATE.read_text())
342
+ except (OSError, json.JSONDecodeError):
343
+ return None
344
+ return record if isinstance(record, dict) else None
345
+
346
+
347
+ def _health_ok(url: str) -> bool:
348
+ try:
349
+ with urllib.request.urlopen(f"{url}/health", timeout=2) as resp:
350
+ payload = json.loads(resp.read().decode())
351
+ except (OSError, urllib.error.URLError, json.JSONDecodeError, TimeoutError):
352
+ return False
353
+ return isinstance(payload, dict) and payload.get("status") == "ok"
354
+
355
+
356
+ def _models_ok(url: str) -> bool:
357
+ try:
358
+ with urllib.request.urlopen(f"{url}/v1/models", timeout=2) as resp:
359
+ return 200 <= getattr(resp, "status", 200) < 300
360
+ except (OSError, urllib.error.URLError, TimeoutError):
361
+ return False
362
+
363
+
364
+ def _probe(url: str) -> str | None:
365
+ """``llama`` for this server's health route, ``chat`` for an OpenAI-style API."""
366
+ if _health_ok(url):
367
+ return "llama"
368
+ if _models_ok(url):
369
+ return "chat"
370
+ return None
371
+
372
+
373
+ def _base_url(url: str) -> str:
374
+ text = url.strip().rstrip("/")
375
+ if "://" not in text:
376
+ text = "http://" + text
377
+ return text
378
+
379
+
380
+ def _split_host(url: str) -> tuple[str, int]:
381
+ parsed = urllib.parse.urlparse(url)
382
+ host = parsed.hostname or "127.0.0.1"
383
+ if parsed.port is not None:
384
+ return host, parsed.port
385
+ return host, 443 if parsed.scheme == "https" else 80
386
+
387
+
388
+ def _server_live() -> bool:
389
+ runtime = _SERVER
390
+ return runtime is not None and runtime["proc"].poll() is None
391
+
392
+
393
+ def _clear_client() -> None:
394
+ global _SERVER
395
+ runtime = _SERVER
396
+ _SERVER = None
397
+ if runtime is None:
398
+ return
399
+ log = runtime.get("log")
400
+ if log is not None:
401
+ log.close()
402
+
403
+
404
+ def _runtime(
405
+ proc,
406
+ *,
407
+ url: str,
408
+ port: int,
409
+ gguf: str,
410
+ adapters: dict[str, str],
411
+ ctx: int,
412
+ tokenizer: str,
413
+ mode: str,
414
+ owned: bool,
415
+ log,
416
+ ) -> dict:
417
+ return {
418
+ "proc": proc,
419
+ "port": port,
420
+ "log": log,
421
+ "log_path": _LOG,
422
+ "gguf": gguf,
423
+ "adapters": adapters,
424
+ "ctx": ctx,
425
+ "tokenizer": tokenizer,
426
+ "tok": None,
427
+ "owned": owned,
428
+ "url": url,
429
+ "mode": mode,
430
+ }
431
+
432
+
433
+ def _use_existing(
434
+ url: str,
435
+ tokenizer: str,
436
+ *,
437
+ gguf: str | None = None,
438
+ adapters: dict[str, str] | None = None,
439
+ ctx: int = 0,
440
+ ) -> bool:
441
+ """Attach to ``url`` when it is already serving. Does not start a process."""
442
+ mode = _probe(url)
443
+ if mode is None:
444
+ return False
445
+ loaded = _server_on(url) if mode == "llama" else None
446
+ if adapters is not None:
447
+ if loaded is None:
448
+ raise RuntimeError(
449
+ f"server at {url} is already running but its GGUF and adapters cannot be verified"
450
+ )
451
+ _require_matching_model(loaded, str(gguf), adapters, ctx)
452
+ if loaded is not None:
453
+ _connect_loaded(loaded, tokenizer)
454
+ else:
455
+ _connect_remote(url, tokenizer, mode)
456
+ print(f"server already loaded at {url}; not loading again", flush=True)
457
+ return True
458
+
459
+
460
+ def _require_matching_model(loaded: dict, gguf: str, adapters: dict[str, str], ctx: int) -> None:
461
+ if (
462
+ loaded.get("gguf") != gguf
463
+ or loaded.get("adapters") != adapters
464
+ or int(loaded.get("ctx") or 0) < ctx
465
+ ):
466
+ raise RuntimeError("server already runs different GGUF, adapters, or context")
467
+
468
+
469
+ def _connect_loaded(loaded: dict, tokenizer: str) -> None:
470
+ global _SERVER
471
+ _SERVER = _runtime(
472
+ _Running(int(loaded["pid"])),
473
+ url=str(loaded["url"]),
474
+ port=int(loaded["port"]),
475
+ gguf=str(loaded.get("gguf") or ""),
476
+ adapters=dict(loaded.get("adapters") or {}),
477
+ ctx=int(loaded.get("ctx") or 0),
478
+ tokenizer=tokenizer,
479
+ mode="llama",
480
+ owned=False,
481
+ log=None,
482
+ )
483
+ _write_state()
484
+
485
+
486
+ def _connect_remote(url: str, tokenizer: str, mode: str) -> None:
487
+ global _SERVER
488
+ _host, port = _split_host(url)
489
+ _SERVER = _runtime(
490
+ _Remote(url),
491
+ url=url,
492
+ port=port,
493
+ gguf="",
494
+ adapters={},
495
+ ctx=0,
496
+ tokenizer=tokenizer,
497
+ mode=mode,
498
+ owned=False,
499
+ log=None,
500
+ )
501
+
502
+
503
+ def _other_local(target: str) -> str | None:
504
+ for item in _loaded_servers():
505
+ if item["url"] != target:
506
+ return str(item["url"])
507
+ return None
508
+
509
+
510
+ def _server_on(url: str) -> dict | None:
511
+ for item in _loaded_servers():
512
+ if item["url"] == url:
513
+ return item
514
+ return None
515
+
516
+
517
+ def _loaded_servers() -> list[dict]:
518
+ """llama-server processes that are listening and healthy."""
519
+ root = Path("/proc")
520
+ if not root.is_dir():
521
+ return []
522
+ found = []
523
+ for entry in root.iterdir():
524
+ if not entry.name.isdigit():
525
+ continue
526
+ argv = _cmdline(int(entry.name))
527
+ if not argv or Path(argv[0]).name != "llama-server":
528
+ continue
529
+ port = _listen_port(int(entry.name))
530
+ if port is None:
531
+ continue
532
+ host = _arg(argv, "--host") or "127.0.0.1"
533
+ url = f"http://{host}:{port}"
534
+ if not _health_ok(url):
535
+ continue
536
+ lora = _arg(argv, "--lora") or ""
537
+ names = [part for part in lora.split(",") if part]
538
+ adapters = dict(zip(_TASKS, names, strict=True)) if len(names) == 2 else {}
539
+ ctx_text = _arg(argv, "-c") or _arg(argv, "--ctx-size")
540
+ found.append(
541
+ {
542
+ "pid": int(entry.name),
543
+ "port": port,
544
+ "url": url,
545
+ "gguf": _arg(argv, "-m") or _arg(argv, "--model") or "",
546
+ "adapters": adapters,
547
+ "ctx": int(ctx_text) if ctx_text and ctx_text.isdigit() else 0,
548
+ }
549
+ )
550
+ return found
551
+
552
+
553
+ def _cmdline(pid: int) -> list[str]:
554
+ try:
555
+ raw = Path(f"/proc/{pid}/cmdline").read_bytes()
556
+ except OSError:
557
+ return []
558
+ return [part.decode(errors="replace") for part in raw.split(b"\0") if part]
559
+
560
+
561
+ def _arg(argv: list[str], flag: str) -> str | None:
562
+ if flag not in argv:
563
+ return None
564
+ index = argv.index(flag)
565
+ if index + 1 >= len(argv):
566
+ return None
567
+ return argv[index + 1]
568
+
569
+
570
+ def _listen_port(pid: int) -> int | None:
571
+ inodes: set[str] = set()
572
+ try:
573
+ for entry in Path(f"/proc/{pid}/fd").iterdir():
574
+ try:
575
+ target = os.readlink(entry)
576
+ except OSError:
577
+ continue
578
+ if target.startswith("socket:[") and target.endswith("]"):
579
+ inodes.add(target[8:-1])
580
+ except OSError:
581
+ return None
582
+ if not inodes:
583
+ return None
584
+ for name in ("/proc/net/tcp", "/proc/net/tcp6"):
585
+ try:
586
+ lines = Path(name).read_text().splitlines()[1:]
587
+ except OSError:
588
+ continue
589
+ for line in lines:
590
+ fields = line.split()
591
+ if len(fields) < 10 or fields[3] != "0A" or fields[9] not in inodes:
592
+ continue
593
+ return int(fields[1].rsplit(":", 1)[-1], 16)
594
+ return None
595
+
596
+
597
+ def _write_state() -> None:
598
+ runtime = _SERVER
599
+ if runtime is None:
600
+ return
601
+ pid = getattr(runtime["proc"], "pid", -1)
602
+ if not isinstance(pid, int) or pid <= 0:
603
+ return
604
+ _private_dir(_STATE.parent)
605
+ _STATE.write_text(
606
+ json.dumps(
607
+ {
608
+ "pid": pid,
609
+ "port": runtime["port"],
610
+ "url": runtime["url"],
611
+ "gguf": runtime["gguf"],
612
+ "adapters": runtime["adapters"],
613
+ "ctx": runtime["ctx"],
614
+ "tokenizer": runtime["tokenizer"],
615
+ }
616
+ )
617
+ )
618
+
619
+
620
+ def _gguf_path(gguf: str) -> str:
621
+ path = Path(gguf)
622
+ if not path.is_file():
623
+ raise RuntimeError(f"Q6_K base is missing: {path}")
624
+ return str(path)
625
+
626
+
627
+ def _binary() -> str:
628
+ env = os.environ.get("LLAMA_SERVER")
629
+ found = env or shutil.which("llama-server")
630
+ if not found or not Path(found).is_file():
631
+ raise RuntimeError("llama-server is not on PATH; set LLAMA_SERVER")
632
+ return str(Path(found).resolve())
633
+
634
+
635
+ def _child_env(binary: str) -> dict[str, str]:
636
+ env = os.environ.copy()
637
+ root = Path(binary).resolve().parent
638
+ libs = [str(root)]
639
+ parent = root.parent
640
+ libs.extend(str(path) for path in parent.glob("cudart-*") if path.is_dir())
641
+ previous = env.get("LD_LIBRARY_PATH")
642
+ if previous:
643
+ libs.append(previous)
644
+ env["LD_LIBRARY_PATH"] = ":".join(libs)
645
+ return env
646
+
647
+
648
+ def _wait_ready(proc: subprocess.Popen, url: str, log_path: Path) -> None:
649
+ deadline = time.time() + 180
650
+ while time.time() < deadline:
651
+ if proc.poll() is not None:
652
+ raise RuntimeError(f"llama-server exited {proc.returncode}\n{_tail(log_path)}")
653
+ if _health_ok(url):
654
+ return
655
+ time.sleep(0.4)
656
+ raise RuntimeError(f"llama-server did not become ready\n{_tail(log_path)}")
657
+
658
+
659
+ def _tail(path: Path) -> str:
660
+ try:
661
+ lines = path.read_text(errors="replace").splitlines()
662
+ except OSError:
663
+ return ""
664
+ return "\n".join(lines[-40:])
665
+
666
+
667
+ def _tokenizer():
668
+ runtime = _SERVER
669
+ if runtime is None:
670
+ raise RuntimeError("shared base is not loaded")
671
+ if runtime["tok"] is None:
672
+ from transformers import AutoTokenizer
673
+
674
+ runtime["tok"] = AutoTokenizer.from_pretrained(runtime["tokenizer"])
675
+ return runtime["tok"]
676
+
677
+
678
+ def _render(system: str, user: str) -> str:
679
+ return _tokenizer().apply_chat_template(
680
+ [{"role": "system", "content": system}, {"role": "user", "content": user}],
681
+ tokenize=False,
682
+ add_generation_prompt=True,
683
+ enable_thinking=False,
684
+ )
685
+
686
+
687
+ def _post(path: str, payload: dict) -> dict:
688
+ runtime = _SERVER
689
+ if runtime is None:
690
+ raise RuntimeError("shared base is not loaded")
691
+ data = json.dumps(payload).encode()
692
+ request = urllib.request.Request(
693
+ f"{runtime['url']}{path}",
694
+ data=data,
695
+ headers={"Content-Type": "application/json"},
696
+ method="POST",
697
+ )
698
+ try:
699
+ with urllib.request.urlopen(request, timeout=180) as resp:
700
+ body = json.loads(resp.read().decode())
701
+ except urllib.error.HTTPError as exc:
702
+ detail = exc.read().decode(errors="replace")
703
+ raise RuntimeError(f"llama-server {exc.code}: {detail}") from exc
704
+ if not isinstance(body, dict):
705
+ raise RuntimeError("llama-server reply is not an object")
706
+ return body