A tool with no properties is rendered so the model must invent an empty parameter name
Environment
|
|
| KoboldCpp |
1.121, koboldcpp-linux-x64 |
| Launch |
--jinja --jinja_tools --quantkv f16 --contextsize 65536 --batchsize 256 --threads 8 --skiplauncher |
| Model |
nemotron-3-nano-4b.gguf (also seen on gemma4-e4b, qwen35-4b/9b, ornith-9b) |
| API |
POST /v1/chat/completions, tools array, using_openai_tools: true |
| Rendered prompt read via |
--debugmode 1 (the server prints the request, including a prompt field holding the fully rendered text) |
Summary
When a tool's JSON Schema declares no properties — {"type": "object", "properties": {}, "required": []} — the rendered prompt declares it as:
<tools>
<function>
<name>eve_vault_index_rebuild</name>
<parameters>
<additionalProperties>False</additionalProperties>
</parameters>
</function>
There is no <parameter> block, and nothing names a parameter. But the same prompt instructs the
model:
If you choose to call a function ONLY reply in the following format with NO suffix:
<tool_call>
<function=example_function_name>
<parameter=example_parameter_1>
value_1
</parameter>
<parameter=example_parameter_2>
…
</parameter>
</function>
</tool_call>
Reminder:
- Function calls MUST follow the specified format: an inner <function=...></function> block must be
nested within <tool_call></tool_call> XML tags
- Required parameters MUST be specified
So the model is given a format that requires a <parameter=NAME> and a tool that has no name to give
it. It writes <parameter=>, an empty name, and the client receives:
"tool_calls": [{"function": {"name": "eve_vault_index_rebuild", "arguments": "{\"\": …}"}}]
Strict clients reject that. In our harness the call is recorded as
unexpected-argument-key, and the lane's guard — which compares the received arguments to the
declared ones — reads "observed_reason": "unexpected-arguments" and fails the step.
Evidence
1. Deterministic, end to end. 12 runs across four campaigns, all with the same model, lane and
weights: argument_keys=[''] every time. ollama, same GGUF, same lane, renders through the model's
own template and sends argument_keys=[] — accepted. The bench is not the variable: its provider
adapters only observe window and parser, one schema is declared for every provider, and one request
body is composed for all three.
2. The rendered prompt (from --debugmode 1) shows both halves side by side: the <parameters>
block above, and the <parameter=NAME> instruction quoted above.
3. --jinjatemplate does not change it. Forcing the model's own
tokenizer.chat_template (10,504 chars, read straight out of the GGUF) still yields [''], 3 of 3.
The rendering comes from the chat-completions adapter (chatcompletionsadapter: "AutoGuess" in the
exported config), not from the template.
4. What does not reproduce it, measured. Two minimal candidates were falsified, which is why
this report does not claim a minimal repro:
- a single no-argument tool, nothing else in the conversation →
arguments='{}', 3 of 3;
- a no-argument tool after an assistant call that did carry a parameter, plus its tool result →
arguments='{}', 3 of 3.
The smallest repro that reproduces it reliably is our bench's own request: five tools including the
deferred-tool bridge (tool_search / tool_describe / tool_call, the last taking calls), a long
system prompt, and a conversation already carrying tool calls and results. That points at the
nested/bridged schema rather than at the bare properties: {} case — the trigger appears to need
the complex tool surface, so the bridge's own schema is the first place to look.
Impact, stated plainly
Any client that validates a tool call's arguments against the declared schema sees a call that cannot
be valid, for a tool that is in fact being called correctly. Whether that is fatal depends on the
client: ours executes the tool and then fails the step at its guard, so the loss is a rung of the
evaluation rather than a crash.
There is no configuration lever for the tool format — measured
Both levers that look like they should affect this do not:
-
--jinjatemplate <the model's own tokenizer.chat_template> — no change: [''], 3 of 3.
-
--chatcompletionsadapter <JSON> — cannot help. Its help says "force custom instruct tags", and
the shipped adapter files contain only chat delimiters. AutoGuess.json (≈40 model presets) and
DeepSeek.json both define exactly:
{ "system_start": …, "system_end": …, "user_start": …, "user_end": …,
"assistant_start": …, "assistant_end": …, "assistant_gen": … }
There is no tool, function, or parameter key in any of them.
The <tools>/<function>/<parameters>/<parameter> block and the instruction beside it are therefore
hard-coded in the chat-completions tool handling, and the suggested fix has to land there.
Suggested fix (either is enough)
- Express a no-argument tool unambiguously.
<parameters/>, empty, with no
<additionalProperties> line standing where a parameter would go — and a sentence in the
instruction covering the no-argument case ("if the function takes no parameters, emit an empty
<function=…></function> block").
- Or accept an empty parameter name as "no arguments" in the parser, so
<parameter=> maps to
{} rather than to {"": …}.
Related open issues
Workaround available to us
None on the KoboldCpp side, as measured above — so the only local option is to stop declaring a
no-argument tool: give the governed chain steps a named parameter (one would do), which changes a
declared surface and therefore needs the bench owner's approval and a re-run of the affected lanes.
A tool with no properties is rendered so the model must invent an empty parameter name
Environment
koboldcpp-linux-x64--jinja --jinja_tools --quantkv f16 --contextsize 65536 --batchsize 256 --threads 8 --skiplaunchernemotron-3-nano-4b.gguf(also seen ongemma4-e4b,qwen35-4b/9b,ornith-9b)POST /v1/chat/completions,toolsarray,using_openai_tools: true--debugmode 1(the server prints the request, including apromptfield holding the fully rendered text)Summary
When a tool's JSON Schema declares no properties —
{"type": "object", "properties": {}, "required": []}— the rendered prompt declares it as:There is no
<parameter>block, and nothing names a parameter. But the same prompt instructs themodel:
So the model is given a format that requires a
<parameter=NAME>and a tool that has no name to giveit. It writes
<parameter=>, an empty name, and the client receives:Strict clients reject that. In our harness the call is recorded as
unexpected-argument-key, and the lane's guard — which compares the received arguments to thedeclared ones — reads
"observed_reason": "unexpected-arguments"and fails the step.Evidence
1. Deterministic, end to end. 12 runs across four campaigns, all with the same model, lane and
weights:
argument_keys=['']every time. ollama, same GGUF, same lane, renders through the model'sown template and sends
argument_keys=[]— accepted. The bench is not the variable: its provideradapters only observe window and parser, one schema is declared for every provider, and one request
body is composed for all three.
2. The rendered prompt (from
--debugmode 1) shows both halves side by side: the<parameters>block above, and the
<parameter=NAME>instruction quoted above.3.
--jinjatemplatedoes not change it. Forcing the model's owntokenizer.chat_template(10,504 chars, read straight out of the GGUF) still yields[''], 3 of 3.The rendering comes from the chat-completions adapter (
chatcompletionsadapter: "AutoGuess"in theexported config), not from the template.
4. What does not reproduce it, measured. Two minimal candidates were falsified, which is why
this report does not claim a minimal repro:
arguments='{}', 3 of 3;arguments='{}', 3 of 3.The smallest repro that reproduces it reliably is our bench's own request: five tools including the
deferred-tool bridge (
tool_search/tool_describe/tool_call, the last takingcalls), a longsystem prompt, and a conversation already carrying tool calls and results. That points at the
nested/bridged schema rather than at the bare
properties: {}case — the trigger appears to needthe complex tool surface, so the bridge's own schema is the first place to look.
Impact, stated plainly
Any client that validates a tool call's arguments against the declared schema sees a call that cannot
be valid, for a tool that is in fact being called correctly. Whether that is fatal depends on the
client: ours executes the tool and then fails the step at its guard, so the loss is a rung of the
evaluation rather than a crash.
There is no configuration lever for the tool format — measured
Both levers that look like they should affect this do not:
--jinjatemplate <the model's own tokenizer.chat_template>— no change:[''], 3 of 3.--chatcompletionsadapter <JSON>— cannot help. Its help says "force custom instruct tags", andthe shipped adapter files contain only chat delimiters.
AutoGuess.json(≈40 model presets) andDeepSeek.jsonboth define exactly:{ "system_start": …, "system_end": …, "user_start": …, "user_end": …, "assistant_start": …, "assistant_end": …, "assistant_gen": … }There is no tool, function, or parameter key in any of them.
The
<tools>/<function>/<parameters>/<parameter>block and the instruction beside it are thereforehard-coded in the chat-completions tool handling, and the suggested fix has to land there.
Suggested fix (either is enough)
<parameters/>, empty, with no<additionalProperties>line standing where a parameter would go — and a sentence in theinstruction covering the no-argument case ("if the function takes no parameters, emit an empty
<function=…></function>block").<parameter=>maps to{}rather than to{"": …}.Related open issues
--jinja --jinja_tools, DeepSeek V4 Flash 0731's well-formed DSML tool call isrejected by the native parser": the parser side of this same tool path. This report is the
renderer side — together they suggest the
--jinja_toolstool handling needs attention at bothends.
(a spin loop rather than a malformed argument), but the same tool-call path. We hit that too and have
added our measurements there.
Workaround available to us
None on the KoboldCpp side, as measured above — so the only local option is to stop declaring a
no-argument tool: give the governed chain steps a named parameter (one would do), which changes a
declared surface and therefore needs the bench owner's approval and a re-run of the affected lanes.