Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,8 @@ ds4>

The interactive CLI is a real multi-turn chat. It keeps the rendered chat
transcript and the live graph KV checkpoint, so each turn extends the previous
conversation. Useful commands are `/help`, `/think`, `/think-max`, `/nothink`,
conversation. Useful commands are `/help`, `/think`, `/think-max`,
`/think-ultra`, `/nothink`,
`/ctx N`, `/read FILE`, and `/quit`. Ctrl+C interrupts the current generation
and returns to `ds4>`.

Expand Down Expand Up @@ -1273,11 +1274,39 @@ the saved prefix instead of processing the whole prompt again.

## Thinking Modes

DeepSeek V4 Flash has distinct non-thinking, thinking, and Think Max modes.
The server defaults to thinking mode. `reasoning_effort=max` requests Think
Max, but it is only applied when the context size is large enough for the model
card recommendation; smaller contexts fall back to normal thinking. OpenAI
`reasoning_effort=xhigh` still maps to normal thinking, not Think Max.
DeepSeek V4 Flash has a non-thinking mode plus three reasoning-effort tiers,
rendered as a plain-text prefix at the very start of the conversation. The
0731 release added the top one; the tier names in the ds4 source are historical
and do not line up 1:1 with DeepSeek's own names:

| ds4 tier | DeepSeek name | prompt prefix |
|---|---|---|
| `DS4_THINK_NONE` | - | non-thinking, no prefix |
| `DS4_THINK_HIGH` | `low` | none (this is DeepSeek's default) |
| `DS4_THINK_MAX` | `high` | "Reasoning Effort: Absolute maximum ..." |
| `DS4_THINK_ULTRA` | `max` | "Reasoning Effort: Beyond maximum ..." |

The server defaults to the unprefixed thinking tier, matching DeepSeek's own
`low` default. Wire `reasoning_effort` names map on as follows:

| `reasoning_effort` | `deepseek` map (default) | `legacy` map |
|---|---|---|
| `minimal`, `low`, `medium` | high | high |
| `high`, `xhigh` | max | high |
| `max` | ultra | max |
| `none` | none | none |

`--reasoning-effort-map legacy` restores the pre-0731 ds4 mapping exactly, for
prompts that were tuned against it. Unknown names are rejected with HTTP 400 in
both maps.

The prefixed tiers are only applied when the context is at least
`--think-effort-min-ctx` (default 393216, the model card recommendation);
below that the tier steps down one level (ultra to max, max to high), so a
small context loses one notch of effort rather than all of it.

On the command line the tiers are `--think`, `--think-max` and `--think-ultra`
(`/think`, `/think-max`, `/think-ultra` in the REPL).

For direct replies, use `thinking: {"type":"disabled"}`, `think:false`, or a
non-thinking model alias such as `deepseek-chat`.
Expand Down
98 changes: 80 additions & 18 deletions ds4.c
Original file line number Diff line number Diff line change
Expand Up @@ -382,16 +382,39 @@ int g_gpu_peer_ok[DS4_MAX_GPUS][DS4_MAX_GPUS];
#define DS4_DEFAULT_COMPRESS_ROPE_FREQ_BASE (160000.0f)
#define DS4_DEFAULT_ROPE_ORIG_CTX UINT64_C(65536)

/* Reasoning-effort prompt prefixes, copied byte-for-byte from DeepSeek's own
* REASONING_EFFORT_PROMPTS in encoding/encoding_dsv4.py (DeepSeek-V4-Flash-0731).
* DeepSeek's "low" tier is the empty string, so only two prefixes exist.
*
* DeepSeek "high" -> DS4_THINK_MAX (this file's ..._MAX_PREFIX)
* DeepSeek "max" -> DS4_THINK_ULTRA (this file's ..._ULTRA_PREFIX, added
* for 0731; it did not exist before)
*
* The ULTRA string contains an em dash (U+2014, bytes E2 80 94) after
* "Beyond maximum". It is part of the tokenised prompt: do not "normalise" it
* to a hyphen and keep this file UTF-8. */
static const char DS4_REASONING_EFFORT_MAX_PREFIX[] =
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
"You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
"Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n";

/* DeepSeek recommends Think Max only with at least a 384K-token context window.
* Below that size we keep ordinary thinking to avoid injecting a prompt that
* asks for a reasoning budget the allocated context is not meant to hold. */
static const char DS4_REASONING_EFFORT_ULTRA_PREFIX[] =
"Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n"
"You MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\n"
"Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n";

/* DeepSeek recommends the prefixed tiers only with at least a 384K-token
* context window. Below that size we step the tier down so we never inject a
* prompt that asks for a reasoning budget the allocated context is not meant
* to hold. */
#define DS4_THINK_MAX_MIN_CONTEXT 393216u

/* Runtime-settable so an operator can lower or raise the floor without a
* rebuild. The default is DeepSeek's recommendation and is what every tool
* uses unless --think-effort-min-ctx says otherwise. Set once during argument
* parsing, before any request is served. */
static uint32_t g_think_effort_min_context = DS4_THINK_MAX_MIN_CONTEXT;

static bool ds4_backend_uses_graph(ds4_backend backend) {
return backend == DS4_BACKEND_METAL || backend == DS4_BACKEND_CUDA;
}
Expand Down Expand Up @@ -36768,9 +36791,13 @@ static void chat_push_bos_sequence(const ds4_vocab *vocab, token_vec *out) {

const char *ds4_glm_reasoning_effort_text(ds4_think_mode mode) {
switch (mode) {
case DS4_THINK_HIGH: return "Reasoning Effort: High";
case DS4_THINK_MAX: return "Reasoning Effort: Max";
case DS4_THINK_NONE: return NULL;
case DS4_THINK_HIGH: return "Reasoning Effort: High";
case DS4_THINK_MAX: return "Reasoning Effort: Max";
/* GLM exposes no tier above "Max", so ULTRA saturates there. This switch
* has no default: an unlisted enumerator returns NULL and drops the effort
* text with no diagnostic. */
case DS4_THINK_ULTRA: return "Reasoning Effort: Max";
case DS4_THINK_NONE: return NULL;
}
return NULL;
}
Expand All @@ -36784,8 +36811,13 @@ static void chat_push_think_prefix(const ds4_vocab *vocab,
token_vec_push(out, vocab->system_id);
bpe_tokenize_text(vocab, effort, out);
}
} else if (think_mode == DS4_THINK_MAX) {
bpe_tokenize_text(vocab, DS4_REASONING_EFFORT_MAX_PREFIX, out);
} else {
/* Tier -> prefix comes from ds4_think_effort_prefix(), which returns ""
* (never NULL) for the unprefixed tiers, so this stays a single call
* site as tiers are added. */
const char *effort_prefix = ds4_think_effort_prefix(think_mode);
if (effort_prefix[0])
bpe_tokenize_text(vocab, effort_prefix, out);
}
}

