Files
xgrammar-gemma4-test/report.md
T
wanhae.lee c70992e51a .
2026-07-05 00:41:50 +09:00

18 KiB

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 <tool_call|> into <|tool_response>. Per the Gemma 4 prompt-formatting spec, the model is only responsible for the call up to <tool_call|>; <|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}<tool_call|>

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
}<tool_call|>

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
}<tool_call|>

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}<tool_call|>

A. complex-schema / unconstrained #0 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

A. complex-schema / unconstrained #1 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

A. complex-schema / unconstrained #2 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

A. complex-schema / unconstrained #3 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|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.<channel|><|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.<channel|><|tool_call>call:convert_temperature{
    	value:3,
    	from_unit:<|"|>celsius<|"|>,
    	to_unit:<|"|>fahrenheit<|"|>
}<tool_call|>

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.<channel|><|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.<channel|><|tool_call>call:convert_temperature{value:3,from_unit:<|"|>celsius<|"|>,to_unit:<|"|>fahrenheit<|"|>}<tool_call|>

B. multiturn-thinking / unconstrained #0 — FAIL (failed stops_at_boundary, thought_ok: ran past <tool_call|> 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_call|><|tool_response>

B. multiturn-thinking / unconstrained #1 — FAIL (failed stops_at_boundary, thought_ok: ran past <tool_call|> 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_call|><|tool_response>

B. multiturn-thinking / unconstrained #2 — FAIL (failed stops_at_boundary, thought_ok: ran past <tool_call|> 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_call|><|tool_response>

B. multiturn-thinking / unconstrained #3 — FAIL (failed stops_at_boundary, thought_ok: ran past <tool_call|> 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_call|><|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|><|tool_call>call:escalate_incident{incident_id:<|"|>TICKET_ID_FROM_PREVIOUS_CALL<|"|>,level:1}<tool_call|>

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|><|tool_call>call:escalate_incident{incident_id:<|"|>TICKET_ID<|"|>,level:1}<tool_call|>

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|><|tool_call>call:escalate_incident{incident_id:<|"|>(id from create_ticket response)<|"|>,level:1}<tool_call|>

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|><|tool_call>call:escalate_incident{incident_id:<|"|>[created_ticket_id]<|"|>,level:1}<tool_call|>

C. adversarial-payload / unconstrained #0 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

C. adversarial-payload / unconstrained #1 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

C. adversarial-payload / unconstrained #2 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>

C. adversarial-payload / unconstrained #3 — FAIL (failed stops_at_boundary: ran past <tool_call|> 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_call|><|tool_response>