The Template Is the Caller's Job
p4 forwards an opaque prompt and applies no chat template. Forget that and the model answers a question you did not ask — fluently, and all the way to the token limit.
The first real answer out of our recovered ring was correct arithmetic followed by a conversation nobody had:
17 × 23, asked of a served model.
" 391 What is 12 times 12? Reply with only the number. 144 What is 14"
The number is right. Everything after it is the model continuing a document, because that is what we handed it: the messages flattened into one string. An instruct model reads that as text to extend, not a turn to answer. It never emits its end-of-turn token, so generation runs to the cap every single time.
A deliberate omission, not a gap
p4 hands the stage server an opaque prompt and applies no turn format of its own — the staged adapter carries only a tool for *reading* a template out of a GGUF, never for applying one. That is a reasonable line to draw: the engine stays narrow and model-agnostic, and the caller, which already knows which model it is talking to, renders the format. But a line drawn and not documented is a line somebody walks over.
Reading the template out of the model file settled it: ChatML turns, <|im_end|> as the end-of-turn token, and an assistant turn that opens with a thinking block. With that rendered by the bridge, the same question:
The same model, the same ring, the turn format applied.
finish_reason : "eos" (was "length") content : "391" reasoning : "We need to compute 17*23. 17*20=340, plus 17*3=51, total 391."
A thinking pass can eat the answer
A reasoning model spends tokens before it says anything. Give it a budget and a hard question and it can spend the whole budget thinking, leaving the answer empty — and in a network where the caller has already paid on-chain before the request ran, an empty answer is not a quality problem. It is a charge for nothing.
The settlement gateway already knew this and asks for thinking to be disabled. The model's template has no switch for it, so the bridge opens *and closes* the thinking block in the prompt, and the model writes its answer after it. We got this wrong once in the obvious way — closing the block in the prompt meant the closing tag was no longer in the output, so the splitter filed the entire answer as reasoning and returned empty content. Which is the exact failure the setting exists to prevent.
Where this leaves the contract. The engine forwards bytes. The bridge knows the model: it renders the turn format named in the placement plan, returns the thinking pass as reasoning_content separate from content, and clamps a request that asks for more output than the ring was loaded to give — because an over-large request is otherwise refused outright, and a shorter answer beats an engine error.