# Gemma-4 tool calling: constrained (xgrammar) vs unconstrained - model: `google/gemma-4-E2B-it` via HF transformers - constraint: `xgr.get_model_structural_tag("gemma_4", tools=..., tool_choice="required")` compiled and applied with `xgr.contrib.hf.LogitsProcessor` - 4 samples per scenario per mode; identical sampling settings for both modes (per-scenario values below) - outputs validated with a pure-Python port of sglang's `Gemma4Detector` parser plus `jsonschema` ## Results (passing samples / total) ### A. complex-schema temperature=0.9, top_p=0.95, max_new_tokens=384, reasoning=False | metric | constrained | unconstrained | |---|---|---| | well_formed | 4/4 ✅ | 4/4 ✅ | | valid_name | 4/4 ✅ | 4/4 ✅ | | schema_valid | 4/4 ✅ | 4/4 ✅ | | stops_at_boundary | 4/4 ✅ | 0/4 ❌ | ### B. multiturn-thinking temperature=0.9, top_p=0.95, max_new_tokens=384, reasoning=True | metric | constrained | unconstrained | |---|---|---| | well_formed | 4/4 ✅ | 4/4 ✅ | | valid_name | 4/4 ✅ | 4/4 ✅ | | schema_valid | 4/4 ✅ | 4/4 ✅ | | stops_at_boundary | 4/4 ✅ | 0/4 ❌ | | thought_ok | 4/4 ✅ | 0/4 ❌ | ### C. adversarial-payload temperature=1.5, top_p=1.0, max_new_tokens=640, reasoning=False | metric | constrained | unconstrained | |---|---|---| | well_formed | 4/4 ✅ | 4/4 ✅ | | valid_name | 4/4 ✅ | 4/4 ✅ | | schema_valid | 4/4 ✅ | 4/4 ✅ | | stops_at_boundary | 4/4 ✅ | 0/4 ❌ | ## Metric definitions - `well_formed` — every tool call block is complete and parseable (sglang parser port) - `valid_name` — every called tool exists - `schema_valid` — parsed arguments validate against the tool's JSON schema - `stops_at_boundary` — generation did not run past into <|tool_response>. Per the Gemma 4 prompt-formatting spec, the model is only responsible for the call up to ; <|tool_response> is appended by the application with the real tool result, and is registered as an additional stop sequence purely as a backstop. This harness configures no such stop, so unconstrained runs free-run past the boundary into engine-owned territory, while required-mode grammar ends the call cleanly at an accept state. - `thought_ok` — <|channel>thought opened and closed before the first tool call ## Sample outputs (raw) ### A. complex-schema **A. complex-schema / constrained #0** — PASS ✅ ``` <|tool_call>call:create_calendar_event{title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,duration_minutes:90,priority:<|"|>high<|"|>,attendees:[{name:<|"|>Alice Kim<|"|>,email:<|"|>alice.kim@example.com<|"|>},{name:<|"|>Bob Lee<|"|>,email:<|"|>bob.lee@example.com<|"|>},{name:<|"|>Chris Park<|"|>,email:<|"|>chris.park@example.com<|"|>}],location:{ room:<|"|>Jupiter<|"|>,floor:7},send_invites:true} ``` **A. complex-schema / constrained #1** — PASS ✅ ``` <|tool_call>call:create_calendar_event{ title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>, start:<|"|>2026-07-10T14:00:00+09:00<|"|>, duration_minutes: 90, priority:<|"|>high<|"|>, attendees: [ { name:<|"|>Alice Kim<|"|>, email:<|"|>alice.kim@example.com<|"|> }, { name:<|"|>Bob Lee<|"|>, email:<|"|>bob.lee@example.com<|"|> }, { name:<|"|>Chris Park<|"|>, email:<|"|>chris.park@example.com<|"|> } ], location: { room:<|"|>Jupiter<|"|>, floor: 7 }, send_invites: true } ``` **A. complex-schema / constrained #2** — PASS ✅ ``` <|tool_call>call:create_calendar_event{ title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>, start:<|"|>2026-07-10T14:00:00+08:00<|"|>, duration_minutes: 90, priority:<|"|>high<|"|>, attendees: [ { name:<|"|>Alice Kim<|"|>, email:<|"|>alice.kim@example.com<|"|> }, { name:<|"|>Bob Lee<|"|>, email:<|"|>bob.lee@example.com<|"|> }, { name:<|"|>Chris Park<|"|>, email:<|"|>chris.park@example.com<|"|> } ], location: { room:<|"|>Jupiter<|"|>, floor: 7 }, send_invites: true } ``` **A. complex-schema / constrained #3** — PASS ✅ ``` <|tool_call>call:create_calendar_event{title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,duration_minutes:90,priority:<|"|>high<|"|>,attendees:[{name:<|"|>Alice Kim<|"|>,email:<|"|>alice.kim@example.com<|"|>},{name:<|"|>Bob Lee<|"|>,email:<|"|>bob.lee@example.com<|"|>},{name:<|"|>Chris Park<|"|>,email:<|"|>chris.park@example.com<|"|>}],location:{room:<|"|>Jupiter<|"|>,floor:7},send_invites:true} ``` **A. complex-schema / unconstrained #0** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_calendar_event{attendees:[{email:<|"|>alice.kim@example.com<|"|>,name:<|"|>Alice Kim<|"|>},{email:<|"|>bob.lee@example.com<|"|>,name:<|"|>Bob Lee<|"|>},{email:<|"|>chris.park@example.com<|"|>,name:<|"|>Chris Park<|"|>}],duration_minutes:90,location:{floor:7,room:<|"|>Jupiter<|"|>},priority:<|"|>high<|"|>,send_invites:true,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>}<|tool_response> ``` **A. complex-schema / unconstrained #1** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_calendar_event{attendees:[{email:<|"|>alice.kim@example.com<|"|>,name:<|"|>Alice Kim<|"|>},{email:<|"|>bob.lee@example.com<|"|>,name:<|"|>Bob Lee<|"|>},{email:<|"|>chris.park@example.com<|"|>,name:<|"|>Chris Park<|"|>}],duration_minutes:90,location:{floor:7,room:<|"|>Jupiter<|"|>},priority:<|"|>high<|"|>,send_invites:true,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>}<|tool_response> ``` **A. complex-schema / unconstrained #2** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_calendar_event{attendees:[{email:<|"|>alice.kim@example.com<|"|>,name:<|"|>Alice Kim<|"|>},{email:<|"|>bob.lee@example.com<|"|>,name:<|"|>Bob Lee<|"|>},{email:<|"|>chris.park@example.com<|"|>,name:<|"|>Chris Park<|"|>}],duration_minutes:90,location:{floor:7,room:<|"|>Jupiter<|"|>},priority:<|"|>high<|"|>,send_invites:true,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>}<|tool_response> ``` **A. complex-schema / unconstrained #3** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_calendar_event{attendees:[{email:<|"|>alice.kim@example.com<|"|>,name:<|"|>Alice Kim<|"|>},{email:<|"|>bob.lee@example.com<|"|>,name:<|"|>Bob Lee<|"|>},{email:<|"|>chris.park@example.com<|"|>,name:<|"|>Chris Park<|"|>}],duration_minutes:90,location:{floor:7,room:<|"|>Jupiter<|"|>},priority:<|"|>high<|"|>,send_invites:true,start:<|"|>2026-07-10T14:00:00+09:00<|"|>,title:<|"|>Q3 Roadmap Review ("final" draft)<|"|>}<|tool_response> ``` ### B. multiturn-thinking **B. multiturn-thinking / constrained #0** — PASS ✅ ``` <|channel>thought The user wants to compare the weather in Seoul and Busan in Celsius, one city at a time. After getting both temperatures, the user specifically requested to convert Seoul's temperature to Fahrenheit using the `convert_temperature` tool before providing the final answer. 1. **Seoul Weather (Celsius):** Already retrieved: 3°C. 2. **Busan Weather (Celsius):** Already retrieved: 8°C. 3. **Convert Seoul's temperature to Fahrenheit:** I need to call `convert_temperature` with `value=3`, `from_unit="celsius"`, and `to_unit="fahrenheit"`. After this conversion, I will present the comparison and the converted temperature. **Plan:** 1. Use the result from the previous calls (Seoul: 3°C, Busan: 8°C). 2. Call `convert_temperature` for Seoul (3°C to Fahrenheit). 3. Synthesize the final answer.<|tool_call>call:convert_temperature{ value:3, from_unit:<|"|>celsiu ``` **B. multiturn-thinking / constrained #1** — PASS ✅ ``` <|channel>thought The user wants me to perform three main tasks: 1. Get the current weather in Seoul in Celsius. (Done) 2. Get the current weather in Busan in Celsius. (Done) 3. Convert Seoul's temperature from Celsius to Fahrenheit using the `convert_temperature` tool. 4. Compare the weather for both cities and present the final answer, including the conversion result. From the previous steps, I have: * Seoul weather: 3°C and sunny. * Busan weather: 8°C and cloudy. Now I need to call the `convert_temperature` tool for Seoul's temperature (3°C to Fahrenheit). After that, I will compile all the information to answer the user's request.<|tool_call>call:convert_temperature{ value:3, from_unit:<|"|>celsius<|"|>, to_unit:<|"|>fahrenheit<|"|> } ``` **B. multiturn-thinking / constrained #2** — PASS ✅ ``` <|channel>thought The user wants to compare the current weather in Seoul and Busan in Celsius, one city at a time. After getting both, the user wants to convert Seoul's temperature to Fahrenheit using the `convert_temperature` tool before providing the final answer. 1. **Get Seoul's weather in Celsius:** This has been done. The result is: `{"temp_c": 3, "condition": "sunny", "humidity": 41}`. 2. **Get Busan's weather in Celsius:** This has been done. The result is: `{"temp_c": 8, "condition": "cloudy", "humidity": 63}`. 3. **Convert Seoul's temperature to Fahrenheit:** I need to use the `convert_temperature` tool with the Seoul temperature (3°C) from Celsius to Fahrenheit. Now I will execute the conversion call. After that, I will compile all the information to answer the user's request.<|tool_call>call:convert_temperature{value:3,from_unit:<|"|>celsius<|"|>,to_unit:<|"|>f ``` **B. multiturn-thinking / constrained #3** — PASS ✅ ``` <|channel>thought The user wants to: 1. Get the current weather in Seoul in Celsius. (Done) 2. Get the current weather in Busan in Celsius. (Done) 3. Convert Seoul's temperature from Celsius to Fahrenheit using the `convert_temperature` tool. (Pending) 4. Compare the weather, presenting the results one city at a time, and including the conversion. I have the following information: * Seoul weather: 3°C, sunny, 41% humidity. * Busan weather: 8°C, cloudy, 63% humidity. Now I need to call `convert_temperature` for Seoul's temperature (3°C to Fahrenheit). After that, I will compile the final answer.<|tool_call>call:convert_temperature{value:3,from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>} ``` **B. multiturn-thinking / unconstrained #0** — FAIL ❌ (failed `stops_at_boundary`, `thought_ok`: ran past into <|tool_response> after the call; skipped or never closed the <|channel>thought section) ``` <|tool_call>call:convert_temperature{from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>,value:3}<|tool_response> ``` **B. multiturn-thinking / unconstrained #1** — FAIL ❌ (failed `stops_at_boundary`, `thought_ok`: ran past into <|tool_response> after the call; skipped or never closed the <|channel>thought section) ``` <|tool_call>call:convert_temperature{from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>,value:3}<|tool_response> ``` **B. multiturn-thinking / unconstrained #2** — FAIL ❌ (failed `stops_at_boundary`, `thought_ok`: ran past into <|tool_response> after the call; skipped or never closed the <|channel>thought section) ``` <|tool_call>call:convert_temperature{from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>,value:3}<|tool_response> ``` **B. multiturn-thinking / unconstrained #3** — FAIL ❌ (failed `stops_at_boundary`, `thought_ok`: ran past into <|tool_response> after the call; skipped or never closed the <|channel>thought section) ``` <|tool_call>call:convert_temperature{from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>,value:3}<|tool_response> ``` ### C. adversarial-payload **C. adversarial-payload / constrained #0** — PASS ✅ ``` <|tool_call>call:create_ticket{title:<|"|>Payments retry storm after config rollout<|"|>,body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42).<|"|>,severity:<|"|>blocker<|"|>,labels:[<|"|>bug<|"|>,<|"|>regression<|"|>,<|"|>backend<|"|>],affected:{service:<|"|>payments<|"|>,version:<|"|>2.14.1<|"|>,regions:[<|"|>ap-northeast-2<|"|>,<|"|>us-east-1<|"|>]},cc_emails:[<|"|>dev-alerts@example.com<|"|>],estimated_minutes:45}<|tool_call>call:escalate_incident{incident_id:<|"|>TICKET_ID_FROM_PREVIOUS_CALL<|"|>,level:1} ``` **C. adversarial-payload / constrained #1** — PASS ✅ ``` <|tool_call>call:create_ticket{ title:<|"|>Payments retry storm after config rollout<|"|>, body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42)<|"|>, severity:<|"|>blocker<|"|>, labels: [ <|"|>bug<|"|>, <|"|>regression<|"|>, <|"|>backend<|"|> ], affected: { service:<|"|>payments<|"|>, version:<|"|>2.14.1<|"|>, regions: [ <|"|>ap-northeast-2<|"|>, <|"|>us-east-1<|"|> ] }, cc_emails: [ <|"|>dev-alerts@example.com<|"|> ], estimated_minutes: 45 }<|tool_call>call:escalate_incident{incident_id:<|"|>TICKET_ID<|"|>,level:1} ``` **C. adversarial-payload / constrained #2** — PASS ✅ ``` <|tool_call>call:create_ticket{ title:<|"|>Payments retry storm after config rollout<|"|>, body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42)<|"|>, severity:<|"|>blocker<|"|>, labels: [ <|"|>bug<|"|>, <|"|>regression<|"|>, <|"|>backend<|"|> ], affected: { service:<|"|>payments<|"|>, version:<|"|>2.14.1<|"|>, regions: [ <|"|>ap-northeast-2<|"|>, <|"|>us-east-1<|"|> ] }, cc_emails: [ <|"|>dev-alerts@example.com<|"|> ], estimated_minutes: 45 }<|tool_call>call:escalate_incident{incident_id:<|"|>(id from create_ticket response)<|"|>,level:1} ``` **C. adversarial-payload / constrained #3** — PASS ✅ ``` <|tool_call>call:create_ticket{ title:<|"|>Payments retry storm after config rollout<|"|>, body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42)<|"|>, severity:<|"|>blocker<|"|>, labels: [ <|"|>bug<|"|>, <|"|>regression<|"|>, <|"|>backend<|"|> ], affected: { service:<|"|>payments<|"|>, version:<|"|>2.14.1<|"|>, regions: [ <|"|>ap-northeast-2<|"|>, <|"|>us-east-1<|"|> ] }, cc_emails: [ <|"|>dev-alerts@example.com<|"|> ], estimated_minutes: 45 }<|tool_call>call:escalate_incident{incident_id:<|"|>[created_ticket_id]<|"|>,level:1} ``` **C. adversarial-payload / unconstrained #0** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_ticket{affected:{regions:[<|"|>ap-northeast-2<|"|>,<|"|>us-east-1<|"|>],service:<|"|>payments<|"|>,version:<|"|>2.14.1<|"|>},body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42).<|"|>,cc_emails:[<|"|>dev-alerts@example.com<|"|>],estimated_minutes:45,labels:[<|"|>regression<|"|>,<|"|>backend<|"|>],severity:<|"|>blocker<|"|>,title:<|"|>Payments retry storm after config rollout<|"|>,urgent_escalation:true}<|tool_response> ``` **C. adversarial-payload / unconstrained #1** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_ticket{affected:{regions:[<|"|>ap-northeast-2<|"|>,<|"|>us-east-1<|"|>],service:<|"|>payments<|"|>,version:<|"|>2.14.1<|"|>},body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42).<|"|>,cc_emails:[<|"|>dev-alerts@example.com<|"|>],estimated_minutes:45,labels:[<|"|>bug<|"|>,<|"|>regression<|"|>,<|"|>backend<|"|>],severity:<|"|>blocker<|"|>,title:<|"|>Payments retry storm after config rollout<|"|>,urgent_escalation:true}<|tool_response> ``` **C. adversarial-payload / unconstrained #2** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_ticket{affected:{regions:[<|"|>ap-northeast-2<|"|>,<|"|>us-east-1<|"|>],service:<|"|>payments<|"|>,version:<|"|>2.14.1<|"|>},body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42)<|"|>,cc_emails:[<|"|>dev-alerts@example.com<|"|>],estimated_minutes:45,labels:[<|"|>bug<|"|>,<|"|>regression<|"|>,<|"|>backend<|"|>],severity:<|"|>blocker<|"|>,title:<|"|>Payments retry storm after config rollout<|"|>,urgent_escalation:true}<|tool_response> ``` **C. adversarial-payload / unconstrained #3** — FAIL ❌ (failed `stops_at_boundary`: ran past into <|tool_response> after the call) ``` <|tool_call>call:create_ticket{affected:{regions:[<|"|>ap-northeast-2<|"|>,<|"|>us-east-1<|"|>],service:<|"|>payments<|"|>,version:<|"|>2.14.1<|"|>},body:<|"|>Deployed config: {"retry": {"max": 3, "backoff_ms": [100, 200], "jitter": true}} Error line: TypeError: cannot destructure {id: undefined} at applyRetry (retry.js:42).<|"|>,cc_emails:[<|"|>dev-alerts@example.com<|"|>],estimated_minutes:45,labels:[<|"|>bug<|"|>,<|"|>regression<|"|>,<|"|>backend<|"|>],severity:<|"|>blocker<|"|>,title:<|"|>Payments retry storm after config rollout<|"|>,urgent_escalation:true}<|tool_response> ```