Skip to content

server: tail_tools are callable without entering the rendered head (restacked on the engine pin) - #75

Merged
joelteply merged 2 commits into
fable/page-checkpoint-subsetfrom
fable/tail-tools-on-pin
Oct 26, 2026
Merged

joelteply merged 2 commits into
fable/page-checkpoint-subsetfrom
fable/tail-tools-on-pin

Conversation

@joelteply

Copy link
Copy Markdown

Supersedes #71. Same change, restacked directly on the deployed engine pin e7e1077 (fable/page-checkpoint-subset), so it no longer carries #70's unreviewed page-store, durable-move and Windows-adapter commits.

The tail-tools diff is unchanged from 0e96a02, which Codex reviewed and passed. These are the two original commits (6cae10f, 0e96a02) cherry-picked onto the pin. Their added and removed lines match git diff 0e96a022e~2 0e96a022e exactly. common/chat.cpp, common/chat.h, tools/server/server-common.cpp and tests/test-chat.cpp were byte-identical between the pin and #71's old base, so the picks applied cleanly. The diff against the pin is those 4 files only (+208/-3).

What it does (continuum goal 10, card ecd5dff3): verbs in tail_tools are callable (parser, lazy grammar, triggers) without being rendered into the head, so loading a verb leaves the cached prefix byte-identical. A tail-only request obeys every tools boundary (requires-Jinja rejection, tools + stream guard), and the library refuses a tail on the legacy path instead of dropping it. The tests cover Qwen2.5 and Qwen3.5, a populated and an empty head, streamed deltas with grammar acceptance, tool_choice "none", and the server-boundary regressions. The answers to each review finding are on #71.

Local (CPU Release, -DGGML_METAL=OFF, on ca53e26, 2026-10-26): test-chat → [chat] All tests passed!, and llama-server builds. Unit coverage only; there is no live-node or speed claim.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 2 commits October 26, 2026 14:49
A request may carry `tail_tools` beside `tools`: the chat template renders only `tools`
(so the prompt bytes, and the KV cached for them, are unchanged), while the format,
parser, lazy grammar, triggers and stops are built over tools + tail_tools. A client that
declares a verb later in the conversation gets a real tool call to it on the next turn,
instead of a grammar that cannot even emit its name. Continuum goal 10 (card ecd5dff3):
a mind's head holds a learned working set and loads the rest into the tail.

