Replies: 1 comment 3 replies
-
|
Need some time to understand this fully. While I agree that having flags only for specific models isn't nice I think for gemma the exception is fine as it is likely the one with Qwen that is going to be used everywhere (e.g. --mistral-thinking would be rejected random example). |
Beta Was this translation helpful? Give feedback.
3 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
llama.cpp "Think" Effort Levels — Integration Design for LATE
Status: Research complete. Architecture revised for production stability.
Target models: Gemma 4 and Qwen3.6
Context: Replace the current
--gemma-thinkingstring-concatenation hack with clean, API-level thinking control usingchat_template_kwargs.enable_thinking+thinking_budget_tokensbacked by context-aware clamping.TLDR
Replace
--gemma-thinking(string hack) with--thinking <mode>(API-level control). The llama.cpp server accepts two cooperating fields:chat_template_kwargs.enable_thinking(master on/off) andthinking_budget_tokens(token ceiling). Five modes:off,auto(ctx-dependent, default),low(1K),medium(4K),high(16K),xhigh(32K).The core system (P0):
ThinkingConfigstruct with*int Budget→buildExtraBody()constructs the payload with context-aware budget clamping. DynamicMaxTokensexpansion prevents truncation. Config via--thinkingflag orconfig.json(thinking_budget,late_subagent_thinking_budget).The upgrades (P1–P4):
Verified safe (no action): slot isolation between concurrent subagents, no attention drift at 16K reasoning, budget cutoff is exact (±2 tokens for UTF-8), tag injection guarded by token-level matching.
Summary
LATE targets Gemma 4 and Qwen3.6. Verified against the llama.cpp server source at
/mnt/storage/llama.cpp/tools/server/server-common.cpp.The API uses two cooperating fields in the
/v1/chat/completionsrequest body:chat_template_kwargs.enable_thinkingfalse= no[REASONING]/<think>markers.server-common.cpp:1071-1086thinking_budget_tokens0= immediate end,N= N tokens max,-1/omit = server default (unbounded).server-common.cpp:1119-1129--reasoning on|off--reasoning-formatnone(raw),deepseek(reasoning_content),deepseek-legacy(both). Default:auto.common/arg.cpp:3258-3267,common/chat.cpp:837-851Key finding from source: Both fields work cooperatively.
chat_template_kwargsis NOT silently ignored — it's explicitly parsed and forwarded to the Jinja engine.enable_thinkingis the master gate;thinking_budget_tokensis the ceiling.CLI:
--thinking off|auto|low|medium|high|xhighreplaces--gemma-thinking. Default isauto— proportional to the model's context window.The Problem
Legacy Hack
The current implementation forcibly prepends
<|think|>to the system prompt.The Upstream Reality
The llama.cpp server exposes two cooperating controls:
chat_template_kwargs.enable_thinking— parsed from body atserver-common.cpp:1079-1086. Passed to the Jinja template to control whether<think>/[REASONING]tags are rendered at all. Works for both Gemma 4 and Qwen3.6.thinking_budget_tokens— parsed from body atserver-common.cpp:1119. Sets a hard token ceiling on reasoning output. Only activates when the template produced athinking_end_tag.You must set both:
enable_thinkingtoggles the template rendering;thinking_budget_tokenscaps the reasoning block length.The Design
1. The Payload
Every thinking-enabled request sends both fields via
extraBody(hoisted to root bymarshalFlattened):{ "model": "...", "messages": [...], "chat_template_kwargs": { "enable_thinking": true }, "thinking_budget_tokens": 4096 }2. Go Implementation —
ThinkingConfigTo avoid Go's zero-value trap (
0cannot be distinguished from "omitted"), the configuration uses a pointer (*int).nilmeans auto (ctx-dependent).0means disabled.>0means that budget.3. Dynamic MaxTokens Adjustment
Reasoning tokens consume the overall
max_tokens(aliased ton_predictin llama.cpp). If LATE requests 4,096 thinking tokens butmax_tokensdefaults to 2,048, the server truncates mid-thought. LATE must expand the ceiling.MaxTokensis passed throughextraBody["max_tokens"]sinceChatCompletionRequestdoesn't have aMaxTokensfield.marshalFlattenedhoists it to root.Key parameters:
ExpectedOutputSafetyMarginn_ctxoverflowReasoningEndOverhead\n + end_tag + \n(2–5 tokens real, 16 safe)4. Configuration & CLI Mapping
CLI flag:
--thinking <mode>replaces--gemma-thinking. Default isauto.enable_thinkingthinking_budget_tokensoff0false0autotruelow1024true1024medium4096true4096high16384true16384xhigh32768true32768Config
config.jsonschema (values overrideautomode):{ "thinking_budget": null, "late_subagent_thinking_budget": null }These integers map to
ThinkingConfig.Budget:nullor omitted → use auto mode (ctx-dependent budget)0→ thinking disabled (off)>0→ that exact budget (maps tolow/medium/high/xhightiers)5. Subagent Propagation
Subagents receive their own
ThinkingConfigvia the runner closure, allowing independent budgets (e.g., primary agent athigh, subagents atlowto preserve context).6. Verification / Observability
The stream response signals whether the budget is respected:
delta.reasoning_contentusage.completion_tokenslen(accumulated.Reasoning)/3.5Pre-flight detection: if the first N chunks contain no
reasoning_contentdespiteenable_thinking: true, emit a warning that the server may not support reasoning.Implementation Plan
sessionThinkingConfigstruct with*int Budget,ExpectedOutput,SafetyMargin. AddbuildExtraBody()method with auto mode and context-aware clamp.sessionMaxTokensexpansion withReasoningEndOverhead.cmd/late--thinking <mode>CLI parser. Remove--gemma-thinking. Defaultauto.configthinking_budgetandlate_subagent_thinking_budgetints to Config (default null).agentgemmaThinking boolparam with*ThinkingConfiginNewSubagentOrchestrator.session/persistenceSaveHistorywithAppendHistory. O(1) per save.session/persistencesaveAndNotify(). Zero-latency mutations.tool/search.gofilepath.WalkDirwith goroutine worker pool. ~8x speedup.tool/utils.gocmd.Stderrinstead of leaking to terminal.executor/cache.go(tool, args_hash)key with 30s TTL.executor/executor.gosession/persistencesearch/index.goNet Impact: Complete eradication of prompt hacking, correct API contract (two cooperating fields), guaranteed context safety via auto-clamping, O(1) session persistence, parallelized I/O throughout the tool stack, and strict alignment with the llama.cpp
/v1/chat/completionsstandard.Operational Considerations
P1: Session Persistence — Rewrite Tax at High Thinking Budgets
Every message mutation calls
saveAndNotify()synchronously — the full[]ChatMessageJSON is marshaled and atomically written to disk before execution continues. With xhigh (32K) thinking, this becomes a measurable bottleneck:For reference: a 50-turn session with
high(16K) thinking and average 3 tool calls per turn produces ~1M tokens of history. Each save rewrites the entire file.Fix 1: Append-only NDJSON
Replace the full JSON array with newline-delimited JSON. Each message is appended as a single line — O(1) per save regardless of history size:
Compaction: Periodically rewrite the NDJSON file as a single compact JSON array to reclaim space from deleted/corrupt lines. Trigger on session load or when the file exceeds a threshold (e.g., 100MB).
Fix 2: Async Save (non-blocking)
Don't block the execution loop waiting for disk:
The goroutine captures the last message by value (no pointer aliasing). If the write fails, it's logged but doesn't halt the agent. On session load, trailing corrupt lines are skipped.
Fix 3 (P3): Gzip
reasoning_contentThe
reasoning_contentfield is the bulk of the history file (80%+ at high thinking budgets). Gzip it independently:Store as a
[]byte(base64-encoded by JSON marshaling). At 5:1 compression for prose reasoning, this cuts the 6.4MB example to ~1.3MB.Fix 4 (P4): Subagent Sidecar Files
Subagents currently set
HistoryPath = ""— their reasoning is invisible on session reload. Without capturing the full conversation (which would bloat the primary session), write a lightweight sidecar with the subagent's structure:Saved to
<session-path>-sidecar-{subagent-id}.json. On load, displayed as a collapsible trace in the TUI.P2: Search Tool Parallelization — Pure Go Speedup
The agent's
SearchTool(internal/tool/search.go) currently walks directories sequentially withfilepath.WalkDir, reading files one by one. For large codebases this is a bottleneck during reasoning — the model burns thinking budget waiting for search results.Fix: Replace the sequential walk with a parallel worker pool. Pure Go, no system tools, no CGO:
Speedup: On an 8-core machine, this gives ~8x faster search for file-read-bound workloads. The sequential Phase 1 (
collectFiles) is fast because it only stats directories and checks names — no file reads.Memory: Peak memory is bounded by the result channel buffer and one open file per worker. The existing
maxSearchChars(32KB) limit on output prevents unbounded growth.This is independent of the thinking system but directly impacts the agent's responsiveness during reasoning: a search that takes 8s sequentially completes in ~1s with parallel workers, saving the thinking budget for actual reasoning instead of blocking on I/O.
P2: sqz Compression — Stderr Leak Fix
LATE uses
sqz(internal/tool/utils.go) to compress shell command output viaCompressWithSqz(). The current implementation leaks sqz's per-compression stats to the terminal:Fix: Capture stderr explicitly — log it for debugging instead of leaking to the user's terminal:
The dedup cache is persistent (SHA-256 content hash in
~/.sqz/sessions.db), so it works correctly across independentsqz compressinvocations — no MCP server needed.P3: Tool Result Cache
The same file is often read multiple times across different reasoning steps (e.g., "read config → edit config → verify config"). Currently each read walks the filesystem, reads, and compresses via sqz — even when the content hasn't changed.
A result cache keyed by
(tool_name, args_hash)skips redundant execution entirely:Where it wins:
TTL of 30s prevents stale results while catching repeated reads within the same reasoning block. On cache miss, the result is still stored for the next call in the sequence.
P3: Parallel Tool Execution
When the model emits multiple tool calls in one response, they're currently executed serially. If the calls are independent (no data dependency), execute them concurrently:
Dependency detection is conservative: file reads are independent, file writes are ordered, and the same variable being set and then read is serialized. For the common case of "read A, read B, read C, then edit both", reads A/B/C run in parallel while the edit waits.
Wall-clock impact: A model response with 3 independent tool calls completes in the time of the slowest call, not the sum. Over a 50-turn session where ~40% of turns emit multiple independent calls, this cuts session time by ~25%.
P4: Symbol Index (go-tree-sitter)
The agent frequently asks structural questions: "where is this function defined?", "find all callers of X", "what imports does this file use?" Currently each query walks the filesystem and regex-scans files — O(n) per query.
A pre-built symbol index answers in O(1):
Speedup: A sequential directory walk for "find function X" takes 200ms–2s depending on repo size. The index lookup is <1µs. Over a 50-turn agent session, this saves 10–100s of wall-clock time — directly converting I/O wait into thinking budget.
Tradeoff: Requires go-tree-sitter (CGO). Only pays off when the agent asks multiple structural questions per session. For the common "fix one file" pattern, the index build time (~1s for a moderate repo) is a net loss. Only build the index lazily on first structural query, not at session start.
Deep Analysis (No Action Required)
These sections document verified behavior that is safe as-is. No code changes needed.
Context Sizing & Auto Mode
The
automode reads the model's context size viaclient.ContextSize()and picks a proportional budget. The mapping reserves ~10–12.5% of context for thinking at each tier:At 64K context, even
xhigh(32K) is not clamped until conversation history exceeds ~31K tokens (~100 tool call rounds). The clamp only matters for very long sessions.Recommendation:
Slot Isolation — Concurrent Requests Are Safe
LATE spawns multiple subagents concurrently, sometimes with different thinking budgets. This works because the llama.cpp server strictly isolates thinking configuration per request:
chat_template_kwargs.enable_thinkingis consumed duringcommon_chat_templates_apply()at request parse time. Not stored in slot state.server-common.cpp:1071-1091thinking_budget_tokens→task_params.sampling.reasoning_budget_tokens. Eachserver_taskcarries its own copy.eval_llama_cmpl_schema()creates a localtask_paramsper task.server-schema.cpp:378-380,server-context.cpp:4132slot.smpl.reset(common_sampler_init(model_tgt, task.params.sampling)).server-context.cpp:1817server-common.cpp:1091-1094Example: Slot 0 gets
enable_thinking: true, budget: 4096. Slot 1 getsenable_thinking: false, budget: 0. No shared state.Long Thinking & Attention Drift
After 16K tokens of reasoning, will the model "forget" the tool schema? No. Three layers of defense:
Budget Cutoff Precision
The budget sampler counts down per-token and transitions to
FORCINGwhenremaining <= 0. The next generated token is forced to be the end-of-reasoning sequence.remaining <= 0→ immediateFORCINGatreasoning-budget.cpp:103-107WAITING_UTF8state atreasoning-budget.cpp:108-112<think>budgettokens — restartsreasoning-budget.cpp:127-137The formula
budget + ExpectedOutput + ReasoningEndOverhead(16)provides 23 tokens of slack — comfortable for any tokenizer.Security: Tag Injection & Reasoning Leakage
LATE ingests untrusted content (file reads, curl output, logs). The server protects at three layers:
delta.tool_callsis independent ofreasoning_content. (server-chat.cpp:552-553,server-task.cpp:768-1210)token_matchermatches exact token sequences, not substrings. (reasoning-budget.cpp:42-43)chat-diff-analyzer.cpp)Clamped budget behavior: When context pressure clamps to a very low value (e.g., 20), the consequence is quality degradation, not agent deadlock. The lazy grammar only activates after the thinking block closes. Floor the clamped budget at 256; below that, disable thinking entirely.
Server Requirements
LATE relies on the user to boot the llama.cpp server correctly.
Gemma 4:
Qwen3.6:
Both require
--jinja(template engine) and--reasoning on(reasoning support).--reasoning-formatdefaults toauto(resolves todeepseek) — no explicit flag needed for either model.Beta Was this translation helpful? Give feedback.
All reactions