Map Hermes reasoning controls into Qwen templates

This commit is contained in:
Mikei386
2026-08-25 14:52:43 +02:00
parent 7df28d9750
commit 63fc921988
6 changed files with 149 additions and 2 deletions
+2
View File
@@ -89,6 +89,8 @@ class Handler(BaseHTTPRequestHandler):
"total_tokens": 15},
"mock_ctx": current_ctx(),
"mock_authorization": self.headers.get("Authorization"),
"mock_reasoning_effort": body.get("reasoning_effort"),
"mock_chat_template_kwargs": body.get("chat_template_kwargs"),
}
def _stream(self, body: dict) -> None:
+19
View File
@@ -156,6 +156,25 @@ assert "Mock-Antwort" in d["choices"][0]["message"]["content"], d
assert d.get("mock_authorization") is None, d
' && ok "Request wurde weitergeleitet, Modell ersetzt" || bad "Forwarding"
echo "== Test 3b: Hermes-Reasoning erreicht das llama.cpp-Chat-Template"
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
-d '{"model":"qwen-fast","reasoning_effort":"none","messages":[{"role":"user","content":"Hallo"}]}')
echo "$RESP" | python3 -c '
import json,sys
d=json.load(sys.stdin)
assert d.get("mock_reasoning_effort") is None, d
assert d.get("mock_chat_template_kwargs") == {"enable_thinking": False}, d
' && ok "none wird als enable_thinking=false weitergegeben" || bad "Reasoning none"
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
-d '{"model":"qwen-fast","reasoning_effort":"medium","messages":[{"role":"user","content":"Hallo"}]}')
echo "$RESP" | python3 -c '
import json,sys
d=json.load(sys.stdin)
assert d.get("mock_reasoning_effort") is None, d
assert d.get("mock_chat_template_kwargs") == {
"enable_thinking": True, "reasoning_effort": "medium"}, d
' && ok "medium erreicht chat_template_kwargs" || bad "Reasoning medium"
# --- 4. Streaming ----------------------------------------------------------------
echo "== Test 4: Streaming (SSE)"
RESP=$(curl -sfN "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
+50
View File
@@ -27,6 +27,7 @@ from ai_profile_router import ( # noqa: E402
_context_matches,
_normalize_chat_image,
_normalize_chat_images,
_normalize_llamacpp_reasoning,
_request_has_image,
)
@@ -126,6 +127,55 @@ class ChatImageInputTests(unittest.TestCase):
self.assertEqual(request, normalized)
class LlamaCppReasoningTests(unittest.TestCase):
def test_none_really_disables_thinking(self) -> None:
request = {"reasoning_effort": "none", "messages": []}
normalized = _normalize_llamacpp_reasoning(request)
self.assertNotIn("reasoning_effort", normalized)
self.assertEqual(
normalized["chat_template_kwargs"],
{"enable_thinking": False},
)
def test_low_and_medium_reach_chat_template(self) -> None:
for effort in ("low", "medium"):
with self.subTest(effort=effort):
request = {"reasoning_effort": effort, "messages": []}
normalized = _normalize_llamacpp_reasoning(request)
self.assertEqual(normalized["chat_template_kwargs"], {
"enable_thinking": True,
"reasoning_effort": effort,
})
def test_unsupported_high_levels_are_clamped_to_xhigh(self) -> None:
for effort in ("high", "xhigh", "max", "ultra"):
with self.subTest(effort=effort):
request = {"reasoning_effort": effort, "messages": []}
normalized = _normalize_llamacpp_reasoning(request)
self.assertEqual(
normalized["chat_template_kwargs"]["reasoning_effort"],
"xhigh",
)
def test_existing_template_kwargs_are_preserved(self) -> None:
request = {
"reasoning_effort": "low",
"chat_template_kwargs": {"preserve_thinking": True},
"messages": [],
}
normalized = _normalize_llamacpp_reasoning(request)
self.assertEqual(normalized["chat_template_kwargs"], {
"preserve_thinking": True,
"enable_thinking": True,
"reasoning_effort": "low",
})
def test_request_without_effort_is_unchanged(self) -> None:
request = {"messages": []}
self.assertIs(_normalize_llamacpp_reasoning(request), request)
self.assertNotIn("chat_template_kwargs", request)
class RetentionTests(unittest.TestCase):
def test_oldest_pairs_are_removed(self) -> None:
with tempfile.TemporaryDirectory() as temp:
+11 -2
View File
@@ -78,6 +78,13 @@ Zielplattform.
Der Router übernimmt:
- OpenAI-kompatibles Chat-Proxying und Streaming
- Übersetzung von OpenAI-/Hermes-`reasoning_effort` in die vom
Qwen3.8-Jinja-Template tatsächlich ausgewerteten
`chat_template_kwargs`: `none` deaktiviert Thinking mit
`enable_thinking: false`; `low` und `medium` bleiben erhalten; höhere
Client-Stufen werden auf das vom Modell unterstützte `xhigh` begrenzt.
Das ist absichtlich zentral im Router implementiert, damit Hermes,
OpenWebUI und weitere OpenAI-kompatible Clients identisches Verhalten haben.
- virtuelle Modelle und automatische Profilumschaltung
- Tool Calls
- direkte integrierte Vision in Fast, Medium, Large und Uncensored
@@ -98,8 +105,10 @@ Der Router übernimmt:
- Die Kompressionszusammenfassung nutzt weiterhin dasselbe aktive Modell. Ein
kleineres Fast-Modell wäre zwar schneller, besitzt aber nicht genug Kontext,
um die vollständige Mitte einer Medium-, Large- oder Ultra-Sitzung sicher zu
verarbeiten. `reasoning_effort: none` vermeidet unnötiges Nachdenken beim
reinen Zusammenfassen.
verarbeiten. `reasoning_effort: none` wird vom Router in
`enable_thinking: false` übersetzt und vermeidet damit nachweislich
unnötiges Nachdenken beim reinen Zusammenfassen. Ein unverändert an
llama.cpp gesendetes Top-Level-`reasoning_effort` wäre wirkungslos.
- `Summarizing thread` ist eine echte zusätzliche Modellanfrage. Bei sehr alten
Sitzungen kann die Desktop-Anzeige nach abgeschlossener Kompression außerdem
veraltet stehen bleiben. Maßgeblich sind dann Sitzungsfortschritt und
+7
View File
@@ -199,6 +199,13 @@ Open WebUI spricht nur mit dem Router auf dessen OpenAI-kompatibler `/v1`-API.
Ein direkter Zugriff auf llama.cpp würde Profilumschaltung, Authentisierung,
Vision-, Bild-, STT- und TTS-Routing umgehen.
Der Router ist außerdem die verbindliche Kompatibilitätsschicht für Thinking:
Clients senden das OpenAI-/Hermes-Feld `reasoning_effort`; der Router überführt
es in `chat_template_kwargs.reasoning_effort` beziehungsweise bei `none` in
`enable_thinking: false`. Diese Übersetzung darf bei einem Router-Umbau nicht
entfernt werden, weil llama.cpp das gleichnamige Top-Level-Feld nicht an das
Qwen3.8-Chat-Template weiterreicht.
Sichtbare Arbeitsbereichsmodelle sind Fast, Medium, Large, Ultra und
Uncensored. Die rohen `qwen-*`-Aliase bleiben ausgeblendet. Globale Filter
behandeln Reasoning, Thinking, Kontext-/Toolschleifen, Secret-Redaktion,
+60
View File
@@ -1108,6 +1108,64 @@ def _request_has_image(data: dict) -> bool:
)
# ---------------------------------------------------------------------------
# llama.cpp Chat-Template-Parameter
# ---------------------------------------------------------------------------
_REASONING_EFFORT_MAP = {
"minimal": "low",
"low": "low",
"medium": "medium",
"high": "xhigh",
"xhigh": "xhigh",
"max": "xhigh",
"ultra": "xhigh",
}
_REASONING_OFF = {"", "none", "off", "disabled", "false"}
def _normalize_llamacpp_reasoning(data: dict) -> dict:
"""Mappt OpenAI/Hermes-Reasoning auf llama.cpp-Template-Parameter.
Hermes sendet ``reasoning_effort`` bei einem Custom Provider als
Top-Level-Feld. llama.cpp akzeptiert das Feld zwar, reicht es dort aber
nicht an das Jinja-Chat-Template weiter. Qwen3.8 erwartet stattdessen
``chat_template_kwargs.reasoning_effort`` bzw. ``enable_thinking=false``.
Die Funktion verändert den übergebenen Request absichtlich in-place und
entfernt das wirkungslose Top-Level-Feld. Andere Template-Argumente des
Clients bleiben erhalten.
"""
if "reasoning_effort" not in data:
return data
raw_effort = data.pop("reasoning_effort")
effort = str(raw_effort).strip().lower() if raw_effort is not None else ""
template_kwargs = data.get("chat_template_kwargs")
if not isinstance(template_kwargs, dict):
template_kwargs = {}
data["chat_template_kwargs"] = template_kwargs
if effort in _REASONING_OFF:
template_kwargs.pop("reasoning_effort", None)
template_kwargs["enable_thinking"] = False
return data
mapped = _REASONING_EFFORT_MAP.get(effort)
if mapped is None:
# Unbekannte OpenAI-Erweiterungen dürfen das Qwen-Template nicht mit
# einem ungültigen Wert zum Abbruch bringen. Das Template verwendet
# in diesem Fall seine eigene Voreinstellung.
log.warning("Unbekanntes reasoning_effort=%r ignoriert", raw_effort)
if not template_kwargs:
data.pop("chat_template_kwargs", None)
return data
template_kwargs["enable_thinking"] = True
template_kwargs["reasoning_effort"] = mapped
return data
# ---------------------------------------------------------------------------
# HTTP-Handler
# ---------------------------------------------------------------------------
@@ -1916,6 +1974,8 @@ class Handler(BaseHTTPRequestHandler):
if profile is not None:
switch_profile(profile, implicit=True)
data = _normalize_llamacpp_reasoning(data)
if _request_has_image(data):
data = _normalize_chat_images(data)
log.info("Vision: Bild wird direkt an das aktive "