2026-06-17 DeepSeek Empty Replies via OpenRouter
What I set out to do
Figure out why SillyTavern was hitting a stored prompt and getting nothing back. Started from “check SigNoz for the most recent requests, check LiteLLM.”
What I actually did
Traced it end to end through SigNoz. Every hop was HTTP 200 (sillytavern → envoy → litellm-proxy → openrouter.ai), so it was never a transport failure — the completions were just empty: finish_reason=stop, output_tokens=0, content="", and usage=null. The empty ones were tagged response model openrouter/deepseek/deepseek-v4-pro (LiteLLM’s fallback label on a degenerate response); working ones came back as deepseek/deepseek-v4-pro.
Chased a few wrong theories and falsified them with data:
- Not a LiteLLM/config problem — only one
openrouter/*wildcard entry, nothing to “diff.” - Not the reasoning-strip handler — it only touches
<think>in assistant turns. - Not “no user message” alone — a working request had the same all-system shape.
Pulled the exact captured payload off the spans and replayed it straight to OpenRouter (proxy fully out of the path) with /tmp/or_replay.py + /tmp/or_variants.py. That nailed it: the failing prompts end on a system message, and DeepSeek (via OpenRouter, also GMICloud) returns empty for that shape. Adding any trailing user turn fixed it.
Root trigger: the “Gen Prompts” Quick Reply set — 21 bubbles (Betrayed, Uneasy, …) each run /impersonate [Generate {{user}}'s next message…], which injects its instruction as a trailing system message with no user turn.
Then searched ST docs + issues. Found #4595 (verbatim my bug: DeepSeek + System-role Utility Prompts on OpenRouter), #2507 / PR #3721. Fix is native and OpenRouter-specific.
What was striking
usage:nullon the empty responses is exactly why nothing showed in OpenRouter’s activity log — zero-billed generations aren’t logged. That dead end was itself a clue.- The same prompt shape worked for one card (Akera) and failed for another (Fiona) — the all-system shape is fragile, not uniformly broken, which is what kept throwing off the theories.
- The real fix was a single dropdown: Prompt Post-Processing = Strict on the OpenRouter connection. It merges to one leading system message and folds trailing instructions into a
userturn — replicating what the native DeepSeek API does. Confirmed live: Fiona “Uneasy” impersonate went 0 → 369 output tokens, all recent calls healthy.
Top 3 tomorrow
- Decide whether to also add the proxy-side backstop (re-role trailing
system→useron DeepSeek routes) for client-agnostic safety, or leave it client-side. - Clean up the throwaway
/tmp/or_*.pydiagnostic scripts. - Nothing else pressing on this thread — it’s resolved.
Related
Saved the full finding to auto-memory (project_sillytavern_deepseek_empty_trailing_system). Distinct from the earlier Continue/reasoning issue (ADR 0018).