reasoning passthrough: enable_thinking for configured local models

Reasoning-capable local backends (deepseek-v4-flash-dspark on bosch) only emit a
separate reasoning stream when the request carries chat_template_kwargs.enable_thinking.
The proxy never asked, so clients (bullpen, OWUI) got reasoning inlined into content or
dropped. Now: if the routed local model matches /etc/llm-proxy/reasoning-models.json
(a JSON list of substrings; absent=off), inject enable_thinking. The byte-passthrough
relay already forwards the reasoning deltas untouched. Verified: dspark now streams
reasoning deltas through :8082.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Markus Fritsche
2026-07-24 14:39:46 +02:00
parent cc3be17994
commit 484296b277
2 changed files with 30 additions and 0 deletions
+29
View File
@@ -75,6 +75,25 @@ def _load_friendly_names():
FRIENDLY_NAMES = _load_friendly_names()
# Optional list of local model-id substrings whose thinking should be split into a
# separate `reasoning` stream (via the chat-template `enable_thinking` kwarg), so
# clients can surface the model's reasoning instead of having it inlined or dropped.
# From $LLM_PROXY_CONFIG_DIR/reasoning-models.json (a JSON list of substrings).
# Absent / empty = feature off, so this changes nothing unless configured.
def _load_reasoning_models():
try:
with open(os.path.join(CONFIG_DIR, "reasoning-models.json")) as f:
data = json.load(f)
return [s for s in data if isinstance(s, str)] if isinstance(data, list) else []
except Exception:
return []
REASONING_LOCAL_MODELS = _load_reasoning_models()
def _is_reasoning_local(model):
return bool(model) and any(sub in model for sub in REASONING_LOCAL_MODELS)
CONNECT_TIMEOUT = 3
READ_TIMEOUT = 1800
DISCOVERY_TTL = 30
@@ -992,6 +1011,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
total_chars = sum(len(str(m.get("content", ""))) for m in j.get("messages", []))
stream = j.get("stream", False)
hint = f" model={model} msgs={n_msgs} in_chars={total_chars} max_tok={max_t} stream={stream}"
# reasoning passthrough: ask reasoning-capable local models to split
# their thinking into a separate `reasoning` stream (chat-template kwarg),
# so clients can surface it. The byte-relay forwards the field untouched.
if _is_reasoning_local(model):
ck = j.get("chat_template_kwargs")
ck = ck if isinstance(ck, dict) else {}
if "enable_thinking" not in ck:
ck["enable_thinking"] = True
j["chat_template_kwargs"] = ck
body = json.dumps(j).encode()
except Exception:
pass
+1
View File
@@ -0,0 +1 @@
["dspark"]