Implement prompt caching, speculative decoding config, parallel slots, and dynamic pricing metrics
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db"
|
||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||
_DEFAULT_CMD_TEMPLATE = (
|
||||
"llama-server -m {model} --host 127.0.0.1 --port ${PORT} "
|
||||
"-c {ctx} -ngl 999 -fa 1 --no-mmap"
|
||||
"-c {ctx} -ngl 999 -fa 1 --no-mmap --prompt-cache --prompt-cache-all"
|
||||
)
|
||||
CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
|
||||
if "{model}" not in CMD_TEMPLATE:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add backend directory to sys.path so we can import services
|
||||
sys.path.append(str(Path(__file__).resolve().parent))
|
||||
|
||||
from services.llamaswap import read_config, write_config
|
||||
from config import CONFIG_PATH
|
||||
|
||||
def migrate():
|
||||
print(f"Reading config from {CONFIG_PATH}...")
|
||||
if not CONFIG_PATH.exists():
|
||||
print(f"Config path {CONFIG_PATH} does not exist. Skipping.")
|
||||
return
|
||||
|
||||
cfg = read_config()
|
||||
models = cfg.get("models", {})
|
||||
|
||||
draft_path = "/srv/models/drafts/qwen2.5-1.5b-instruct-q4_k_m.gguf"
|
||||
|
||||
for name, spec in models.items():
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
cmd = spec.get("cmd", "")
|
||||
if not cmd:
|
||||
continue
|
||||
|
||||
print(f"Migrating model: {name}")
|
||||
|
||||
# 1. Ensure prompt caching flags exist
|
||||
if "--prompt-cache " not in cmd and not cmd.endswith("--prompt-cache") and not cmd.endswith("--prompt-cache\n"):
|
||||
cmd = cmd.strip() + " --prompt-cache"
|
||||
if "--prompt-cache-all" not in cmd:
|
||||
cmd = cmd.strip() + " --prompt-cache-all"
|
||||
|
||||
# 2. Extract aliases/role
|
||||
aliases = spec.get("aliases", [])
|
||||
role = aliases[0] if aliases else None
|
||||
|
||||
# 3. Add parallel and speculative decoding for fast and coder
|
||||
if role in ("fast", "coder"):
|
||||
if "--parallel" not in cmd:
|
||||
cmd = cmd.strip() + " --parallel 2"
|
||||
if "--spec-draft-model" not in cmd:
|
||||
cmd = cmd.strip() + f" --spec-draft-model {draft_path}"
|
||||
|
||||
# Update cmd
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
spec["cmd"] = LiteralScalarString(cmd.strip() + "\n")
|
||||
|
||||
print(f"Writing updated config back to {CONFIG_PATH}...")
|
||||
write_config(cfg)
|
||||
print("Migration completed successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
@@ -52,7 +52,7 @@ async def _proxy(path: str, request: Request):
|
||||
prompt = usage.get("prompt_tokens", 0)
|
||||
completion = usage.get("completion_tokens", 0)
|
||||
if prompt or completion:
|
||||
increment_tokens(prompt, completion)
|
||||
increment_tokens(prompt, completion, model=alias)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
@@ -69,7 +69,7 @@ async def _proxy(path: str, request: Request):
|
||||
prompt = usage.get("prompt_tokens", 0)
|
||||
completion = usage.get("completion_tokens", 0)
|
||||
if prompt or completion:
|
||||
increment_tokens(prompt, completion)
|
||||
increment_tokens(prompt, completion, model=alias)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse(resp_json, status_code=r.status_code, headers=routed)
|
||||
|
||||
@@ -91,6 +91,7 @@ def self_update() -> dict:
|
||||
|
||||
|
||||
from services.token_stats import get_stats
|
||||
from services.llamaswap import list_models
|
||||
|
||||
@router.get("/system/token-stats")
|
||||
def token_stats() -> dict:
|
||||
@@ -99,9 +100,48 @@ def token_stats() -> dict:
|
||||
c = stats.get("completion_tokens", 0)
|
||||
total = p + c
|
||||
|
||||
# Juni 2026 API-Preise (deutlich gestiegen):
|
||||
# Premium-Modelle (Claude 4 / GPT-5): 15,00 $ / 1M Input-Tokens und 75,00 $ / 1M Output-Tokens
|
||||
saved_usd = (p * 15.0 + c * 75.0) / 1_000_000.0
|
||||
# Map model IDs and aliases to their respective roles for pricing resolution
|
||||
role_map = {}
|
||||
try:
|
||||
for m in list_models():
|
||||
role_map[m["name"].lower()] = m.get("role")
|
||||
for alias in m.get("aliases", []):
|
||||
role_map[alias.lower()] = m.get("role")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Dynamic pricing tiers based on model class in June 2026
|
||||
PRICING = {
|
||||
"heavy": (15.0, 75.0),
|
||||
"coder": (3.0, 15.0),
|
||||
"hermes": (1.0, 5.0),
|
||||
"fast": (0.15, 0.60),
|
||||
"scout": (0.15, 0.60),
|
||||
"vision": (0.15, 0.60),
|
||||
"reasoning": (0.15, 0.60),
|
||||
}
|
||||
|
||||
modeled_p = 0
|
||||
modeled_c = 0
|
||||
saved_usd = 0.0
|
||||
|
||||
models_data = stats.get("models") or {}
|
||||
for m_name, m_tokens in models_data.items():
|
||||
mp = m_tokens.get("prompt", 0)
|
||||
mc = m_tokens.get("completion", 0)
|
||||
modeled_p += mp
|
||||
modeled_c += mc
|
||||
|
||||
role = role_map.get(m_name, m_name)
|
||||
rate_in, rate_out = PRICING.get(role, (0.15, 0.60))
|
||||
saved_usd += (mp * rate_in + mc * rate_out) / 1_000_000.0
|
||||
|
||||
# Baseline/legacy tokens calculated at premium rates ($15.00 / $75.00)
|
||||
# to preserve historical savings value prior to model-specific logging
|
||||
baseline_p = max(0, p - modeled_p)
|
||||
baseline_c = max(0, c - modeled_c)
|
||||
saved_usd += (baseline_p * 15.0 + baseline_c * 75.0) / 1_000_000.0
|
||||
|
||||
saved_eur = saved_usd * 0.92 # 1 USD = 0.92 EUR
|
||||
|
||||
return {
|
||||
|
||||
@@ -57,6 +57,15 @@ def _parse_model(name: str, spec: dict) -> dict:
|
||||
aliases = [str(a) for a in aliases]
|
||||
role = aliases[0].lower() if aliases else (name.lower() if name.lower() in ROLE_IDS else None)
|
||||
|
||||
prompt_cache = "--prompt-cache " in cmd or cmd.endswith("--prompt-cache") or "--prompt-cache-all" in cmd
|
||||
spec_draft = None
|
||||
if "--spec-draft-model" in cmd:
|
||||
m_draft = re.search(r"--spec-draft-model\s+([^\s]+)", cmd)
|
||||
if m_draft:
|
||||
spec_draft = os.path.basename(m_draft.group(1).replace("'", "").replace('"', ""))
|
||||
parallel_match = re.search(r"--parallel\s+(\d+)", cmd)
|
||||
parallel_slots = int(parallel_match.group(1)) if parallel_match else 1
|
||||
|
||||
from services.caps import capabilities
|
||||
return {
|
||||
"name": name,
|
||||
@@ -71,6 +80,9 @@ def _parse_model(name: str, spec: dict) -> dict:
|
||||
"quant": quant,
|
||||
"size_bytes": size_bytes,
|
||||
"incomplete": not path,
|
||||
"prompt_cache": prompt_cache,
|
||||
"spec_draft_model": spec_draft,
|
||||
"parallel_slots": parallel_slots,
|
||||
"capabilities": capabilities(
|
||||
name=filename or name, cmd=cmd,
|
||||
gguf_path=(path if (path and os.path.exists(path)) else ""),
|
||||
@@ -171,6 +183,15 @@ def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
||||
cmd = _augment_vision(cmd, model_path, mmproj_path)
|
||||
if jinja and "--jinja" not in cmd:
|
||||
cmd += " --jinja"
|
||||
|
||||
role_lower = (role or "").strip().lower()
|
||||
if role_lower in ("fast", "coder"):
|
||||
if "--parallel" not in cmd:
|
||||
cmd += " --parallel 2"
|
||||
draft_path = "/srv/models/drafts/qwen2.5-1.5b-instruct-q4_k_m.gguf"
|
||||
if os.path.exists(draft_path) and "--spec-draft-model" not in cmd:
|
||||
cmd += f" --spec-draft-model {draft_path}"
|
||||
|
||||
cfg.setdefault("models", {})[model_id] = {
|
||||
"cmd": LiteralScalarString(cmd + "\n"),
|
||||
"ttl": ttl if ttl is not None else DEFAULT_TTL,
|
||||
|
||||
@@ -27,9 +27,11 @@ def get_stats() -> dict:
|
||||
data["prompt_tokens"] = 0
|
||||
if "completion_tokens" not in data:
|
||||
data["completion_tokens"] = 0
|
||||
if "models" not in data:
|
||||
data["models"] = {}
|
||||
return data
|
||||
except Exception:
|
||||
return {"prompt_tokens": 0, "completion_tokens": 0}
|
||||
return {"prompt_tokens": 0, "completion_tokens": 0, "models": {}}
|
||||
|
||||
def save_stats(stats: dict):
|
||||
try:
|
||||
@@ -39,8 +41,16 @@ def save_stats(stats: dict):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def increment_tokens(prompt: int, completion: int):
|
||||
def increment_tokens(prompt: int, completion: int, model: str = None):
|
||||
stats = get_stats()
|
||||
stats["prompt_tokens"] += prompt
|
||||
stats["completion_tokens"] += completion
|
||||
if model:
|
||||
model = model.lower()
|
||||
if "models" not in stats:
|
||||
stats["models"] = {}
|
||||
if model not in stats["models"]:
|
||||
stats["models"][model] = {"prompt": 0, "completion": 0}
|
||||
stats["models"][model]["prompt"] += prompt
|
||||
stats["models"][model]["completion"] += completion
|
||||
save_stats(stats)
|
||||
|
||||
+20
-20
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-9qPdHcPL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Djoh-Jwz.css">
|
||||
<script type="module" crossorigin src="/assets/index-CJm59bcL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -86,6 +86,9 @@ export interface ModelInfo {
|
||||
quant: string
|
||||
size_bytes: number | null
|
||||
incomplete: boolean
|
||||
prompt_cache: boolean
|
||||
spec_draft_model: string | null
|
||||
parallel_slots: number
|
||||
capabilities: Capabilities
|
||||
}
|
||||
|
||||
|
||||
@@ -602,7 +602,7 @@ export function DashboardView() {
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1 mr-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
|
||||
role === "fast" ? "bg-cyan-500/15 text-cyan-400 border-cyan-500/25" :
|
||||
role === "heavy" ? "bg-amber-500/15 text-amber-400 border-amber-500/25" :
|
||||
@@ -614,9 +614,30 @@ export function DashboardView() {
|
||||
{role}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-semibold truncate font-mono text-foreground">
|
||||
{m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
|
||||
</span>
|
||||
{m && (
|
||||
<div className="flex gap-1 items-center mt-0.5 flex-wrap">
|
||||
{m.prompt_cache && (
|
||||
<span className="text-[7px] leading-none font-mono font-bold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 px-1 py-0.5 rounded" title="Prompt Caching aktiv">
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_draft_model && (
|
||||
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
)}
|
||||
{m.parallel_slots > 1 && (
|
||||
<span className="text-[7px] leading-none font-mono font-bold bg-violet-500/10 text-violet-400 border border-violet-500/20 px-1 py-0.5 rounded" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -942,6 +942,21 @@ function Cockpit() {
|
||||
{m.role}
|
||||
</span>
|
||||
)}
|
||||
{m.prompt_cache && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-cyan-500/10 border border-cyan-500/20 text-[9px] font-mono text-cyan-400 font-bold uppercase" title="Prompt Caching aktiv">
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_draft_model && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[9px] font-mono text-emerald-400 font-bold uppercase" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
)}
|
||||
{m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[9px] font-mono text-violet-400 font-bold uppercase" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1062,6 +1077,21 @@ function Cockpit() {
|
||||
{m.role}
|
||||
</span>
|
||||
)}
|
||||
{m.prompt_cache && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-cyan-500/10 border border-cyan-500/20 text-[8px] font-mono text-cyan-400 font-bold uppercase shrink-0" title="Prompt Caching aktiv">
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_draft_model && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[8px] font-mono text-emerald-400 font-bold uppercase shrink-0" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
)}
|
||||
{m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[8px] font-mono text-violet-400 font-bold uppercase shrink-0" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
</span>
|
||||
)}
|
||||
{isRunning && (
|
||||
<span className="flex items-center gap-0.5 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider shrink-0">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> warm
|
||||
|
||||
Reference in New Issue
Block a user