coderouter-cli 2.7.5__py3-none-any.whl → 2.7.6__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.
- coderouter/gguf_introspect.py +19 -0
- coderouter/ingress/launcher_routes.py +215 -20
- coderouter/launcher_speculative.py +333 -0
- {coderouter_cli-2.7.5.dist-info → coderouter_cli-2.7.6.dist-info}/METADATA +1 -1
- {coderouter_cli-2.7.5.dist-info → coderouter_cli-2.7.6.dist-info}/RECORD +8 -7
- {coderouter_cli-2.7.5.dist-info → coderouter_cli-2.7.6.dist-info}/WHEEL +1 -1
- {coderouter_cli-2.7.5.dist-info → coderouter_cli-2.7.6.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.7.5.dist-info → coderouter_cli-2.7.6.dist-info}/licenses/LICENSE +0 -0
coderouter/gguf_introspect.py
CHANGED
|
@@ -136,6 +136,17 @@ class GGUFInfo:
|
|
|
136
136
|
n_kv_heads: int | None
|
|
137
137
|
file_type: int | None
|
|
138
138
|
file_size_bytes: int
|
|
139
|
+
n_nextn: int | None = None
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def supports_mtp(self) -> bool:
|
|
143
|
+
"""True when the GGUF embeds Multi-Token-Prediction (nextn) layers.
|
|
144
|
+
|
|
145
|
+
Derived from ``{arch}.nextn_predict_layers`` — a positive count means
|
|
146
|
+
the main model carries MTP/nextn tensors and can drive llama.cpp's
|
|
147
|
+
``--spec-type draft-mtp`` without a separate draft gguf.
|
|
148
|
+
"""
|
|
149
|
+
return bool(self.n_nextn and self.n_nextn > 0)
|
|
139
150
|
|
|
140
151
|
@property
|
|
141
152
|
def quant_name(self) -> str | None:
|
|
@@ -224,6 +235,10 @@ _KEY_BLOCK_COUNT = ".block_count"
|
|
|
224
235
|
_KEY_EMBED_LEN = ".embedding_length"
|
|
225
236
|
_KEY_HEAD_COUNT = ".attention.head_count"
|
|
226
237
|
_KEY_HEAD_COUNT_KV = ".attention.head_count_kv"
|
|
238
|
+
# Number of Multi-Token-Prediction (nextn) layers embedded in the main model
|
|
239
|
+
# (e.g. ``glm4moe.nextn_predict_layers``). A positive value means the GGUF can
|
|
240
|
+
# drive llama.cpp speculative decoding via ``--spec-type draft-mtp`` alone.
|
|
241
|
+
_KEY_NEXTN = ".nextn_predict_layers"
|
|
227
242
|
|
|
228
243
|
|
|
229
244
|
def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
@@ -245,6 +260,7 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
245
260
|
n_heads: int | None = None
|
|
246
261
|
n_kv_heads: int | None = None
|
|
247
262
|
file_type: int | None = None
|
|
263
|
+
n_nextn: int | None = None
|
|
248
264
|
|
|
249
265
|
with p.open("rb") as fh:
|
|
250
266
|
magic = fh.read(4)
|
|
@@ -275,6 +291,8 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
275
291
|
n_kv_heads = value
|
|
276
292
|
elif key.endswith(_KEY_HEAD_COUNT) and isinstance(value, int):
|
|
277
293
|
n_heads = value
|
|
294
|
+
elif key.endswith(_KEY_NEXTN) and isinstance(value, int):
|
|
295
|
+
n_nextn = value
|
|
278
296
|
|
|
279
297
|
return GGUFInfo(
|
|
280
298
|
architecture=arch,
|
|
@@ -284,6 +302,7 @@ def read_gguf_metadata(path: str | Path) -> GGUFInfo:
|
|
|
284
302
|
n_kv_heads=n_kv_heads,
|
|
285
303
|
file_type=file_type,
|
|
286
304
|
file_size_bytes=file_size,
|
|
305
|
+
n_nextn=n_nextn,
|
|
287
306
|
)
|
|
288
307
|
|
|
289
308
|
|
|
@@ -30,6 +30,7 @@ import secrets
|
|
|
30
30
|
import shlex
|
|
31
31
|
import shutil
|
|
32
32
|
import subprocess
|
|
33
|
+
import time
|
|
33
34
|
import uuid
|
|
34
35
|
from collections import deque
|
|
35
36
|
from dataclasses import dataclass, field
|
|
@@ -40,6 +41,7 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
40
41
|
from fastapi.responses import HTMLResponse
|
|
41
42
|
from pydantic import BaseModel
|
|
42
43
|
|
|
44
|
+
from coderouter.launcher_speculative import resolve_speculative
|
|
43
45
|
from coderouter.logging import get_logger
|
|
44
46
|
|
|
45
47
|
logger = get_logger(__name__)
|
|
@@ -115,9 +117,23 @@ class ManagedProcess:
|
|
|
115
117
|
options: dict[str, Any]
|
|
116
118
|
extra_args: str
|
|
117
119
|
status: str = "starting" # "starting" | "running" | "stopped" | "error"
|
|
120
|
+
# MTP / speculative-decoding controls (defaults keep existing call sites
|
|
121
|
+
# working). Recorded for introspection; the resolved flags live in the cmd.
|
|
122
|
+
draft_model_path: str | None = None
|
|
123
|
+
mtp_mode: str = "auto"
|
|
118
124
|
pid: int | None = None
|
|
119
125
|
returncode: int | None = None
|
|
120
126
|
log_tail: deque = field(default_factory=lambda: deque(maxlen=200))
|
|
127
|
+
# MTP auto-fallback state. When the speculative flags were added by AUTO
|
|
128
|
+
# detection and the child dies during startup, the process is relaunched
|
|
129
|
+
# ONCE without them (some archs' draft-mtp support in llama.cpp is
|
|
130
|
+
# immature and crashes the context init). These fields make that possible
|
|
131
|
+
# and impossible to loop.
|
|
132
|
+
spec_tokens: list = field(default_factory=list)
|
|
133
|
+
spec_auto: bool = False
|
|
134
|
+
mtp_fallback_done: bool = False
|
|
135
|
+
fallback_cmd: list | None = None
|
|
136
|
+
started_at: float = 0.0
|
|
121
137
|
# asyncio subprocess handle — not serialised
|
|
122
138
|
_proc: Any = field(default=None, repr=False, compare=False)
|
|
123
139
|
|
|
@@ -208,8 +224,15 @@ _BACKEND_DEFAULTS: dict[str, str] = {
|
|
|
208
224
|
# - llama.cpp: ``-m`` / ``--model`` select the GGUF file.
|
|
209
225
|
# - vllm / mlx: ``--model`` selects the model; ``-m`` selects the python
|
|
210
226
|
# module that ``_build_cmd`` pins, so it must not be re-specified either.
|
|
227
|
+
# - llama.cpp: the draft/MTP model is likewise set only via the dedicated
|
|
228
|
+
# ``draft_model_path`` field, so its aliases (``-md`` / ``--model-draft``
|
|
229
|
+
# / ``--spec-draft-model``) are rejected in options / extra_args too. The
|
|
230
|
+
# remaining spec knobs (``--spec-type``, ``--spec-draft-n-max`` …) stay
|
|
231
|
+
# free-form.
|
|
211
232
|
_MODEL_FLAGS: dict[str, frozenset[str]] = {
|
|
212
|
-
"llama.cpp": frozenset(
|
|
233
|
+
"llama.cpp": frozenset(
|
|
234
|
+
{"-m", "--model", "-md", "--model-draft", "--spec-draft-model"}
|
|
235
|
+
),
|
|
213
236
|
"vllm": frozenset({"-m", "--model"}),
|
|
214
237
|
"mlx": frozenset({"-m", "--model"}),
|
|
215
238
|
}
|
|
@@ -407,6 +430,24 @@ def _resolve_within_model_dirs(model_path: str, model_dirs: list[str]) -> Path:
|
|
|
407
430
|
raise ValueError("model_path is not under any configured model_dirs.")
|
|
408
431
|
|
|
409
432
|
|
|
433
|
+
def _option_tokens(options: dict[str, Any]) -> list[str]:
|
|
434
|
+
"""Flatten an ``options`` dict into CLI tokens (``{flag: val}`` semantics).
|
|
435
|
+
|
|
436
|
+
Boolean values become a bare flag when true and vanish when false;
|
|
437
|
+
everything else becomes ``[flag, str(val)]``. Shared by ``_build_cmd`` and
|
|
438
|
+
``api_start`` so the tokens fed to ``resolve_speculative`` match exactly
|
|
439
|
+
what lands on the command line (no double-parsing drift).
|
|
440
|
+
"""
|
|
441
|
+
tokens: list[str] = []
|
|
442
|
+
for flag, val in options.items():
|
|
443
|
+
if isinstance(val, bool):
|
|
444
|
+
if val:
|
|
445
|
+
tokens.append(flag)
|
|
446
|
+
else:
|
|
447
|
+
tokens.extend([flag, str(val)])
|
|
448
|
+
return tokens
|
|
449
|
+
|
|
450
|
+
|
|
410
451
|
def _build_cmd(
|
|
411
452
|
backend: str,
|
|
412
453
|
model_path: str,
|
|
@@ -414,17 +455,27 @@ def _build_cmd(
|
|
|
414
455
|
options: dict[str, Any],
|
|
415
456
|
extra_args: str,
|
|
416
457
|
binary: str | None = None,
|
|
458
|
+
spec_tokens: list[str] | None = None,
|
|
417
459
|
) -> list[str]:
|
|
418
460
|
"""Build the CLI command list for the given backend and options.
|
|
419
461
|
|
|
420
462
|
``binary`` overrides the default executable (``llama-server`` /
|
|
421
463
|
``python``). When None, the default is used and PATH resolution
|
|
422
464
|
is left to the OS.
|
|
465
|
+
|
|
466
|
+
``spec_tokens`` are pre-resolved speculative-decoding / MTP flags
|
|
467
|
+
(from :func:`coderouter.launcher_speculative.resolve_speculative`).
|
|
468
|
+
For llama.cpp they are appended right after the port args and BEFORE
|
|
469
|
+
the profile / extra args. They are trusted (the launcher, not the
|
|
470
|
+
caller, produced them) so they bypass the model-override guard even
|
|
471
|
+
though they may contain ``--model-draft``.
|
|
423
472
|
"""
|
|
424
473
|
exe = _resolve_binary(backend, binary)
|
|
425
474
|
|
|
426
475
|
if backend == "llama.cpp":
|
|
427
476
|
cmd: list[str] = [exe, "-m", model_path, "--port", str(port)]
|
|
477
|
+
if spec_tokens:
|
|
478
|
+
cmd.extend(spec_tokens)
|
|
428
479
|
elif backend == "vllm":
|
|
429
480
|
cmd = [
|
|
430
481
|
exe, "-m", "vllm.entrypoints.openai.api_server",
|
|
@@ -446,12 +497,7 @@ def _build_cmd(
|
|
|
446
497
|
# H8: reject model-path re-specification via options keys.
|
|
447
498
|
_assert_no_model_override(backend, list(options.keys()))
|
|
448
499
|
|
|
449
|
-
|
|
450
|
-
if isinstance(val, bool):
|
|
451
|
-
if val:
|
|
452
|
-
cmd.append(flag)
|
|
453
|
-
else:
|
|
454
|
-
cmd.extend([flag, str(val)])
|
|
500
|
+
cmd.extend(_option_tokens(options))
|
|
455
501
|
|
|
456
502
|
if extra_args.strip():
|
|
457
503
|
extra_tokens = shlex.split(extra_args)
|
|
@@ -474,12 +520,37 @@ def _build_cmd(
|
|
|
474
520
|
# recover by reading the buffered chunk and continuing so log tailing survives.
|
|
475
521
|
_LOG_STREAM_LIMIT = 256 * 1024 # 256 KB
|
|
476
522
|
|
|
523
|
+
# Window (seconds) after spawn in which a non-zero exit is treated as a
|
|
524
|
+
# startup crash eligible for the MTP auto-fallback. A backend that ran fine
|
|
525
|
+
# for longer and then died is a normal crash, not a load-time failure, so it
|
|
526
|
+
# is never retried.
|
|
527
|
+
_MTP_FALLBACK_WINDOW_SECS = 180.0
|
|
477
528
|
|
|
478
|
-
|
|
479
|
-
|
|
529
|
+
|
|
530
|
+
def _should_mtp_fallback(proc: ManagedProcess) -> bool:
|
|
531
|
+
"""True when a just-crashed process qualifies for the one-shot MTP retry.
|
|
532
|
+
|
|
533
|
+
Only auto-detected speculative launches that die within the startup
|
|
534
|
+
window are eligible, and only once (guarded by ``mtp_fallback_done``).
|
|
535
|
+
"""
|
|
480
536
|
p = proc._proc
|
|
481
|
-
if p is None
|
|
482
|
-
|
|
537
|
+
rc = p.returncode if p is not None else proc.returncode
|
|
538
|
+
return (
|
|
539
|
+
proc.spec_auto
|
|
540
|
+
and not proc.mtp_fallback_done
|
|
541
|
+
and bool(proc.fallback_cmd)
|
|
542
|
+
and rc not in (0, None)
|
|
543
|
+
and (time.monotonic() - proc.started_at) <= _MTP_FALLBACK_WINDOW_SECS
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
async def _tail_logs(proc: ManagedProcess) -> None:
|
|
548
|
+
"""Read stdout+stderr into proc.log_tail until the process exits.
|
|
549
|
+
|
|
550
|
+
Loops so that a one-shot MTP fallback (relaunch without the auto-detected
|
|
551
|
+
speculative flags) can be tailed by the same task. The loop exits as soon
|
|
552
|
+
as no fallback applies.
|
|
553
|
+
"""
|
|
483
554
|
|
|
484
555
|
async def _drain(stream: asyncio.StreamReader | None) -> None:
|
|
485
556
|
if stream is None:
|
|
@@ -505,14 +576,53 @@ async def _tail_logs(proc: ManagedProcess) -> None:
|
|
|
505
576
|
break
|
|
506
577
|
proc.log_tail.append(line.decode(errors="replace").rstrip())
|
|
507
578
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
579
|
+
while True:
|
|
580
|
+
p = proc._proc
|
|
581
|
+
if p is None:
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
await asyncio.gather(_drain(p.stdout), _drain(p.stderr))
|
|
585
|
+
await p.wait()
|
|
586
|
+
proc.returncode = p.returncode
|
|
587
|
+
proc.pid = None
|
|
588
|
+
proc.status = "stopped" if (p.returncode or 0) == 0 else "error"
|
|
589
|
+
proc.log_tail.append(
|
|
590
|
+
f"[launcher] process exited with code {p.returncode}"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if not _should_mtp_fallback(proc):
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
# Auto-detected MTP crashed during startup — retry ONCE without the
|
|
597
|
+
# speculative flags. The port is unchanged, so the existing provider
|
|
598
|
+
# registration still stands (do not re-register).
|
|
599
|
+
rc = proc.returncode
|
|
600
|
+
proc.mtp_fallback_done = True
|
|
601
|
+
proc.log_tail.append(
|
|
602
|
+
f"[launcher] MTP startup failure detected (exit code {rc}); "
|
|
603
|
+
"retrying without speculative decoding"
|
|
604
|
+
)
|
|
605
|
+
proc.log_tail.append(
|
|
606
|
+
f"[launcher] cmd: {' '.join(proc.fallback_cmd)}"
|
|
607
|
+
)
|
|
608
|
+
try:
|
|
609
|
+
p = await asyncio.create_subprocess_exec(
|
|
610
|
+
*proc.fallback_cmd,
|
|
611
|
+
stdout=asyncio.subprocess.PIPE,
|
|
612
|
+
stderr=asyncio.subprocess.PIPE,
|
|
613
|
+
limit=_LOG_STREAM_LIMIT,
|
|
614
|
+
)
|
|
615
|
+
except (FileNotFoundError, OSError) as exc:
|
|
616
|
+
proc.log_tail.append(f"[launcher] fallback relaunch failed: {exc}")
|
|
617
|
+
proc.status = "error"
|
|
618
|
+
return
|
|
619
|
+
proc._proc = p
|
|
620
|
+
proc.pid = p.pid
|
|
621
|
+
proc.status = "running"
|
|
622
|
+
proc.returncode = None
|
|
623
|
+
proc.started_at = time.monotonic()
|
|
624
|
+
proc.log_tail.append(f"[launcher] started PID {p.pid}")
|
|
625
|
+
# Loop to tail the relaunched process.
|
|
516
626
|
|
|
517
627
|
|
|
518
628
|
async def shutdown_launcher(app: Any) -> None:
|
|
@@ -555,6 +665,11 @@ class StartRequest(BaseModel):
|
|
|
555
665
|
port: int
|
|
556
666
|
options: dict[str, Any] = {}
|
|
557
667
|
extra_args: str = ""
|
|
668
|
+
# MTP / speculative-decoding controls (llama.cpp only). ``draft_model_path``
|
|
669
|
+
# is an explicit companion draft/MTP gguf; ``mtp_mode`` is "auto" (detect)
|
|
670
|
+
# or "off" (never emit speculative flags).
|
|
671
|
+
draft_model_path: str | None = None
|
|
672
|
+
mtp_mode: str = "auto"
|
|
558
673
|
|
|
559
674
|
|
|
560
675
|
# ---------------------------------------------------------------------------
|
|
@@ -690,15 +805,69 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
690
805
|
if bc and bc.binary:
|
|
691
806
|
configured_binary = bc.binary
|
|
692
807
|
|
|
808
|
+
# Validate the MTP mode up front (only "auto" / "off" are accepted).
|
|
809
|
+
if req.mtp_mode not in ("auto", "off"):
|
|
810
|
+
raise HTTPException(
|
|
811
|
+
status_code=400,
|
|
812
|
+
detail=f"mtp_mode must be 'auto' or 'off', got {req.mtp_mode!r}.",
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
# Build the user token list once (options flags + extra_args) so
|
|
816
|
+
# resolve_speculative sees exactly what _build_cmd will emit. shlex.split
|
|
817
|
+
# can raise on unbalanced quotes → surface as 400 like other bad input.
|
|
818
|
+
try:
|
|
819
|
+
user_tokens = _option_tokens(req.options)
|
|
820
|
+
if req.extra_args.strip():
|
|
821
|
+
user_tokens += shlex.split(req.extra_args)
|
|
822
|
+
except ValueError as exc:
|
|
823
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
824
|
+
|
|
825
|
+
# Resolve MTP / speculative-decoding flags (llama.cpp only; no-op elsewhere).
|
|
826
|
+
try:
|
|
827
|
+
spec_tokens, spec_notes = resolve_speculative(
|
|
828
|
+
req.backend,
|
|
829
|
+
req.model_path,
|
|
830
|
+
req.draft_model_path,
|
|
831
|
+
req.mtp_mode,
|
|
832
|
+
user_tokens,
|
|
833
|
+
)
|
|
834
|
+
except ValueError as exc:
|
|
835
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
836
|
+
|
|
693
837
|
try:
|
|
694
838
|
cmd = _build_cmd(
|
|
695
839
|
req.backend, req.model_path, req.port,
|
|
696
840
|
req.options, req.extra_args,
|
|
697
841
|
binary=configured_binary,
|
|
842
|
+
spec_tokens=spec_tokens,
|
|
698
843
|
)
|
|
699
844
|
except ValueError as exc:
|
|
700
845
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
701
846
|
|
|
847
|
+
# Speculative flags qualify for the one-shot startup-crash fallback only
|
|
848
|
+
# when they came from AUTO detection (mtp_mode="auto", no explicit draft
|
|
849
|
+
# model, and detection actually emitted flags). Explicit draft paths and
|
|
850
|
+
# operator-supplied --spec-type are never auto-retried. The fallback cmd
|
|
851
|
+
# is rebuilt from scratch with spec_tokens=None (exact — never spliced).
|
|
852
|
+
spec_auto = (
|
|
853
|
+
req.mtp_mode == "auto"
|
|
854
|
+
and req.draft_model_path is None
|
|
855
|
+
and bool(spec_tokens)
|
|
856
|
+
)
|
|
857
|
+
fallback_cmd: list[str] | None = None
|
|
858
|
+
if spec_auto:
|
|
859
|
+
try:
|
|
860
|
+
fallback_cmd = _build_cmd(
|
|
861
|
+
req.backend, req.model_path, req.port,
|
|
862
|
+
req.options, req.extra_args,
|
|
863
|
+
binary=configured_binary,
|
|
864
|
+
spec_tokens=None,
|
|
865
|
+
)
|
|
866
|
+
except ValueError:
|
|
867
|
+
# A failure to rebuild the non-spec command simply disables the
|
|
868
|
+
# fallback; it must never block the primary start.
|
|
869
|
+
fallback_cmd = None
|
|
870
|
+
|
|
702
871
|
proc_id = uuid.uuid4().hex[:8]
|
|
703
872
|
proc = ManagedProcess(
|
|
704
873
|
id=proc_id,
|
|
@@ -708,9 +877,16 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
708
877
|
port=req.port,
|
|
709
878
|
options=req.options,
|
|
710
879
|
extra_args=req.extra_args,
|
|
880
|
+
draft_model_path=req.draft_model_path,
|
|
881
|
+
mtp_mode=req.mtp_mode,
|
|
711
882
|
status="starting",
|
|
883
|
+
spec_tokens=spec_tokens,
|
|
884
|
+
spec_auto=spec_auto and fallback_cmd is not None,
|
|
885
|
+
fallback_cmd=fallback_cmd,
|
|
712
886
|
)
|
|
713
887
|
proc.log_tail.append(f"[launcher] cmd: {' '.join(cmd)}")
|
|
888
|
+
for note in spec_notes:
|
|
889
|
+
proc.log_tail.append(f"[launcher] {note}")
|
|
714
890
|
|
|
715
891
|
try:
|
|
716
892
|
p = await asyncio.create_subprocess_exec(
|
|
@@ -732,6 +908,7 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
732
908
|
proc._proc = p
|
|
733
909
|
proc.pid = p.pid
|
|
734
910
|
proc.status = "running"
|
|
911
|
+
proc.started_at = time.monotonic()
|
|
735
912
|
proc.log_tail.append(f"[launcher] started PID {p.pid}")
|
|
736
913
|
|
|
737
914
|
_registry(request).add(proc)
|
|
@@ -767,6 +944,7 @@ async def api_start(req: StartRequest, request: Request) -> dict[str, Any]:
|
|
|
767
944
|
"pid": p.pid,
|
|
768
945
|
"command": cmd,
|
|
769
946
|
"provider_sync": provider_sync,
|
|
947
|
+
"speculative": spec_tokens,
|
|
770
948
|
}
|
|
771
949
|
|
|
772
950
|
|
|
@@ -971,6 +1149,20 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
971
1149
|
<div id="profile-args" class="mt-1 text-xs font-mono text-slate-400 bg-slate-800/50 rounded p-2 hidden"></div>
|
|
972
1150
|
</div>
|
|
973
1151
|
|
|
1152
|
+
<div class="grid grid-cols-2 gap-2">
|
|
1153
|
+
<div>
|
|
1154
|
+
<label class="block text-xs text-slate-400 mb-1">MTP/draft gguf (空欄で自動検出)</label>
|
|
1155
|
+
<input id="f-draft" type="text" placeholder="companion .gguf (任意)" />
|
|
1156
|
+
</div>
|
|
1157
|
+
<div>
|
|
1158
|
+
<label class="block text-xs text-slate-400 mb-1">MTP</label>
|
|
1159
|
+
<select id="f-mtp">
|
|
1160
|
+
<option value="auto">auto</option>
|
|
1161
|
+
<option value="off">off</option>
|
|
1162
|
+
</select>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
|
|
974
1166
|
<div>
|
|
975
1167
|
<div class="flex items-center justify-between mb-1">
|
|
976
1168
|
<label class="block text-xs text-slate-400">追加オプション(自由入力)</label>
|
|
@@ -1292,6 +1484,8 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1292
1484
|
const backend = document.getElementById("f-backend").value;
|
|
1293
1485
|
const model = document.getElementById("f-model").value.trim();
|
|
1294
1486
|
const extra = document.getElementById("f-extra").value.trim();
|
|
1487
|
+
const draft = document.getElementById("f-draft").value.trim();
|
|
1488
|
+
const mtp = document.getElementById("f-mtp").value;
|
|
1295
1489
|
|
|
1296
1490
|
if (!name) { showLaunchErr("名前を入力してください"); return; }
|
|
1297
1491
|
if (!model) { showLaunchErr("モデルパスを入力してください"); return; }
|
|
@@ -1306,7 +1500,8 @@ _LAUNCHER_HTML = r"""<!doctype html>
|
|
|
1306
1500
|
method: "POST",
|
|
1307
1501
|
headers: authHeaders({"Content-Type": "application/json"}),
|
|
1308
1502
|
body: JSON.stringify({name, backend, model_path: model, port,
|
|
1309
|
-
options: selectedProfileArgs(), extra_args: extra
|
|
1503
|
+
options: selectedProfileArgs(), extra_args: extra,
|
|
1504
|
+
draft_model_path: draft || null, mtp_mode: mtp}),
|
|
1310
1505
|
});
|
|
1311
1506
|
const d = await res.json();
|
|
1312
1507
|
if (!res.ok) { showLaunchErr(d.detail || "起動失敗"); return; }
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Speculative-decoding / MTP flag resolution for the llama.cpp launcher.
|
|
2
|
+
|
|
3
|
+
Why this module exists
|
|
4
|
+
======================
|
|
5
|
+
|
|
6
|
+
llama.cpp's ``llama-server`` (2026) folds Multi-Token Prediction (MTP) into
|
|
7
|
+
its speculative-decoding framework. Instead of asking the operator to hand-
|
|
8
|
+
assemble the right ``--spec-type`` / ``--model-draft`` incantation, the
|
|
9
|
+
launcher exposes two high-level knobs — ``draft_model_path`` and
|
|
10
|
+
``mtp_mode`` — and derives the concrete CLI tokens here.
|
|
11
|
+
|
|
12
|
+
Both launchers (the FastAPI web UI in
|
|
13
|
+
:mod:`coderouter.ingress.launcher_routes` and the tkinter desktop app in
|
|
14
|
+
``launcher_gui.py``) call :func:`resolve_speculative` so the decision logic
|
|
15
|
+
lives in exactly one place.
|
|
16
|
+
|
|
17
|
+
Decision summary (llama.cpp only)
|
|
18
|
+
---------------------------------
|
|
19
|
+
|
|
20
|
+
* ``mtp_mode="off"`` — never emit speculative flags (reproduces the historical
|
|
21
|
+
command exactly). Combining it with ``draft_model_path`` is a conflict.
|
|
22
|
+
* Explicit ``draft_model_path`` — validate it, then pick ``draft-mtp`` when the
|
|
23
|
+
filename looks MTP-ish (``/mtp/i``) or ``draft-simple`` otherwise.
|
|
24
|
+
* ``mtp_mode="auto"`` (the default) — inspect the main GGUF: if it embeds nextn
|
|
25
|
+
layers (``{arch}.nextn_predict_layers > 0``) use ``--spec-type draft-mtp``
|
|
26
|
+
with no separate draft model; otherwise scan the *same directory* for a
|
|
27
|
+
small companion draft/MTP gguf and wire it up if one is found.
|
|
28
|
+
|
|
29
|
+
The functions here are pure stdlib + :mod:`coderouter.gguf_introspect`; they
|
|
30
|
+
perform filesystem reads but never spawn processes and hold no FastAPI
|
|
31
|
+
dependency, which keeps them trivially unit-testable.
|
|
32
|
+
|
|
33
|
+
Security
|
|
34
|
+
--------
|
|
35
|
+
|
|
36
|
+
Every resolved path is placed into an argv list by the caller (never a shell
|
|
37
|
+
string), and ``draft_model_path`` must resolve to an existing regular file —
|
|
38
|
+
so this module adds no new shell-interpolation or path-probing surface beyond
|
|
39
|
+
reading GGUF headers the operator already pointed us at.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import re
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
from coderouter.gguf_introspect import try_read_gguf_metadata
|
|
48
|
+
|
|
49
|
+
__all__ = ["find_draft_companion", "resolve_speculative"]
|
|
50
|
+
|
|
51
|
+
_LLAMA_CPP = "llama.cpp"
|
|
52
|
+
|
|
53
|
+
# Companion candidates must be meaningfully smaller than the main model — a
|
|
54
|
+
# draft / MTP head is a fraction of the target's weights. Guards against
|
|
55
|
+
# mistaking a second full model in the same folder for a draft.
|
|
56
|
+
_COMPANION_MAX_SIZE_RATIO = 0.5
|
|
57
|
+
|
|
58
|
+
# Minimum stripped-prefix length before a shared-prefix match is trusted
|
|
59
|
+
# (avoids matching two unrelated models that merely share a short token).
|
|
60
|
+
_MIN_PREFIX_LEN = 3
|
|
61
|
+
|
|
62
|
+
# Trailing GGUF shard marker, e.g. "-00001-of-00003".
|
|
63
|
+
_SHARD_RE = re.compile(r"[-._]\d{5}-of-\d{5}$", re.IGNORECASE)
|
|
64
|
+
|
|
65
|
+
# Trailing quantisation token, e.g. "-Q4_K_M", ".IQ3_XXS", "-F16", "-BF16".
|
|
66
|
+
_QUANT_RE = re.compile(
|
|
67
|
+
r"[-._]?(i?q\d[_a-z0-9]*|bf16|fp?16|fp?32)$",
|
|
68
|
+
re.IGNORECASE,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Filename hint that the companion carries MTP tensors (→ draft-mtp).
|
|
72
|
+
_MTP_NAME_RE = re.compile(r"mtp", re.IGNORECASE)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _strip_quant_suffix(stem: str) -> str:
|
|
76
|
+
"""Return ``stem`` with a trailing shard marker and quant token removed.
|
|
77
|
+
|
|
78
|
+
Used to derive a model's "family prefix" so a companion sitting next to
|
|
79
|
+
``Foo-7B-Q4_K_M.gguf`` (prefix ``Foo-7B``) can be matched even though its
|
|
80
|
+
own quant/shape suffix differs.
|
|
81
|
+
"""
|
|
82
|
+
s = _SHARD_RE.sub("", stem)
|
|
83
|
+
s = _QUANT_RE.sub("", s)
|
|
84
|
+
return s
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _has_spec_type(user_tokens: list[str]) -> bool:
|
|
88
|
+
"""True if the operator already supplied ``--spec-type`` in extra args."""
|
|
89
|
+
return any(
|
|
90
|
+
tok == "--spec-type" or tok.startswith("--spec-type=")
|
|
91
|
+
for tok in user_tokens
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _has_split_mode_tensor(user_tokens: list[str]) -> bool:
|
|
96
|
+
"""True if the tokens request ``--split-mode tensor`` (either form).
|
|
97
|
+
|
|
98
|
+
llama.cpp issue #24309: a nextn-embedded model combined with tensor split
|
|
99
|
+
crashes, so callers warn (but do not block) when this pairs with emitted
|
|
100
|
+
speculative flags.
|
|
101
|
+
"""
|
|
102
|
+
for i, tok in enumerate(user_tokens):
|
|
103
|
+
if tok == "--split-mode=tensor":
|
|
104
|
+
return True
|
|
105
|
+
if (
|
|
106
|
+
tok == "--split-mode"
|
|
107
|
+
and i + 1 < len(user_tokens)
|
|
108
|
+
and user_tokens[i + 1] == "tensor"
|
|
109
|
+
):
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _spec_type_for_name(path: Path) -> str:
|
|
115
|
+
"""Pick ``draft-mtp`` for MTP-named files, ``draft-simple`` otherwise."""
|
|
116
|
+
return "draft-mtp" if _MTP_NAME_RE.search(path.name) else "draft-simple"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_draft_companion(main_path: str | Path) -> Path | None:
|
|
120
|
+
"""Scan the directory of ``main_path`` for a companion draft/MTP gguf.
|
|
121
|
+
|
|
122
|
+
A candidate qualifies when it is a ``*.gguf`` other than the main file,
|
|
123
|
+
is smaller than :data:`_COMPANION_MAX_SIZE_RATIO` of the main file, and
|
|
124
|
+
either:
|
|
125
|
+
|
|
126
|
+
* has an ``mtp`` or ``draft`` hint in its filename, or
|
|
127
|
+
* shares the main file's family prefix (main filename with its shard /
|
|
128
|
+
quant suffix stripped).
|
|
129
|
+
|
|
130
|
+
If a candidate's GGUF ``architecture`` is readable and differs from the
|
|
131
|
+
main model's, it is dropped (tokenizer/vocab must match for speculation);
|
|
132
|
+
unreadable metadata is kept on a best-effort basis.
|
|
133
|
+
|
|
134
|
+
Candidates are ranked by name hint (``mtp`` > ``draft`` > none), then by
|
|
135
|
+
whether they share the prefix, then by smallest size. Returns the best
|
|
136
|
+
match, or ``None`` when the directory holds no suitable companion.
|
|
137
|
+
"""
|
|
138
|
+
main = Path(main_path).expanduser()
|
|
139
|
+
directory = main.parent
|
|
140
|
+
try:
|
|
141
|
+
main_size = main.stat().st_size
|
|
142
|
+
except OSError:
|
|
143
|
+
return None
|
|
144
|
+
if main_size <= 0:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
main_prefix = _strip_quant_suffix(main.stem)
|
|
148
|
+
main_meta = try_read_gguf_metadata(main)
|
|
149
|
+
main_arch = main_meta.architecture if main_meta else None
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
entries = list(directory.iterdir())
|
|
153
|
+
except OSError:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
# Each ranked candidate: (name_hint, shares_prefix, -size) sorted so that
|
|
157
|
+
# the strongest hint / prefix / smallest file sorts first.
|
|
158
|
+
ranked: list[tuple[int, int, int, Path]] = []
|
|
159
|
+
for cand in entries:
|
|
160
|
+
if cand == main:
|
|
161
|
+
continue
|
|
162
|
+
if cand.suffix.lower() != ".gguf":
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
if not cand.is_file():
|
|
166
|
+
continue
|
|
167
|
+
size = cand.stat().st_size
|
|
168
|
+
except OSError:
|
|
169
|
+
continue
|
|
170
|
+
if size <= 0 or size >= main_size * _COMPANION_MAX_SIZE_RATIO:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
name_lower = cand.name.lower()
|
|
174
|
+
has_mtp = "mtp" in name_lower
|
|
175
|
+
has_draft = "draft" in name_lower
|
|
176
|
+
shares_prefix = (
|
|
177
|
+
len(main_prefix) >= _MIN_PREFIX_LEN
|
|
178
|
+
and cand.stem.lower().startswith(main_prefix.lower())
|
|
179
|
+
)
|
|
180
|
+
if not (has_mtp or has_draft or shares_prefix):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Reject a candidate whose architecture is readable and mismatches the
|
|
184
|
+
# main model — a speculator must share the target's vocabulary.
|
|
185
|
+
cand_meta = try_read_gguf_metadata(cand)
|
|
186
|
+
if (
|
|
187
|
+
cand_meta is not None
|
|
188
|
+
and cand_meta.architecture is not None
|
|
189
|
+
and main_arch is not None
|
|
190
|
+
and cand_meta.architecture != main_arch
|
|
191
|
+
):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
name_hint = 2 if has_mtp else (1 if has_draft else 0)
|
|
195
|
+
ranked.append((name_hint, int(shares_prefix), size, cand))
|
|
196
|
+
|
|
197
|
+
if not ranked:
|
|
198
|
+
return None
|
|
199
|
+
# Highest hint, then prefix match, then smallest size.
|
|
200
|
+
ranked.sort(key=lambda t: (-t[0], -t[1], t[2]))
|
|
201
|
+
return ranked[0][3]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def resolve_speculative(
|
|
205
|
+
backend: str,
|
|
206
|
+
model_path: str,
|
|
207
|
+
draft_model_path: str | None,
|
|
208
|
+
mtp_mode: str,
|
|
209
|
+
user_tokens: list[str],
|
|
210
|
+
) -> tuple[list[str], list[str]]:
|
|
211
|
+
"""Resolve speculative-decoding / MTP CLI tokens for a launch.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
backend:
|
|
216
|
+
Target backend id. Only ``"llama.cpp"`` supports speculation; other
|
|
217
|
+
backends must not receive ``draft_model_path`` / ``mtp_mode="off"``.
|
|
218
|
+
model_path:
|
|
219
|
+
The vetted main model path (already set via the launcher's dedicated
|
|
220
|
+
field). Inspected for embedded nextn layers / same-folder companions.
|
|
221
|
+
draft_model_path:
|
|
222
|
+
Optional explicit companion draft or MTP gguf. ``None`` means "not
|
|
223
|
+
supplied".
|
|
224
|
+
mtp_mode:
|
|
225
|
+
``"auto"`` (default behaviour — detect) or ``"off"`` (never emit
|
|
226
|
+
speculative flags).
|
|
227
|
+
user_tokens:
|
|
228
|
+
The flat list of option/extra-args tokens already destined for the
|
|
229
|
+
command line. Used to defer to an operator-supplied ``--spec-type``
|
|
230
|
+
and to warn about the tensor-split crash.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
``(spec_tokens, notes)`` where ``spec_tokens`` is the list of CLI tokens to
|
|
235
|
+
append (empty when none apply) and ``notes`` is a list of human-readable
|
|
236
|
+
strings describing what was decided (surfaced in the process log).
|
|
237
|
+
|
|
238
|
+
Raises
|
|
239
|
+
------
|
|
240
|
+
ValueError
|
|
241
|
+
On misuse — the web API maps this to HTTP 400:
|
|
242
|
+
|
|
243
|
+
* ``draft_model_path`` / ``mtp_mode="off"`` on a non-llama.cpp backend,
|
|
244
|
+
* ``mtp_mode="off"`` combined with an explicit ``draft_model_path``,
|
|
245
|
+
* an explicit ``draft_model_path`` that does not resolve to a file.
|
|
246
|
+
"""
|
|
247
|
+
notes: list[str] = []
|
|
248
|
+
|
|
249
|
+
# 1. Non-llama.cpp backends do not support speculation. Reject explicit
|
|
250
|
+
# use; otherwise (auto default, no draft) stay a silent no-op.
|
|
251
|
+
if backend != _LLAMA_CPP:
|
|
252
|
+
if draft_model_path or mtp_mode == "off":
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"draft_model_path / mtp_mode are only supported for llama.cpp"
|
|
255
|
+
)
|
|
256
|
+
return [], notes
|
|
257
|
+
|
|
258
|
+
# 2. Explicit opt-out. Never emit flags; conflicting draft path is an error.
|
|
259
|
+
if mtp_mode == "off":
|
|
260
|
+
if draft_model_path:
|
|
261
|
+
raise ValueError(
|
|
262
|
+
"mtp_mode='off' conflicts with an explicit draft_model_path"
|
|
263
|
+
)
|
|
264
|
+
return [], notes
|
|
265
|
+
|
|
266
|
+
# 3. Operator already drives speculation via extra args → defer entirely.
|
|
267
|
+
if _has_spec_type(user_tokens):
|
|
268
|
+
notes.append(
|
|
269
|
+
"spec flags supplied via extra args; auto MTP detection skipped"
|
|
270
|
+
)
|
|
271
|
+
return [], notes
|
|
272
|
+
|
|
273
|
+
spec_tokens: list[str] = []
|
|
274
|
+
|
|
275
|
+
# 4. Explicit companion draft / MTP gguf.
|
|
276
|
+
if draft_model_path:
|
|
277
|
+
draft = Path(draft_model_path).expanduser()
|
|
278
|
+
if not draft.is_file():
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"draft_model_path does not exist or is not a file: {draft}"
|
|
281
|
+
)
|
|
282
|
+
spec_type = _spec_type_for_name(draft)
|
|
283
|
+
spec_tokens = ["--spec-type", spec_type, "--model-draft", str(draft)]
|
|
284
|
+
notes.append(
|
|
285
|
+
f"MTP: explicit draft model {draft.name} → --spec-type {spec_type}"
|
|
286
|
+
)
|
|
287
|
+
# 5. Auto detection (only when the main path is an existing .gguf).
|
|
288
|
+
else:
|
|
289
|
+
main = Path(model_path).expanduser()
|
|
290
|
+
if main.suffix.lower() != ".gguf" or not main.is_file():
|
|
291
|
+
notes.append(
|
|
292
|
+
"MTP auto: main model is not an existing .gguf; "
|
|
293
|
+
"skipping speculative detection"
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
info = try_read_gguf_metadata(main)
|
|
297
|
+
if info is not None and info.n_nextn and info.n_nextn > 0:
|
|
298
|
+
# 5a. Main gguf embeds nextn/MTP tensors — no draft model needed.
|
|
299
|
+
spec_tokens = ["--spec-type", "draft-mtp"]
|
|
300
|
+
notes.append(
|
|
301
|
+
f"MTP: nextn layers ({info.n_nextn}) detected in main "
|
|
302
|
+
"gguf → --spec-type draft-mtp"
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
# 5b. Look for a companion gguf next to the main model.
|
|
306
|
+
companion = find_draft_companion(main)
|
|
307
|
+
if companion is not None:
|
|
308
|
+
spec_type = _spec_type_for_name(companion)
|
|
309
|
+
spec_tokens = [
|
|
310
|
+
"--spec-type",
|
|
311
|
+
spec_type,
|
|
312
|
+
"--model-draft",
|
|
313
|
+
str(companion),
|
|
314
|
+
]
|
|
315
|
+
notes.append(
|
|
316
|
+
f"MTP: companion gguf {companion.name} found next to "
|
|
317
|
+
f"main → --spec-type {spec_type}"
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
# 5c. Nothing found — start without speculative decoding.
|
|
321
|
+
notes.append(
|
|
322
|
+
f"MTP/draft gguf not found next to {main.name}; "
|
|
323
|
+
"starting without speculative decoding"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# 6. Warn (do not block) about the nextn + tensor-split crash.
|
|
327
|
+
if spec_tokens and _has_split_mode_tensor(user_tokens):
|
|
328
|
+
notes.append(
|
|
329
|
+
"WARNING: --split-mode tensor with speculative/nextn is known to "
|
|
330
|
+
"crash llama.cpp (issue #24309); consider --split-mode layer"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return spec_tokens, notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.6
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -7,9 +7,10 @@ coderouter/doctor.py,sha256=2luNk6BHSRvpQStJnHcqzNvNi-SKdOuKV0WZdorZhVk,82854
|
|
|
7
7
|
coderouter/doctor_apply.py,sha256=r_J6xbu5-HivofPNriw4_vjNYs_VRs7GsGTS0oMEX10,24209
|
|
8
8
|
coderouter/env_security.py,sha256=FEBZnXfJ0xE39kmMMn39zk0W_DRRnmcB_REmP9f4xWo,14796
|
|
9
9
|
coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
|
|
10
|
-
coderouter/gguf_introspect.py,sha256=
|
|
10
|
+
coderouter/gguf_introspect.py,sha256=KeE00CfbYRa4gSTrzuwC6zVuHi20IzLC4nUQa98BEXI,10387
|
|
11
11
|
coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
|
|
12
12
|
coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
|
|
13
|
+
coderouter/launcher_speculative.py,sha256=kWaHNmhzBsbWuM_lRGDlbWkgJ1suuxBWQNTKhJOC-yg,12644
|
|
13
14
|
coderouter/logging.py,sha256=91cveN8SCJEMNz9DGi6TrFSlc8cx0wdUkOdSLWLA-z8,56144
|
|
14
15
|
coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
|
|
15
16
|
coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
|
|
@@ -41,7 +42,7 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
|
|
|
41
42
|
coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
|
|
42
43
|
coderouter/ingress/app.py,sha256=h6s-UWsbGP22rku4pPVlvRdrtbMj4KoEEUZoQQOrTTw,19370
|
|
43
44
|
coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
|
|
44
|
-
coderouter/ingress/launcher_routes.py,sha256=
|
|
45
|
+
coderouter/ingress/launcher_routes.py,sha256=8EY3a6O_vbA45h2uhp_wc37bP_DJFqVVjsd68d_bwtk,66179
|
|
45
46
|
coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
|
|
46
47
|
coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
|
|
47
48
|
coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
|
|
@@ -67,8 +68,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
|
|
|
67
68
|
coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
|
|
68
69
|
coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
|
|
69
70
|
coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
|
|
70
|
-
coderouter_cli-2.7.
|
|
71
|
-
coderouter_cli-2.7.
|
|
72
|
-
coderouter_cli-2.7.
|
|
73
|
-
coderouter_cli-2.7.
|
|
74
|
-
coderouter_cli-2.7.
|
|
71
|
+
coderouter_cli-2.7.6.dist-info/METADATA,sha256=VNl2YqpfRQDN5Ww8ukg21mjL5JVvgAOtPj6HKTZKP6Y,15546
|
|
72
|
+
coderouter_cli-2.7.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
73
|
+
coderouter_cli-2.7.6.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
|
|
74
|
+
coderouter_cli-2.7.6.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
|
|
75
|
+
coderouter_cli-2.7.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|