serve: carry Kimi K3 tool-call structure on the shared codec instead of the text channel
Corrected scope. This issue originally proposed doing GLM and Kimi K3 together. That was wrong, and checking the two engines rather than assuming they were symmetric is what showed it:
- Kimi K3:
<|open|>, <|close|>, <|sep|>, <|end_of_msg|> are real special tokens — chat_special() resolves them to ids held in sp[0..3] (kimi_k3.c:2126, :2203). The engine therefore knows whether a structural marker was generated by the model or merely looks like one.
- GLM:
<tool_call> does not appear in colibri.c at all. The markers are ordinary text rendered into the prompt and recovered by parse_tool_calls in openai_server.py. The engine has no ground truth to preserve, so a sideband would carry nothing better than the text already does. GLM's exposure is inherent to its format and can only be narrowed in the parser, which _unclosed_tail already does for the lenient path.
So: K3 only.
What it would fix. #1144 ships K3 tool calling with the structural run re-emitted as literal text for the gateway to pattern-match (option (a) from #1143). That discards a distinction the engine holds: a user prompt that gets the model to echo <|open|>call tool="…"<|sep|> as ordinary characters is, downstream, indistinguishable from a real call. The gateway reports tool_calls and never executes — the client application decides what to run — so this is a trust-boundary weakness rather than code execution, but a client acting on our tool_calls field is acting on a statement of fact we would be getting wrong.
Why the codec makes this cheap. c/serve_codec.h owns both directions since the framing migration finished (#1087, #1090, #1096, #1116), each engine landed behind a byte-exact wire-transcript freeze, and Inkling already rides an engine-specific structured payload on it (audio, as an opaque extension). A tool-call record is the same shape: one file, one new record type, an existing test that proves nothing else moved.
Not urgent, and not user-visible. The wire between engine and gateway is internal; the OpenAI tool_calls contract clients see is identical either way, so this can land whenever without a migration for anyone.
Context: #1143 (design), #1144 (implementation), #1029 (original gap).
serve: carry Kimi K3 tool-call structure on the shared codec instead of the text channel
Corrected scope. This issue originally proposed doing GLM and Kimi K3 together. That was wrong, and checking the two engines rather than assuming they were symmetric is what showed it:
<|open|>,<|close|>,<|sep|>,<|end_of_msg|>are real special tokens —chat_special()resolves them to ids held insp[0..3](kimi_k3.c:2126,:2203). The engine therefore knows whether a structural marker was generated by the model or merely looks like one.<tool_call>does not appear incolibri.cat all. The markers are ordinary text rendered into the prompt and recovered byparse_tool_callsinopenai_server.py. The engine has no ground truth to preserve, so a sideband would carry nothing better than the text already does. GLM's exposure is inherent to its format and can only be narrowed in the parser, which_unclosed_tailalready does for the lenient path.So: K3 only.
What it would fix. #1144 ships K3 tool calling with the structural run re-emitted as literal text for the gateway to pattern-match (option (a) from #1143). That discards a distinction the engine holds: a user prompt that gets the model to echo
<|open|>call tool="…"<|sep|>as ordinary characters is, downstream, indistinguishable from a real call. The gateway reportstool_callsand never executes — the client application decides what to run — so this is a trust-boundary weakness rather than code execution, but a client acting on ourtool_callsfield is acting on a statement of fact we would be getting wrong.Why the codec makes this cheap.
c/serve_codec.howns both directions since the framing migration finished (#1087, #1090, #1096, #1116), each engine landed behind a byte-exact wire-transcript freeze, and Inkling already rides an engine-specific structured payload on it (audio, as an opaque extension). A tool-call record is the same shape: one file, one new record type, an existing test that proves nothing else moved.Not urgent, and not user-visible. The wire between engine and gateway is internal; the OpenAI
tool_callscontract clients see is identical either way, so this can land whenever without a migration for anyone.Context: #1143 (design), #1144 (implementation), #1029 (original gap).