Skip to content
thereprocase / All projectsSELF-HOSTED MODELSSource on GitHub ↗

SELF-HOSTED MODELS / FIELD GUIDE / OCTOBER 2026

DeepSeek V4.1 Flash on DGX Spark with Open WebUI.

A working setup for serving DeepSeek V4.1 Flash from DGX Spark-class machines to Open WebUI through a LiteLLM proxy, and measurements of the long planning loops the model falls into during tool-using chats.

The guide covers the stack, a reproducible way to measure the loop, which settings change it and which leave it alone, and a small proxy hook that restores the model's earlier reasoning between tool calls. Recommendations are grouped as must, should, consider and avoid.

Field report from one deployment, measured with small samples (3 to 16 runs per setting) on one prompt family. Re-measure on your own stack before relying on the numbers. Screenshots marked SIMULATED are illustrations with invented conversations; charts use the recorded measurements.

REPLAYS THAT LOOPED AT SERVER DEFAULTS
3 of 3
FULL REPLAYS THAT REACHED THE TOOL CALL WITH RECOMMENDED SETTINGS
6 of 6
SHORT REPLAYS WITHOUT IN-THINKING CODE, REMINDER ON (OFF: 5 OF 16)
13 of 16

01 / The stack

BrowserChat in Open WebUI.
Open WebUINative function calling; one OpenAI-compatible connection to the proxy.
LiteLLM + loopguardPer-model defaults, reasoning replay, trailing reminder, loop metrics. Bound to localhost.
vLLM on SparkDeepSeek V4.1 Flash, OpenAI-compatible API on port 8000.
Hardware
DGX Spark-class GB10 machines, one model per machine. The front end and proxy run on a separate small server and reach the Sparks over a private network (Tailscale in the measured deployment).
Serving
vLLM nightly (September–October 2026) with the DeepSeek V4.1 tokenizer, reasoning and tool-call parsers from the vLLM recipe. Speculative decoding with 3 draft tokens; about 65% of drafts accepted.
Observed throughput
About 22 output tokens per second per stream, with two sequences running at once. A long tool-using turn of 10,000–20,000 tokens therefore takes 8–15 minutes on one stream.
Proxy
LiteLLM Proxy v1.99.4, non-root image pinned by digest, config-file mode with no database. The loopguard callback is one Python file mounted read-only.
Front end
Open WebUI with native function calling and its built-in tools (notes, memory, chat search, knowledge bases).

02 / The planning loop

When a turn ends in one large deliverable — for example, “write a full project scaffold into a note” — the model's thinking fills with commitments to act (“Let me write it.”, “Writing now.”, “Now write.”) followed by more planning. In the measured deployment, three of three replays of such a conversation looped: 23–29 commitment phrases and 5,000–11,000 reasoning tokens before the first tool call, and one replay was still thinking when stopped at 15 minutes.

Simulated chat: a thinking panel running for over seven minutes, with highlighted phrases such as Let me write it and Writing now between planning paragraphs and code fragments
SIMULATED SCREENSHOT · Invented conversation. The highlighted phrases and in-thinking code fragments follow the pattern recorded in real traces.

Two features make the loop hard to catch. The text never repeats verbatim, so n-gram repetition detectors stay silent; and each commitment phrase is followed by new, plausible planning. In the recorded traces, 22–44% of the thinking text was code drafted inside the reasoning, against none in healthy thinking blocks from the same chat. The model also returns repeatedly to the length of the output it is about to write.

The model sometimes describes itself as Claude in these conversations. Other users report the same identity claim for the V4 Flash family; an identity line in the system prompt made no measurable difference to the loop here.

03 / How it was measured

Replays

The conversation state just before a looping turn was exported from Open WebUI — messages, tool calls and tool results — and replayed directly against vLLM with each setting under test. A scorer counts commitment phrases, words of thinking after the first one, and code blocks inside the thinking. Short replays stop after about 1,200 tokens; full replays stop shortly after the first tool call.

One-token exit probes

To measure the loop without long generations, a recorded trace is cut at each commitment phrase and sent to /v1/completions with the chat template's own rendering of the prompt, asking for one token and its top log-probabilities. The probability of the end-of-thinking token at that point is the model's propensity to stop planning there. Each probe costs one to three single-token requests, and the result is independent of sampling.

Line chart: probability of ending the thinking stays near zero for the first 21 commitment points, then rises to between 0.2 and 0.37 after about 2,400 words
Exit probability at each of the 26 commitment points in one recorded looping trace. The trace ended its thinking at the last point. Values are listed in the table below.

At 21 of the 26 points the model was almost certain to keep planning; the most likely next step was a paragraph break or another “I'll…” sentence. Willingness to stop appeared only after about 2,400 words, once much of the deliverable had been drafted in the thinking. Probing the same trace under different effort levels and system prompts moved these values by no more than the probe's own run-to-run variation (±0.005).

