- Added Gemini and Gemma syntax routing by model family and owned syntax env values. - Added Gemini and Gemma in-band parsers for tool_code and token-based tool_call streams. - Added rendering support for Gemini fenced tool_code/tool_outputs and Gemma tool tokens. - Fixed parsing edge cases for comments, string escapes, nested args, and truncated blocks.
6.8 KiB
Gemma 4 tool-calling format (token-delimited call:NAME{…})
Tool-calling convention of Google's Gemma 4 open-weights family (google/gemma-4-*-it). It is a clean break from the prompt-engineered Pythonic tool_code form used by Gemma 3 and hosted Gemini (see gemini.md): Gemma 4 introduces dedicated special tokens and a compact token-delimited brace syntax. Tool declarations, calls, and responses each get their own paired markers, and every string value is wrapped in a <|"|> token rather than ASCII quotes. The model emits one call as <|tool_call>call:NAME{key:value,…}<tool_call|>; the developer parses it, runs the tool, and appends <|tool_response>response:NAME{…}<tool_response|>.
Verified against: the official "Function calling with Gemma 4" guide (ai.google.dev/gemma/docs/capabilities/text/function-calling-gemma4), including the byte-exact processor.apply_chat_template(...) renderings and the reference extract_tool_calls regex it ships. All example streams below are copied from that page (model google/gemma-4-E2B-it).
Special tokens
Gemma 4 wraps each structural element in a paired token. Note the asymmetric pipe placement — an opener carries the pipe on the left (<|x>) and its closer carries it on the right (<x|>):
| Open | Close | Purpose |
|---|---|---|
<bos> |
— | Beginning of sequence |
| `< | turn>` | `<turn |
| `< | tool>` | `<tool |
| `< | tool_call>` | `<tool_call |
| `< | tool_response>` | `<tool_response |
| `< | " | >` |
<eos> |
— | End of sequence |
Because the string delimiter is a token (<|"|>), values may contain raw ASCII quotes and commas without escaping — only a literal <|"|> token sequence cannot appear inside a string.
Roles / turn structure
Each turn is <|turn>{role}\n{body}<turn|>. Roles are system, user, model. With a generation prompt the stream ends at <|turn>model\n and the model continues. Tool declarations are merged into the system turn; tool calls and the following tool responses are emitted inside the model turn (the response block immediately follows the call block in the re-rendered history).
Tool definitions
Each tool is declared in the system turn as <|tool>declaration:NAME{…}<tool|>, where the body is the schema serialized in the same brace syntax used by calls. Types are upper-cased strings (STRING, OBJECT, …). Byte-exact, from the guide:
<|tool>declaration:get_current_temperature{description:<|"|>Gets the current temperature for a given location.<|"|>,parameters:{properties:{location:{description:<|"|>The city name, e.g. San Francisco<|"|>,type:<|"|>STRING<|"|>} },required:[<|"|>location<|"|>],type:<|"|>OBJECT<|"|>} }<tool|>
Tool-call format
The model emits one call per <|tool_call>…<tool_call|> block. The body is call:NAME{ARGS}, where ARGS is a comma-separated list of key:value pairs:
<|tool_call>call:get_current_temperature{location:<|"|>London<|"|>}<tool_call|>
Value grammar inside {…}:
| Value kind | Encoding | Example |
|---|---|---|
| string | `< | " |
| int / float | bare | count:42 |
| bool | bare | flag:true |
| null | bare | unit:null |
| list | [v,v,…] |
`tags:[< |
| nested object | {k:v,…} |
`config:{theme:< |
The reference parser shipped in the guide:
[{
"name": name,
"arguments": {
k: cast((v1 or v2).strip())
for k, v1, v2 in re.findall(r'(\w+):(?:<\|"\|>(.*?)<\|"\|>|([^,}]*))', args)
}
} for name, args in re.findall(r"<\|tool_call>call:(\w+)\{(.*?)\}<tool_call\|>", text, re.DOTALL)]
i.e. each argument value is either a <|"|>…<|"|> string or a bare run of non-,} characters (cast to int/float/bool, else kept as a string).
Multiple / parallel tool calls
Parallel calls are consecutive <|tool_call>…<tool_call|> blocks (one call each), returned in order. The application returns one <|tool_response> per call in the same order.
Tool-result format
Each result is <|tool_response>response:NAME{…}<tool_response|>, the response object serialized in the same brace syntax. Byte-exact, from the guide's re-rendered history:
<|tool_response>response:get_current_weather{temperature:15,weather:<|"|>sunny<|"|>}<tool_response|>
End-to-end example
Byte-exact apply_chat_template output from the guide (system + tool, user, model call, tool response, final answer — note the response block sits in the same model turn, right after the call):
<bos><|turn>system
You are a helpful assistant.<|tool>declaration:get_current_weather{description:<|"|>Gets the current weather in a given location.<|"|>,parameters:{properties:{location:{description:<|"|>The city and state, e.g. "San Francisco, CA" or "Tokyo, JP"<|"|>,type:<|"|>STRING<|"|>},unit:{description:<|"|>The unit to return the temperature in.<|"|>,enum:[<|"|>celsius<|"|>,<|"|>fahrenheit<|"|>],type:<|"|>STRING<|"|>} },required:[<|"|>location<|"|>],type:<|"|>OBJECT<|"|>} }<tool|><turn|>
<|turn>user
Hey, what's the weather in Tokyo right now?<turn|>
<|turn>model
<|tool_call>call:get_current_weather{location:<|"|>Tokyo, JP<|"|>}<tool_call|><|tool_response>response:get_current_weather{temperature:15,weather:<|"|>sunny<|"|>}<tool_response|>The current weather in Tokyo is 15 degrees Celsius and sunny.<turn|>
Parsing notes & gotchas
- String delimiter is a token, not a quote. Inside
<|"|>…<|"|>the bytes"and,are literal data — the example<|"|>The city and state, e.g. "San Francisco, CA"…<|"|>contains both. Split arguments on,/}only outside a<|"|>…<|"|>span. - Asymmetric pipes. The closer is
<tool_call|>, not</tool_call>or<|tool_call>. Matching the wrong pipe side will never close the block. - One call per block. Unlike a JSON
tool_calls[]array, parallelism is "more blocks", not "more entries in one block". - Bare scalars. A value not wrapped in
<|"|>istrue/false→ bool,null/none→ null, numeric → number, otherwise a bare string (e.g. an unquoted enum or type name likeSTRING). - Not Gemma 3 / hosted Gemini. Those use the Pythonic
tool_code/default_apiform ingemini.md. Gemma 4 replaced it with this token syntax; the two are not interchangeable.
Sources
- Function calling with Gemma 4 (byte-exact chat-template renderings + reference parser): https://ai.google.dev/gemma/docs/capabilities/text/function-calling-gemma4
- Gemma 4 prompt formatting: https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4