Expand Down Expand Up @@ -36921,8 +36953,14 @@ void ds4_encode_chat_prompt(
encode_chat_prompt(&e->vocab, system, prompt ? prompt : "", think_mode, out);
}

void ds4_chat_append_effort_prefix(ds4_engine *e, ds4_tokens *tokens, ds4_think_mode mode) {
const char *prefix = ds4_think_effort_prefix(mode);
if (!prefix[0]) return;
bpe_tokenize_text(&e->vocab, prefix, tokens);
}

void ds4_chat_append_max_effort_prefix(ds4_engine *e, ds4_tokens *tokens) {
bpe_tokenize_text(&e->vocab, DS4_REASONING_EFFORT_MAX_PREFIX, tokens);
ds4_chat_append_effort_prefix(e, tokens, DS4_THINK_MAX);
}

static void bpe_tokenize_wrapped_payload_text(ds4_vocab *vocab, const char *content,
Expand Down Expand Up @@ -47999,31 +48037,55 @@ static void ds4_linux_graph_backend_set_oom_score(ds4_backend backend) {
#endif
}

/* Every tier above NONE is a thinking tier. ULTRA MUST be listed here: this
* predicate gates the <think> opener, the streaming reasoning channels and the
* tool-marker handling at ~25 call sites, and omitting a new tier disables
* thinking for it everywhere at once without a single compiler warning. */
bool ds4_think_mode_enabled(ds4_think_mode mode) {
return mode == DS4_THINK_HIGH || mode == DS4_THINK_MAX;
return mode == DS4_THINK_HIGH || mode == DS4_THINK_MAX ||
mode == DS4_THINK_ULTRA;
}

const char *ds4_think_mode_name(ds4_think_mode mode) {
switch (mode) {
case DS4_THINK_NONE: return "none";
case DS4_THINK_HIGH: return "high";
case DS4_THINK_MAX: return "max";
case DS4_THINK_NONE: return "none";
case DS4_THINK_HIGH: return "high";
case DS4_THINK_MAX: return "max";
case DS4_THINK_ULTRA: return "ultra";
}
return "unknown";
}

const char *ds4_think_effort_prefix(ds4_think_mode mode) {
switch (mode) {
case DS4_THINK_NONE: return "";
case DS4_THINK_HIGH: return "";
case DS4_THINK_MAX: return DS4_REASONING_EFFORT_MAX_PREFIX;
case DS4_THINK_ULTRA: return DS4_REASONING_EFFORT_ULTRA_PREFIX;
}
return "";
}

const char *ds4_think_max_prefix(void) {
return DS4_REASONING_EFFORT_MAX_PREFIX;
return ds4_think_effort_prefix(DS4_THINK_MAX);
}

uint32_t ds4_think_max_min_context(void) {
return DS4_THINK_MAX_MIN_CONTEXT;
return g_think_effort_min_context;
}

void ds4_think_set_effort_min_context(uint32_t min_context) {
g_think_effort_min_context = min_context;
}

/* Step the tier down ONE level when the context is too small, so ULTRA in a
* small context still reasons at MAX rather than collapsing all the way to the
* unprefixed tier. */
ds4_think_mode ds4_think_mode_for_context(ds4_think_mode mode, int ctx_size) {
if (mode == DS4_THINK_MAX && (uint32_t)(ctx_size > 0 ? ctx_size : 0) < DS4_THINK_MAX_MIN_CONTEXT) {
return DS4_THINK_HIGH;
}
const uint32_t ctx = (uint32_t)(ctx_size > 0 ? ctx_size : 0);
if (ctx >= g_think_effort_min_context) return mode;
if (mode == DS4_THINK_ULTRA) return DS4_THINK_MAX;
if (mode == DS4_THINK_MAX) return DS4_THINK_HIGH;
return mode;
}

Expand Down
23 changes: 23 additions & 0 deletions ds4.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,22 @@ typedef enum {
DS4_BACKEND_CPU,
} ds4_backend;

/* Reasoning-effort tiers. The enumerators are APPENDED, never renumbered or
* renamed: the existing names are spelled out in ~120 literals across the
* server, CLI, agent and eval code, so a rename would silently re-point every
* one of them. The names are historical and do NOT line up 1:1 with DeepSeek's
* own tier names; ds4_think_effort_prefix() below is the mapping of record.
*
* DS4_THINK_NONE no thinking at all (</think> opener, no prefix)
* DS4_THINK_HIGH thinking with no prompt prefix == DeepSeek "low"
* DS4_THINK_MAX "Reasoning Effort: Absolute maximum" == DeepSeek "high"
* DS4_THINK_ULTRA "Reasoning Effort: Beyond maximum" == DeepSeek "max"
*/
typedef enum {
DS4_THINK_NONE,
DS4_THINK_HIGH,
DS4_THINK_MAX,
DS4_THINK_ULTRA,
} ds4_think_mode;

typedef enum {
Expand Down Expand Up @@ -252,9 +264,18 @@ bool ds4_engine_is_glm_dsa(ds4_engine *e);
const char *ds4_backend_name(ds4_backend backend);
bool ds4_think_mode_enabled(ds4_think_mode mode);
const char *ds4_think_mode_name(ds4_think_mode mode);
/* Prompt prefix for a tier. Returns "" (never NULL) for NONE and HIGH, so
* callers can append unconditionally. */
const char *ds4_think_effort_prefix(ds4_think_mode mode);
/* Compatibility shim: the pre-tier single-constant accessor, == the
* DS4_THINK_MAX prefix. Prefer ds4_think_effort_prefix(). */
const char *ds4_think_max_prefix(void);
const char *ds4_glm_reasoning_effort_text(ds4_think_mode mode);
uint32_t ds4_think_max_min_context(void);
/* Context floor below which ds4_think_mode_for_context() steps a tier down.
* Defaults to DeepSeek's recommended 393216. Call during argument parsing,
* before any request is served: it is a process-wide setting. */
void ds4_think_set_effort_min_context(uint32_t min_context);
ds4_think_mode ds4_think_mode_for_context(ds4_think_mode mode, int ctx_size);
/* Uses the active model shape selected by ds4_engine_open(); call after opening
* the GGUF so Flash/Pro dimensions are known. */
Expand Down Expand Up @@ -306,6 +327,8 @@ void ds4_encode_chat_prompt(
const char *prompt,
ds4_think_mode think_mode,
ds4_tokens *out);
void ds4_chat_append_effort_prefix(ds4_engine *e, ds4_tokens *tokens, ds4_think_mode mode);
/* Compatibility shim for ds4_chat_append_effort_prefix(.., DS4_THINK_MAX). */
void ds4_chat_append_max_effort_prefix(ds4_engine *e, ds4_tokens *tokens);
void ds4_chat_append_message(ds4_engine *e, ds4_tokens *tokens, const char *role, const char *content);
void ds4_chat_append_assistant_prefix(ds4_engine *e, ds4_tokens *tokens, ds4_think_mode think_mode);
Expand Down
17 changes: 14 additions & 3 deletions ds4_agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,15 @@ static agent_config parse_options(int argc, char **argv) {
c.gen.think_mode = DS4_THINK_HIGH;
} else if (!strcmp(arg, "--think-max")) {
c.gen.think_mode = DS4_THINK_MAX;
} else if (!strcmp(arg, "--think-ultra")) {
c.gen.think_mode = DS4_THINK_ULTRA;
} else if (!strcmp(arg, "--think-effort-min-ctx")) {
int v = parse_int(need_arg(&i, argc, argv, arg), arg);
if (v < 0) {
fprintf(stderr, "ds4-agent: --think-effort-min-ctx must be >= 0\n");
exit(2);
}
ds4_think_set_effort_min_context((uint32_t)v);
} else if (!strcmp(arg, "--nothink")) {
c.gen.think_mode = DS4_THINK_NONE;
} else if (!strcmp(arg, "--backend")) {
Expand Down Expand Up @@ -4381,9 +4390,11 @@ static void agent_worker_build_system_tokens(agent_worker *w, ds4_tokens *out) {
if (agent_tool_syntax_for_engine(w->engine) == AGENT_TOOL_SYNTAX_GLM) {
const char *effort = ds4_glm_reasoning_effort_text(think_mode);
if (effort) ds4_chat_append_message(w->engine, out, "system", effort);
} else if (w->cfg->gen.think_mode == DS4_THINK_MAX &&
think_mode == DS4_THINK_MAX) {
ds4_chat_append_max_effort_prefix(w->engine, out);
} else {
/* Use the context-adjusted tier: the prompt must carry the
* prefix the run will actually use. Appending is a no-op for
* the unprefixed tiers, so no per-tier test is needed. */
ds4_chat_append_effort_prefix(w->engine, out, think_mode);
}
agent_append_system_prompt(w->engine, out, w->cfg->gen.system);
}
Expand Down
Loading