Exit probability at each commitment point
Words into thinkingP(end thinking)Words into thinkingP(end thinking)
1,2100.0002,2630.000
1,2310.0002,2650.000
1,2940.0002,2800.000
1,3870.0002,3020.000
1,4160.0002,3210.001
2,0380.0002,4020.041
2,0760.0002,4120.002
2,1010.0002,4260.000
2,1300.0002,4580.302
2,1360.0002,4710.197
2,1600.0002,5620.000
2,2260.0002,5660.008
2,2430.0002,6190.372

Tokenizer trap. DeepSeek V4.1 encodes “.”, “.\n” and “.\n\n” as separate tokens, and the choice among them carries the decision: after a bare “.” the model ends its thinking with probability above 0.99, after “.\n\n” almost never. Cut traces just before the final punctuation and sum over the punctuation branches. A first version of this probe cut after the period, which silently selected the exit branch and reported exit probabilities near 1.0 at points the live run had passed.

04 / Results

Short replays: first ~1,200 tokens

Two bar charts. Drafted code inside the thinking: server default 11 of 16, effort 75 6 of 16, section tools 6 of 8, trailing reminder 2 of 8, reminder plus section tools 1 of 8. Started the tool call within about 600 words: 1 of 16, 1 of 16, 0 of 8, 3 of 8, 3 of 8
Share of short replays that drafted code inside the thinking, and share that started the tool call within about 600 words of thinking.
SettingRunsDrafted code in thinkingStarted the tool callCommitment phrases per 1,000 words
Server default (effort 50)161116.9
Effort 7516616.1
Section-by-section note tools8606.5
Trailing reminder82311.3
Reminder + section tools8136.4

The trailing reminder is the one setting that changed the mechanism. With both reminder arms pooled against the default, 13 of 16 replays skipped in-thinking code drafts against 5 of 16 (Fisher one-sided p = 0.006), and 6 of 16 started the tool call within about 600 words against 1 of 16 (p = 0.04). The same instruction at the top of the system prompt had no measurable effect. Effort 75 against 50 showed a weaker signal on code drafting (6 of 16 against 11 of 16, p ≈ 0.08) and no change in commitment phrases. Section-by-section tools changed nothing; in the first 600 words only two of eight traces mentioned them, because drafting starts before the model plans delivery.

The reminder raises the count of commitment phrases while the drafting drops, so that count stops being a useful loop measure once the reminder is on. Measure code blocks in the thinking and words before the first tool call instead.

Full replays

Dot plot of thinking words before the first tool call. Server defaults: 2,619 and 4,906, plus one run still thinking at 2,230 words when stopped. Recommended settings: 354, 652, 1,509, 2,130, 2,447 and 2,563
Words of thinking before the first tool call in full replays. The hollow marker is a run stopped at 15 minutes before any tool call, so its true length is longer.

With the recommended settings (effort 75, temperature 1.0, top_p 0.95 and the trailing reminder), all six full replays reached the tool call, with a median of about 1,800 words of thinking against about 2,600 at defaults (Mann–Whitney one-sided p ≈ 0.05). The runs split into two groups: three acted after 350–1,500 words with little or no drafting, and three still drafted 7–17 code blocks before acting, in fewer words than at defaults.

05 / Recommendations

Each item notes whether it was measured in this deployment or comes from other sources.

Must

  1. Return the model's earlier reasoning on tool-calling turns. With tools present, the V4.1 chat encoding keeps every earlier turn's reasoning and renders an empty think block where it is missing. Open WebUI's Default and LiteLLM provider modes leave it out. Restore it in the proxy (the loopguard hook below) or switch the connection's provider to llama.cpp, which sends reasoning_content. Encoding behavior and Open WebUI behavior verified; effect on loops reported elsewhere.
  2. Run a LiteLLM release that keeps reasoning_content for hosted_vllm. Versions 1.100.0 through 1.104.0 remove it from assistant messages before they reach vLLM. v1.99.4 keeps it (verified by prompt-token counts through the proxy); the fix is in 1.105.0-rc.1.
  3. Pin the proxy image by digest and inspect it before first start. LiteLLM's PyPI releases 1.82.7 and 1.82.8 were compromised in March 2026. Check for the published indicators, run non-root with a read-only filesystem, and bind the proxy to localhost.
  4. Serve with the DeepSeek V4.1 parsers. Use the deepseek_v41 tokenizer mode, reasoning parser and tool-call parser from the vLLM recipe. The V4 tool parser misreads V4.1's tool tags. From the recipe and other reports; tool calls parsed correctly in this deployment.

Should

  1. Add a trailing system message right before the assistant turn. The text used here: “Keep your thinking to a short plan: what you will produce and in what order. Do not draft code or document text in your thinking. Write it directly in the tool call or the reply.” Measured: the largest effect of any setting.
  2. Set reasoning effort explicitly. Pass an integer from 1 to 100 in chat_template_kwargs.reasoning_effort. Some vLLM builds carry a pre-release effort table in which the default of 50 equals DeepSeek's “low”; DeepSeek's reference default is 75.
  3. Use temperature 1.0 and top_p 0.95. These are the model card's values.
  4. Log loop metrics for every response. Record words of thinking, code blocks in the thinking, commitment phrases and tool calls. Real traffic shows the loop rate better than replays.
  5. On shared Sparks, run experiments one request at a time. Check vllm:num_requests_running on /metrics before each batch.

Consider

  1. Turn thinking off for the step that writes a large deliverable. Send chat_template_kwargs: {"thinking": false} on that request and keep thinking on for planning. Other harnesses report fewer loops this way. Untested here.
  2. Leave flagged reasoning out of the replay. A looped thinking block in history can act as an example that sustains the loop (reported elsewhere); loopguard replays only blocks below its loop thresholds.
  3. Compare settings with one-token exit probes. Each probe costs one to three single-token requests, which keeps experiments light on a shared machine.
  4. Use a mid-conversation system message as the reminder slot. The V4.1 encoding defines a latest_reminder role; vLLM rejected it in the measured build, and a trailing system message renders in the same position.

Avoid

  1. thinking_token_budget on builds where it truncates. It forces a bare end-of-thinking token, and several vLLM issues about it remain open.
  2. Relying on vLLM's repetition_detection for this loop. It matches exact repeated token patterns, and this loop never repeats verbatim.
  3. Placing the anti-drafting rule only at the top of the system prompt. No measurable effect here; position next to the decision matters.
  4. Cutting probe traces after punctuation. The tokenizer merges punctuation with line breaks, so the cut selects a branch for the model.
  5. Upgrading the proxy to the newest release without checking reasoning passthrough. Repeat the prompt-token check below after every upgrade.

06 / Configuration

LiteLLM model entry

config.yaml
model_list:
  - model_name: deepseek-v4.1-flash
    litellm_params:
      model: hosted_vllm/deepseek-v4.1-flash
      api_base: http://SPARK_HOST:8000/v1
      api_key: "none"
      temperature: 1.0
      top_p: 0.95
      extra_body:
        chat_template_kwargs:
          reasoning_effort: 75

litellm_settings:
  callbacks: loopguard.proxy_handler_instance
  drop_params: false

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

Container

docker run
docker run -d --name litellm --restart unless-stopped --network host \
  --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges \
  --env-file /opt/litellm/litellm.env --env-file /opt/litellm/loopguard.env \
  -e LITELLM_LOCAL_MODEL_COST_MAP=True \
  -v /opt/litellm/config.yaml:/app/config.yaml:ro \
  -v /opt/litellm/loopguard.py:/app/loopguard.py:ro \
  -v /opt/litellm/data:/data \
  ghcr.io/berriai/litellm-non_root@sha256:63dfaf8232b7b524bc2ebca12bd77e997baec0179719c340443c74f14fd8822e \
  --config /app/config.yaml --host 127.0.0.1 --port 4000

The digest is litellm-non_root:v1.99.4 as of October 2026; re-resolve it from the registry before use. litellm.env holds only the master key (mode 600). loopguard.env sets the reminder text and the log path. Host networking with a localhost bind keeps the proxy reachable only from the front-end machine; Open WebUI then uses http://127.0.0.1:4000/v1 with the master key and the Default provider.

What loopguard does

  • Caches each streamed response's reasoning, keyed by tool-call ID and a hash of the visible reply, and re-attaches it to the matching assistant message on later requests.
  • Leaves out any reasoning block that crosses its loop thresholds (six or more commitment phrases, or 800 or more words after the first one).
  • Appends the trailing reminder as a system message when LOOPGUARD_TAIL_REMINDER is set.
  • Writes one JSON line of loop metrics per response for the configured models, and passes every chunk through unchanged.
Simulated chat: thinking finished after 19 seconds with a one-paragraph plan, a one-line visible plan, and a write_note tool card
SIMULATED SCREENSHOT · The same invented request with the recommended settings: a short plan, then the tool call.
Simulated terminal: six JSON lines of loop metrics, one flagged, and a count of flagged responses
SIMULATED SCREENSHOT · INVENTED VALUES · Reading the loop log with jq.

Checks after install

  1. Reasoning passthrough. Send the same tool-call conversation twice with max_tokens: 1, once with reasoning_content on the assistant turn and once without, directly to vLLM and through the proxy. The prompt-token difference must match (17 tokens in the measured setup).
  2. Replay. Let the model make a real tool call through the proxy, then send the follow-up without reasoning; the proxied request should carry more prompt tokens than the direct one.
  3. Defaults. Start a throwaway instance with --detailed_debug on another port, send one request, and confirm chat_template_kwargs, temperature and top_p in the logged upstream request. Remove the instance afterwards; its log contains prompt text.
  4. Reminder. A one-word prompt costs more prompt tokens through the proxy than directly (31 against 71 here).

07 / Limits

  • One prompt family (a large code scaffold written into a note through a tool) and one deployment. Other deliverables and other serving stacks may behave differently.
  • Small samples: 3 to 16 runs per setting. The p-values are one-sided and uncorrected for the number of settings tried.
  • Answer quality was read in samples and was not scored.
  • Exit probes on a recorded trace are off-policy: they measure the propensity to stop given that history, which is the quantity compared across settings.
  • The thinking-off writing step, speculative decoding settings and the proxy's behavior on aborted streams were not tested.

08 / Files & sources

Files

The replay cases themselves are private conversations and are not published; rig.py takes any case file of the form {"name", "messages", "tools"}.

Sources