test-chat: test_tail_tools (Qwen3.5 template) asserts the prompt and generation prompt are
byte-identical with and without tail_tools, the tail verb is in the grammar and parses as a
tool call with its arguments, the head-only parser does not produce it, and the same through
oaicompat_chat_params_parse. Disabling the tail branch fails the test (checked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… for Qwen2.5, empty head, tool_choice none and streaming

Codex's review of #71: has_tools read only `tools`, so a request whose verbs were all in
`tail_tools` passed the requires-Jinja rejection on a non-Jinja server (the legacy renderer then
dropped the tail while parse_tool_calls was set) and skipped the tools + stream format guard.
has_tools now counts the tail, and common_chat_templates_apply refuses a tail on the legacy path
instead of dropping it.

test_tail_tools now covers, per template family (Qwen2.5 JSON <tool_call>, Qwen3.5 XML
<tool_call>) and for a populated and an empty head: prompt bytes equal the head-only render, the
tail verb's call parses through streamed deltas with the lazy grammar accepting it, and with
tool_choice "none" the tail adds nothing to the grammar and the verb does not parse as a call.
Server boundary: tail-only on non-Jinja is refused, tail-only + stream + logprobs is refused,
tool_choice "none" sets no parse_tool_calls, empty head + tail renders the no-tools prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply

Copy link
Copy Markdown
Author

Exact-head source PASS at ca53e26. Independently verified the base is deployed pin e7e1077 and only the four tail-tools files change. GitHub blob SHA comparison confirms all four files byte-identical to reviewed 0e96a02 (common/chat.cpp, chat.h, tests/test-chat.cpp, tools/server/server-common.cpp). This removes the unrelated #70 page-publication changes from the adoption prerequisite. My boundary/coverage findings are closed. Exact-head CI and supported engine artifact adoption remain required; no live speed or tail-call receipt asserted.

@joelteply

Copy link
Copy Markdown
Author

Metal receipt for #75 (the gpu-metal and gpu-cuda jobs are queued with no self-hosted runner, so the M5 is standing in as the Metal executor, as Codex asked).

  • Head: ca53e26
  • Host: Apple M5 Pro (MTLGPUFamilyMetal4), macOS, serving down (build window 9), 2026-10-26
  • Configure: these are the gpu-metal job's ci/run.sh ctest_release flags with GG_BUILD_METAL=1:
    cmake -S . -B build-metal -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=ON -DLLAMA_FATAL_WARNINGS=ON -DLLAMA_OPENSSL=OFF -DGGML_SCHED_NO_REALLOC=ON
  • Build: cmake --build build-metal --config Release -j18 builds all targets, with exit 0, 0 warnings (fatal warnings on), in 53.5 s.
  • Tests: ctest -C Release --output-on-failure -L main -j18. 55/56 passed.
    • test-chat Passed (2.98 s), test-chat-peg-parser Passed, test-chat-auto-parser Passed, test-chat-template Passed.
    • The one failure is test-tokenizers-ggml-vocabs, and it comes from the host. git-lfs is not installed on this machine, so the cloned ggml-org/vocabs files are LFS pointer stubs (version https://git-lfs.github.com/spec/v1), and loading one fails with invalid magic characters: 'vers'. That test touches no file in this diff.
    • The CI script also runs ctest's python label; I did not run it here.

Hosted CI on this head: Server windows and ubuntu pass. CI (cpu) windows / arm64, x64-cpu-static and x64-openblas pass. gpu-metal, gpu-cuda and gpu-vulkan-intel-windows are still waiting for a self-hosted runner.

@joelteply

Copy link
Copy Markdown
Author

Complete Metal gate for #75: both gaps from my earlier receipt are closed. ctest -L main passes 56/56 and the python label passes 1/1.

  • Head: ca53e26 (clean tree)
  • Host: Apple M5 Pro (MTLGPUFamilyMetal4), macOS, serving down (build window), 2026-10-26
  • Fixtures: git-lfs is installed now, and git lfs pull in models/ggml-vocabs gave 11 real GGUF files where there had been LFS pointer stubs.
  • Python: a venv built the way ci/run.sh builds it (pip install -r requirements.txt plus pip install --editable gguf-py), on Python 3.14.6: jinja2 3.1.6, torch 2.11.0. The system python, 3.9, is too old for torch==2.11.0. test-jinja-py called the venv's python3 (checked with which).

Commands, run from the activated venv, using the gpu-metal job's ci/run.sh ctest_release flags with GG_BUILD_METAL=1:

cmake -S . -B build-metal -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=ON -DLLAMA_FATAL_WARNINGS=ON -DLLAMA_OPENSSL=OFF -DGGML_SCHED_NO_REALLOC=ON
cmake --build build-metal --config Release -j18          # exit 0, 0 warnings, 33.7 s
ctest -C Release --output-on-failure -L main -j18        # 100% tests passed, 0 failed out of 56
ctest -C Release --output-on-failure -L python           # 100% tests passed, 0 failed out of 1 (test-jinja-py)

Of note: test-tokenizers-ggml-vocabs Passed (4.10 s) and test-chat Passed (5.33 s).

One gap remains: the server pytest suite under tools/server/tests is not a ctest label, so it was not part of this run. The hosted Server workflow (windows, ubuntu) passed on this head.

@joelteply
joelteply merged commit 1c66815 into fable/page-checkpoint-subset Oct 26, 2026
10 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant