merge improvements from Moore2877/Qwen-Fixed-Chat-Templates-llamacpp

#94
by l0rdraiden - opened

https://huggingface.co/Moore2877/Qwen-Fixed-Chat-Templates-llamacpp

It would be interesting to merge the improvements of this template

There is also another project that could be interesting to look at.

https://github.com/Stacey2911/hermes-local-ai-stack

I know is focused on Hermes bu at the end is agent behavior, it could be worth to take a look

Hey @l0rdraiden , thanks for sharing! Most of Andrew's tweaks (Anthropic thinking field, tool response injection guards, format handling) were pulled into v21.3 and updated with the new error detection in v22.3, so they are already in main. I will take a look at the hermes stack repo too.

Generated with AI but I think is interesting anyway

Notes after comparing v22.4 against Moore2877's qwen3.8-llamacpp-agentic-v19

Went through your template side by side with Moore2877's Qwen-Fixed-Chat-Templates-llamacpp v19, since I'm running both against agentic harnesses (Hermes Agent, Oh My Pi) on top of llama.cpp. Wanted to share what I think is genuinely worth borrowing versus what I'd leave alone, rather than just dumping a diff.

A real bug, not just a stylistic gap

When tools are present but thinking is off (enable_thinking=false, or reasoning_effort in ('none','off')), the tools system block still unconditionally tells the model it can use <think></think> to plan the call, that all reasoning must go strictly inside that block, and that the <tool_call> must come immediately after thinking โ€” and it shows a worked example with a <think>Brief explanation...</think> prefix.

But at generation time, add_generation_prompt prefills:

<think>

</think>

already closed and empty. So the instructions the model just read in the system prompt are impossible to follow โ€” it's told to plan inside a block that's already shut before it gets a turn.

Moore's v19 avoids this by building the tool-instructions text incrementally and gating every thinking-related bullet/example on ns_state.thinking:

{%- set tool_instructions = 'If you choose to call a function ONLY reply in the following format with NO suffix:' %}
{%- if _tool_format == 'json' %}
    {%- if ns_state.thinking %}
        {%- set tool_instructions = tool_instructions ~ '\n<think>\nBrief explanation of tool call\n</think>' %}
    {%- endif %}
    ...

I'd port this regardless of anything else below โ€” it's a correctness fix, not a feature debate. It probably also explains why the JSON/XML reminder blocks are currently two hardcoded near-duplicate strings: once you gate on ns_state.thinking, keeping them as independent literals means fixing this twice and re-syncing them by hand on every future edit. Moore's incremental tool_instructions ~= ... pattern fixes the bug at the root and keeps the two variants in sync automatically.

Worth porting as-is

strftime_now date injection. One line, no downside:

{%- if strftime_now is defined %}
    {{- '\n\nToday is ' + strftime_now('%Y-%m-%d') + '.' }}
{%- endif %}

v22.4 doesn't ground the model in the current date at all. Any runtime that already exposes strftime_now (transformers, several server backends) gets this for free.

The ping-pong tool-call nudge. This is the best thing in v19 and it's fully self-contained โ€” no tool-name lists, no assumptions about the harness. It hashes tool name + arguments across the last two assistant tool-call turns, and once the same signature repeats past a threshold (pingpong_nudge_after, Moore defaults to 4), injects a one-time system warning to change tool/strategy or stop and report. This is exactly the failure mode neither template currently catches: two tool calls alternating with slightly different but useless argument tweaks won't necessarily look like "errors" to the consecutive-failure check, so they loop silently. I'd port this specific piece (not the content-repeat variant below).

The vanished-tools warning. Also self-contained โ€” compares tool names the assistant already called against the current tools list, and warns once if any disappeared mid-session (MCP servers dropping, dynamic tool sets changing). Relevant for anyone running this against Hermes Agent or Oh My Pi, both of which can have tool availability change across a long session.

Worth porting, with a caveat

The identical-content repeat nudge (ns2.repeat, default threshold 6) is useful in principle, but Moore's version excludes "mutating" tool calls from the check using a large hardcoded mutating_tools name list, to avoid false positives on short, structurally similar success messages from consecutive legitimate edits. That list will always be incomplete โ€” it can't know a harness's custom tool names unless the caller overrides it via template kwargs, and most integrations won't bother. If you want this, I'd either (a) skip the name-based exclusion and reuse your existing content heuristics (_is_code_or_grep, the length threshold) to suppress false positives the same way you already do for error detection, or (b) just ship the ping-pong nudge above and skip this one โ€” it covers a good chunk of the same failure mode without the upkeep.

Moore's per-tier reasoning-effort text. v19 has four distinct paragraphs (xhigh/high/medium/low) vs. your two (xhigh/low, medium silent). I don't think the full four-tier system is worth adopting โ€” it's more prompt bloat for marginal differentiation between "high" and "xhigh." But the medium-tier text has one genuinely good line worth lifting on its own: when verification fails, validate the verifier and choose one falsifiable probe; if two probes don't narrow the same issue, stop the loop, summarize the evidence, and change strategy. Solid, generic anti-thrashing advice for coding-agent debugging loops, independent of the rest of the tiered system.

Generic tool-instruction bullets. A few of Moore's extra reminders are useful and tool-name-agnostic: ground tool arguments in visible state or a prior tool result (inspect first if unknown), take exact file paths/replacement strings from prior tool output rather than inventing them, and explicit criteria for when to end a turn without a tool call. Worth adding independent of everything else here.


Part 2: comparing v22.4 against Stacey2911's qwen3-caduceus-v1.9

Following up on the Moore2877 comparison with a look at Caduceus, which turned out to be a genuinely different animal โ€” much less tool/harness-opinionated overall, more focused on input hygiene and hallucination-grounding. Same approach as last time: concrete bugs first, then what I'd actually port, with honest caveats, filtering out anything tied to a specific tool or harness.

Another concrete bug, this time in yours

max_tool_response_chars truncation is gated behind _tool_format != 'json':

{%- if _tool_format != 'json' and max_tool_response_chars > 0 and content | length > max_tool_response_chars %}
    {%- set content = content[:max_tool_response_chars] + '\n[TRUNCATED...

That gate makes sense for tool-call argument truncation (cutting a JSON string mid-token could break the JSON you're emitting), but it's applied here to the incoming tool response wrapper, which is rendered as plain text identically regardless of _tool_format โ€” nothing about it is JSON-shaped. As written, anyone using tool_call_format='json' (the default, and probably the more common choice) gets no response truncation at all, no matter what they set max_tool_response_chars to. Reads like the JSON-safety guard from the argument-truncation logic leaked into the response-truncation logic by copy-paste. Caduceus applies its response truncation unconditionally, which is the correct behavior here.

The one thing I'd port with real enthusiasm

sanitize_supplied_content. It walks every system/user/assistant/tool content string and neutralizes literal occurrences of <tool_call>, </tool_call>, <function=, <parameter=, <tool_response>, <think>, <|im_start|>, <|im_end|> (HTML-entity-escaping them) before they reach the prompt. Neither your template nor Moore's does anything like this, and it's completely harness-agnostic โ€” no tool names, no framework assumptions โ€” which is exactly the kind of thing worth filtering for.

Why it matters: a tool response that pulls in untrusted content (a scraped page, a document, a shell command's output) can currently contain a literal <tool_call>...</tool_call> or <|im_start|>assistant sequence. Depending on how strictly the inference server treats role boundaries versus body text, that's a real injection surface โ€” content crafted to look like it came from the model or the system rather than from the tool result. Escaping it at the template level is cheap insurance regardless of what's calling the model.

Two honest caveats before lifting it wholesale:

  • It's a blunt find/replace, not context-aware. Anyone pasting a genuine example of ChatML syntax or a <think> block into a message โ€” technical discussions about the template itself, for instance โ€” gets it silently mangled to &lt;think&gt; in what the model sees. For a template repo whose own audience debugs chat templates for fun, that's not hypothetical.
  • It raises the bar against literal-token injection specifically. It does nothing against natural-language instruction injection ("ignore previous instructions..."), which is a different, harder problem. Worth being precise about that scope if you document it, so people don't treat it as a general prompt-injection fix.

Worth porting, lightly edited

The three reasoning "contracts" (reasoning_boundary_contract, reasoning_efficiency_contract, thinking_tool_generation_contract). Stripped of one thing, these are good:

  • Boundary contract: close <think> exactly once, don't reopen it, don't leak planning into the visible response. A real failure mode neither template currently instructs against.
  • Efficiency contract: match reasoning depth to actual task complexity rather than to available budget; stop as soon as the answer is determined. This reads as a better, more general replacement for the per-tier reasoning paragraphs both templates already carry, rather than an addition to them โ€” I'd pick one approach, not stack both, or you end up with two overlapping sets of instructions about how much to think.
  • Tool-generation contract: don't predict, simulate, or quote expected tool output before actually calling the tool. This is the one I'd prioritize โ€” a real, common failure mode (models fabricating a plausible tool result instead of waiting for the real one) that nothing in either template currently addresses, and it's entirely generic.

The edit needed: the tool-generation contract's last line hardcodes "the actual JSON-shaped tool-call block" โ€” trivially reworded as "the configured tool-call format" so it still makes sense next to your existing XML option.

task_execution_contract's completion-grounding rule, minus its framing. "Never replace execution with narration or a report that the action succeeded" and "any failed, blocked, or missing result prevents an all-success claim" are exactly the kind of thing worth having โ€” a real anti-hallucination guard, plain-English and portable. I'd drop the repeated "permitted by higher-priority instructions and available tool policy" qualifier when lifting it, though โ€” it references a prioritization scheme that isn't defined anywhere in the template itself, so in isolation it reads as a dangling reference rather than an actual constraint.

Fail-loud input validation. Caduceus raises a clear exception if messages isn't an iterable of mappings, if tools isn't an iterable of schemas, or if a historical tool call is missing a name โ€” instead of quietly producing malformed output the way your version currently would (an empty "name": "" if a caller ever passes a tool call without one, for instance). This isn't a style preference: a "fixed chat template" project is explicitly in the business of catching integration bugs early rather than passing them through, so failing loudly here fits your own stated goal better than silent degradation does.

Small, low-priority polish

  • Sane non-zero defaults for max_tool_arg_chars/max_tool_response_chars (Caduceus ships 8000/16000) instead of your current 0 = unlimited. Shipping with some default cap protects against a single oversized tool result silently blowing up context โ€” right now a user has to already know to set the limit before they hit the problem.
  • Your image detection checks 'image_url' in item in addition to 'image'/type == 'image', but the video branch only checks 'video'/type == 'video', missing 'video_url'. Caduceus checks all three for video. One-line fix, same pattern you already use for images.

What I'd leave out or adapt rather than copy

Caduceus is far less tool-opinionated than Moore's overall โ€” no hardcoded tool-name lists, no skill/task detection โ€” so there's less to actively reject here. The one concrete thing: multi_call_contract has a sentence naming HermesAgent directly ("HermesAgent may serialize an emitted batch according to its runtime safety policy"). The surrounding guidance โ€” keep dependent/shared-state calls sequential, never invent call-IDs, don't emit semantically duplicate calls โ€” is genuinely useful and worth having; that one sentence should just say "the runtime" if you port the rest.

Sign up or log in to comment