Refactor: Pricing/Draft-Pfad als Single Source of Truth (Phase 1)
- Neuer services/pricing.py: PRICING-Dict + compute_savings() aus dem system-Router extrahiert; Router ist jetzt dünn (nur role_map + Aufruf). - /system/token-stats liefert zusätzlich das pricing-Dict → Frontend zeigt die Tarife daraus an statt sie im Text zu hartkodieren. - SPEC_DRAFT_MODEL_PATH in config.py (MC_SPEC_DRAFT_MODEL); llamaswap.py und migrate_config.py referenzieren die Konstante statt des doppelten Literals. - Ersparnis-Berechnung verhaltensneutral verifiziert (35,09 $ / 32,28 €). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,10 @@ CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
|
|||||||
if "{model}" not in CMD_TEMPLATE:
|
if "{model}" not in CMD_TEMPLATE:
|
||||||
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
|
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
|
||||||
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
||||||
|
# Draft-Modell für Speculative Decoding (nur fast/coder, wenn vorhanden). Eine
|
||||||
|
# Quelle der Wahrheit für llamaswap.register_model + migrate_config.
|
||||||
|
SPEC_DRAFT_MODEL_PATH = os.environ.get(
|
||||||
|
"MC_SPEC_DRAFT_MODEL", f"{MODELS_DIR.as_posix()}/drafts/qwen2.5-1.5b-instruct-q4_k_m.gguf")
|
||||||
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
|
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
|
||||||
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
sys.path.append(str(Path(__file__).resolve().parent))
|
sys.path.append(str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
from services.llamaswap import read_config, write_config
|
from services.llamaswap import read_config, write_config
|
||||||
from config import CONFIG_PATH
|
from config import CONFIG_PATH, SPEC_DRAFT_MODEL_PATH
|
||||||
|
|
||||||
def migrate():
|
def migrate():
|
||||||
print(f"Reading config from {CONFIG_PATH}...")
|
print(f"Reading config from {CONFIG_PATH}...")
|
||||||
@@ -15,9 +15,9 @@ def migrate():
|
|||||||
|
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
models = cfg.get("models", {})
|
models = cfg.get("models", {})
|
||||||
|
|
||||||
draft_path = "/srv/models/drafts/qwen2.5-1.5b-instruct-q4_k_m.gguf"
|
draft_path = SPEC_DRAFT_MODEL_PATH
|
||||||
|
|
||||||
for name, spec in models.items():
|
for name, spec in models.items():
|
||||||
if not isinstance(spec, dict):
|
if not isinstance(spec, dict):
|
||||||
continue
|
continue
|
||||||
|
|||||||
+11
-54
@@ -5,6 +5,7 @@ Lokal (Windows) schlagen die Shell-Befehle harmlos fehl und werden als Fehler
|
|||||||
zurückgegeben statt zu crashen.
|
zurückgegeben statt zu crashen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -15,8 +16,12 @@ from config import GATEWAY_URL, HERMES_API_URL, HERMES_WEBUI_URL, LLAMA_SWAP_URL
|
|||||||
from services import backup as backup_svc
|
from services import backup as backup_svc
|
||||||
from services.agent import agent_status
|
from services.agent import agent_status
|
||||||
from services.gateway import gateway_reachable
|
from services.gateway import gateway_reachable
|
||||||
from services.llamaswap import engine_reachable
|
from services.llamaswap import engine_reachable, list_models
|
||||||
|
from services.pricing import compute_savings
|
||||||
from services.system import system_status
|
from services.system import system_status
|
||||||
|
from services.token_stats import get_stats
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
@@ -90,64 +95,16 @@ def self_update() -> dict:
|
|||||||
return {"pull": pull, "reset": reset, "restart": restart_res}
|
return {"pull": pull, "reset": reset, "restart": restart_res}
|
||||||
|
|
||||||
|
|
||||||
from services.token_stats import get_stats
|
|
||||||
from services.llamaswap import list_models
|
|
||||||
|
|
||||||
@router.get("/system/token-stats")
|
@router.get("/system/token-stats")
|
||||||
def token_stats() -> dict:
|
def token_stats() -> dict:
|
||||||
stats = get_stats()
|
"""Token-Verbrauch + Cloud-Ersparnis. Logik im pricing-Service (SSoT)."""
|
||||||
p = stats.get("prompt_tokens", 0)
|
# Rolle je Modell/Alias (lowercase) für die Tarif-Auflösung auflösen.
|
||||||
c = stats.get("completion_tokens", 0)
|
role_map: dict[str, str | None] = {}
|
||||||
total = p + c
|
|
||||||
|
|
||||||
# Map model IDs and aliases to their respective roles for pricing resolution
|
|
||||||
role_map = {}
|
|
||||||
try:
|
try:
|
||||||
for m in list_models():
|
for m in list_models():
|
||||||
role_map[m["name"].lower()] = m.get("role")
|
role_map[m["name"].lower()] = m.get("role")
|
||||||
for alias in m.get("aliases", []):
|
for alias in m.get("aliases", []):
|
||||||
role_map[alias.lower()] = m.get("role")
|
role_map[alias.lower()] = m.get("role")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
log.warning("token_stats: list_models fehlgeschlagen, Tarife per Name", exc_info=True)
|
||||||
|
return compute_savings(get_stats(), role_map)
|
||||||
# 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 {
|
|
||||||
"prompt_tokens": p,
|
|
||||||
"completion_tokens": c,
|
|
||||||
"total_tokens": total,
|
|
||||||
"saved_usd": round(saved_usd, 2),
|
|
||||||
"saved_eur": round(saved_eur, 2)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import re
|
|||||||
import httpx
|
import httpx
|
||||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||||
|
|
||||||
from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL
|
from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, SPEC_DRAFT_MODEL_PATH
|
||||||
|
|
||||||
# Kanonische Rollen (vereinheitlicht ggü. v1: kein manager/reviewer mehr).
|
# Kanonische Rollen (vereinheitlicht ggü. v1: kein manager/reviewer mehr).
|
||||||
ROLE_IDS = {"vision", "coder", "reasoning", "agent", "scout"}
|
ROLE_IDS = {"vision", "coder", "reasoning", "agent", "scout"}
|
||||||
@@ -188,9 +188,8 @@ def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
|||||||
if role_lower in ("fast", "coder"):
|
if role_lower in ("fast", "coder"):
|
||||||
if "--parallel" not in cmd:
|
if "--parallel" not in cmd:
|
||||||
cmd += " --parallel 2"
|
cmd += " --parallel 2"
|
||||||
draft_path = "/srv/models/drafts/qwen2.5-1.5b-instruct-q4_k_m.gguf"
|
if os.path.exists(SPEC_DRAFT_MODEL_PATH) and "--spec-draft-model" not in cmd:
|
||||||
if os.path.exists(draft_path) and "--spec-draft-model" not in cmd:
|
cmd += f" --spec-draft-model {SPEC_DRAFT_MODEL_PATH}"
|
||||||
cmd += f" --spec-draft-model {draft_path}"
|
|
||||||
|
|
||||||
cfg.setdefault("models", {})[model_id] = {
|
cfg.setdefault("models", {})[model_id] = {
|
||||||
"cmd": LiteralScalarString(cmd + "\n"),
|
"cmd": LiteralScalarString(cmd + "\n"),
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Kosten-/Ersparnis-Berechnung für die Token-Statistik (eine Quelle der Wahrheit).
|
||||||
|
|
||||||
|
Vergleicht die lokal verbrauchten Tokens gegen die Cloud-Listenpreise vergleichbarer
|
||||||
|
Modellklassen (Stand Juni 2026, USD pro 1M Tokens, in/out) und liefert die so
|
||||||
|
eingesparte Summe. Wird vom System-Router dünn aufgerufen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Cloud-Listenpreise je Rolle/Modellklasse: (input_usd_per_1M, output_usd_per_1M).
|
||||||
|
PRICING: dict[str, tuple[float, float]] = {
|
||||||
|
"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),
|
||||||
|
}
|
||||||
|
# Tarif für nicht zuordenbare Tokens (Default-/Fallback-Klasse).
|
||||||
|
DEFAULT_RATE: tuple[float, float] = (0.15, 0.60)
|
||||||
|
# Baseline/Legacy-Tokens (vor modellspezifischem Logging) am Premium-Tarif bewerten,
|
||||||
|
# damit historische Ersparnis erhalten bleibt.
|
||||||
|
BASELINE_RATE: tuple[float, float] = PRICING["heavy"]
|
||||||
|
USD_TO_EUR = float(os.environ.get("MC_USD_TO_EUR", "0.92"))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_savings(stats: dict, role_map: dict[str, str | None]) -> dict:
|
||||||
|
"""Aggregiert Tokens und berechnet die Cloud-Ersparnis.
|
||||||
|
|
||||||
|
role_map: Modell-/Alias-Name (lowercase) -> Rolle, zur Tarif-Auflösung.
|
||||||
|
"""
|
||||||
|
prompt = stats.get("prompt_tokens", 0)
|
||||||
|
completion = stats.get("completion_tokens", 0)
|
||||||
|
|
||||||
|
modeled_p = modeled_c = 0
|
||||||
|
saved_usd = 0.0
|
||||||
|
for m_name, m_tokens in (stats.get("models") or {}).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, DEFAULT_RATE)
|
||||||
|
saved_usd += (mp * rate_in + mc * rate_out) / 1_000_000.0
|
||||||
|
|
||||||
|
baseline_p = max(0, prompt - modeled_p)
|
||||||
|
baseline_c = max(0, completion - modeled_c)
|
||||||
|
saved_usd += (baseline_p * BASELINE_RATE[0] + baseline_c * BASELINE_RATE[1]) / 1_000_000.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"prompt_tokens": prompt,
|
||||||
|
"completion_tokens": completion,
|
||||||
|
"total_tokens": prompt + completion,
|
||||||
|
"saved_usd": round(saved_usd, 2),
|
||||||
|
"saved_eur": round(saved_usd * USD_TO_EUR, 2),
|
||||||
|
"pricing": {role: {"in": r[0], "out": r[1]} for role, r in PRICING.items()},
|
||||||
|
}
|
||||||
+69
-69
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CJm59bcL.js"></script>
|
<script type="module" crossorigin src="/assets/index-BYvMJHPL.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function DashboardView() {
|
|||||||
total_tokens: number
|
total_tokens: number
|
||||||
saved_usd: number
|
saved_usd: number
|
||||||
saved_eur: number
|
saved_eur: number
|
||||||
|
pricing?: Record<string, { in: number; out: number }>
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// Sudo & Action states
|
// Sudo & Action states
|
||||||
@@ -777,7 +778,10 @@ export function DashboardView() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 text-[9px] text-muted-foreground/70 border-t border-border/30 pt-2.5 leading-normal">
|
<div className="mt-4 text-[9px] text-muted-foreground/70 border-t border-border/30 pt-2.5 leading-normal">
|
||||||
Berechnet im Vergleich zu Cloud-APIs von Juni 2026 (Ø 15,00 $ / 75,00 $ pro 1M tkn).
|
Berechnet im Vergleich zu Cloud-APIs von Juni 2026
|
||||||
|
{tokenStats?.pricing?.heavy
|
||||||
|
? ` (Ø ${tokenStats.pricing.heavy.in.toFixed(2).replace(".", ",")} $ / ${tokenStats.pricing.heavy.out.toFixed(2).replace(".", ",")} $ pro 1M tkn).`
|
||||||
|
: "."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user