fix(rollout-gateway): parse tool calls with model-native response schemas, auto-detected from the chat template - #97
Merged
Merged
Conversation
…onse, auto-detected from the chat template The default derender's XML tool-call regex silently missed Qwen3's JSON <tool_call> format: zero matches, ill_formed=False hardcoded, the agent saw a plain-text turn and stopped — the validated GSM8K run was single-turn/no-tool without any signal. Fix the default instead of adding config: - response_schemas.py: response-schema dicts vendored from huggingface/trl (Apache-2.0, baseline 7073af94; see NOTICE) for qwen3 (JSON <tool_call>: Qwen2.5/Qwen3/Instruct-2507/VL), qwen3_5 (XML <function=>: Qwen3.5/3.6, Nemotron-3), llama3, glm4moe, gptoss — plus a sha256(chat_template) -> schema table for auto-detection. Byte-exact matching: fine-tunes inherit the template and keep resolving; a revised template stops matching loudly rather than misparsing silently. - HfTemplateRenderer: when the tokenizer's template hash matches, derender the whole output in one tokenizer.parse_response pass (reasoning + text + tool calls, ordered by the schema's anchored regex, so a literal <tool_call> inside a think block cannot corrupt the split). Parse failures and structurally invalid tool calls degrade in place to raw text with ill_formed=True — flagged, never a 500, never partial extraction. - No implicit tool parser anymore: with no matched schema and no injected tool_parser, a tools-bearing parse raises instead of guessing with the one-format regex (parsing.parse_tool_uses remains available by explicit injection). Injected stage parsers disable detection entirely — explicit wins (slime's SGLang detectors path is unchanged). - Drop the two no-op vLLM parser flags from the GSM8K script: they configure vLLM's chat layer, which the token-in/token-out path never touches. - gateway extra: transformers floor 4.44 -> 5.0 (tokenizer.parse_response and the schema engine shipped in 5.0.0); declare jmespath (schema transforms). The verl integration needs zero changes: gateway_host's bare HfTemplateRenderer(tokenizer) construction now does the right thing for every covered model family.
…i_key
The agent previously derived its gateway session key from the ACR runtime
session id (context.session_id). Make the trainer supply it explicitly instead:
AgentCoreAgentLoop passes api_key=sid through RolloutClient's _rollout
overrides, and the math agent reads payload['_rollout'].get('api_key') — the
key is trainer-supplied configuration like base_url/model_id, not something
the agent infers from its runtime. This also removes rl_app.py's dead
assignment left by the #91/#93 merge (the payload-derived api_key was
immediately overwritten by context.session_id).
Backward compatible both ways: the loop's sid still doubles as the ACR
runtimeSessionId, so an already-deployed image reading context.session_id
produces the same key; and 'EMPTY' still covers local runs and evaluation
endpoints that ignore the api key.
…plate Pass return_dict=False instead of accepting the BatchEncoding default and unwrapping input_ids after the fact. The bundled attention mask is a padding artifact the training backend builds itself when batching rows (verl's own apply_chat_template wrapper defaults return_dict=False for the same reason). The stub tokenizer now mirrors the real API's dict-by-default behavior, so dropping the kwarg fails tests on behavior, not just the kwargs echo.
Remove LLAMA3_SCHEMA, its registry entry, and the Llama 3.1/3.2 template hashes: Llama 3.x is not a fine-tuning target worth carrying a vendored schema for. Also drops the only hub-gated repos from template verification, so hash checks need no HF token.
…directly test_render.py: drop test_tool_parser_ill_formed_propagates (asserted a hard-coded lambda return, already covered by the stage-sequence test) and test_recognized_chat_template_switches_to_schema_derender (duplicated the schema-path coverage and leaked a fake hash into the global registry). test_response_schemas.py: rewrite without the sentinel-template/hash- registration machinery — tests assign the schema on the renderer directly. Down to what only this repo can break: one golden-path test per vendored schema dict (a transcription error in the hand-copied regexes would parse wrong silently) and the _parse_with_schema degradation contract (parse failures degrade to raw text flagged ill_formed, never an exception or a silently dropped call). Adversarial regex cases are TRL's own test surface; detection is covered live in test_template_hashes_live.py.
Detection is byte-exact and the offline unit tests bypass it, so a mistranscribed registry hash — or a vendor revising a chat template upstream — would silently drop real models into the no-schema rejection. Fetch each registered hash's actual template from the hub (a few KB per repo, never weights; one repo per hash, e.g. both Qwen3.5 think/nothink variants) and assert it resolves to the expected schema. Always-on: runs in the default suite and CI.
Linbo-Liu
reviewed
Aug 3, 2026
|
|
||
| # Qwen2.5 / Qwen3 (thinking) / Qwen3-Instruct-2507 / Qwen3-VL: JSON tool calls inside | ||
| # <tool_call>...</tool_call>, optional <think> block, <|im_end|> end-of-turn. | ||
| QWEN3_SCHEMA = { |
Contributor
There was a problem hiding this comment.
Why copy them over from: https://github.com/huggingface/trl/blob/main/trl/chat_template_utils.py?
Contributor
Author
There was a problem hiding this comment.
otherwise we will need to add TRL as a dependency : )
lyzustc
approved these changes
Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The issue
The gateway's default derender parsed tool calls with a regex for one specific XML format (
<tool_call><function=...>). Qwen3 emits JSON tool calls (<tool_call>{"name": ..., "arguments": ...}</tool_call>), so the regex never matched — and the failure was invisible three times over:parse_tool_useshardcodedill_formed=False, so no flag was raised;<tool_call>block flowed back to the agent as plain text, so Strands saw a turn with no tool calls and simply ended the conversation;#### <answer>, and Qwen3-4B can solve GSM8K mentally — so the previous "validated" run reached ~0.93 val reward while never firing a single tool call. Every trajectory was single-turn; the multi-turn concatenation and tool loss-mask paths (the gateway's core purpose) had never actually been exercised.The
engine_kwargs.vllm.tool_call_parser=hermesflags in the run script were no-ops: verl's token-in/token-out channel never touches vLLM's chat-completions layer where that parser lives.The fix
Make the default correct instead of adding configuration:
rollout_gateway/response_schemas.py: response-schema dicts vendored from huggingface/trl (Apache-2.0, baseline7073af94, see NOTICE) covering Qwen2.5 / Qwen3 / Qwen3-Instruct-2507 / Qwen3-VL (JSON<tool_call>), Qwen3.5 / Qwen3.6 / Nemotron-3 (XML<function=>), GLM4-MoE, and GPT-OSS — plus asha256(chat_template) → schematable.HfTemplateRendererhashes the tokenizer's chat template at construction; on a match it derenders the whole output in onetokenizer.parse_response(text, schema=...)pass — reasoning, text, and tool calls together, ordered by the schema's anchored regex (a literal<tool_call>inside a think block cannot corrupt the split). A schema is pure data; the parsing engine ships in transformers (>= 5.0).tool_parser=parse_tool_uses). Malformed model output degrades in place to raw text withill_formed=True— flagged, never a 500, never partial extraction.gateway_host's existing bareHfTemplateRenderer(tokenizer)construction now does the right thing.The agent previously derived its gateway session key from the ACR runtime session id (
context.session_id). The trainer now passes it explicitly as_rollout.api_key(it still equals the ACRruntimeSessionId, so already-deployed agent images keep working), andrl_app.pyreads it from the payload — removing a dead assignment left by the #91/#93 merge and keeping the agent free of any inference-infrastructure awareness.Validation
test_template_hashes_live.pyverifies every vendored template hash against the actual hub templates — one repo per registered hash, incl. both Qwen3.5 think/nothink variants). Real-tokenizer probes: Qwen3-4B-Instruct-2507 auto-detectsqwen3, Qwen3.6-35B-A3B auto-detectsqwen3_5, both parse their native formats correctly.verl.trainer.main_ppo, Qwen3-4B-Instruct-2507 full-FT on 8 GPUs, live AgentCore Runtime rollouts):num_turnsmean 2.20 / max 5 (verl counts the user message, sonum_turns=2= one LLM turn with no tool call; >2 = tool loop). The previous run was uniformly 2 — no tool call ever — so this is the first real exercise of multi-turn concatenation and tool loss-mask boundaries.ill_formedturns, zero parse errors across all rollouts (~70k over two full epochs).num_turnsrises early (mean ~3.1 by step 6) then collapses back to 2.0 by step ~16 — the policy learns to drop the calculator, since the answer-only GSM8K reward gives tool use no advantage for a model that solves GSM8K mentally. That is a task/reward-design property, orthogonal to this fix; the parsing fix is what made those early tool turns possible (and trainable) at all.agent.num_workers=2also validated: two AgentLoopWorkers, each with its own gateway on an auto-assigned port, identical metrics.Also in this PR
gatewayextra: transformers floor>=4.44→>=5.0(tokenizer.parse_response);jmespathdeclared (schema transforms).verl-experimentalextras: transformers pinned5.12.1(compatible with the pinned verl78bba31dand vllm 0.23.0).output_parserparagraph describing a never-implemented seam; added the TRL row to the
vendoring/re-sync table.
render()requests bare token ids (return_dict=False) instead of unwrapping a BatchEncoding; llama3 schema dropped (not a fine-tuning target); tautological renderer/schema tests removed in favor of golden-path + degradation-contract coverage.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.