Skip to content

[Bugfix] Clear conflicting structured outputs in strict tool calling - #44134

Closed
alexeldeib wants to merge 1 commit into
vllm-project:mainfrom
alexeldeib:alex/fix-strict-tool-calling-structured-outputs
Closed

[Bugfix] Clear conflicting structured outputs in strict tool calling#44134
alexeldeib wants to merge 1 commit into
vllm-project:mainfrom
alexeldeib:alex/fix-strict-tool-calling-structured-outputs

Conversation

@alexeldeib

@alexeldeib alexeldeib commented May 31, 2026

Copy link
Copy Markdown
Contributor

Purpose

Fix strict tool-calling request adjustment when a structural tag is installed over an existing structured-output constraint or response_format.

With VLLM_ENFORCE_STRICT_TOOL_CALLING, ToolParser.adjust_request could mutate structured_outputs.structural_tag in place and leave an existing json/regex/choice/grammar/json_object constraint or a response_format. A later validation step then rejects the request with the one-constraint error. This change rebuilds structured_outputs with only the structural tag, preserves whitespace/additional-property knobs, and clears response_format.

Duplicate-work check: open PR searches for VLLM_ENFORCE_STRICT_TOOL_CALLING structural_tag response_format, strict tool calling structured_outputs structural_tag, ToolParser adjust_request response_format structural outputs, You can only use one kind structured outputs constraint structural_tag, and strict structural tag conflicting json found no duplicate generic ToolParser.adjust_request fix. #41178 and #35904 are related but target model/API-specific paths.

No docs update is needed. AI assistance was used; I reviewed the changed code and test results.

Test Plan

.venv/bin/python -m pytest tests/tool_use/test_strict_tool_calling_adjust_request.py -q
pre-commit run --files \
  tests/tool_use/test_strict_tool_calling_adjust_request.py \
  vllm/tool_parsers/abstract_tool_parser.py

Test Result

Local validation after rebasing on origin/main:

# pytest assertions passed with torch.accelerator.empty_cache disabled locally
# to avoid a macOS MPS teardown bug unrelated to these tests.
5 passed
pre-commit run --files \
  tests/tool_use/test_strict_tool_calling_adjust_request.py \
  vllm/tool_parsers/abstract_tool_parser.py
# Passed

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model. Not applicable.

@mergify mergify Bot added tool-calling bug Something isn't working labels May 31, 2026
ToolParser.adjust_request's strict structural-tag path (added in vllm-project#40894, gated by
VLLM_ENFORCE_STRICT_TOOL_CALLING) installs structural_tag on a pre-existing
StructuredOutputsParams via in-place attribute assignment and returns without
nulling response_format. The in-place set bypasses
StructuredOutputsParams.__post_init__, so the params keep a prior
mutually-exclusive constraint (json/regex/choice/grammar/json_object, or one
lowered from response_format) next to the new structural_tag. On the next
re-validation this trips the one-constraint invariant, so a strict-mode request
that also carries a structured-output constraint or a response_format fails with:

    ValueError: You can only use one kind of structured outputs constraint
    but multiple are specified

This affects any parser that installs a structural tag -- currently DeepSeek-V4
and Qwen3-Coder via get_structural_tag. The env var is off by default, and a
request with no pre-existing constraint is unaffected.

Fix: rebuild structured_outputs with only the structural tag (preserving the
whitespace / additional-properties knobs) and null response_format, mirroring
Step 2 of the same method. This "tool constraint wins, response_format dropped"
resolution already exists in Step 2 and the DeepSeek-V3.2 override (vllm-project#41178), and
is the intent of the open auto-path fix vllm-project#39969; the in-place-vs-rebuild trade-off
was discussed on vllm-project#40894 and vllm-project#43155 (whose Kimi path already rebuilds).

Repro / regression test (CPU, no model required):

    pytest tests/tool_use/test_strict_tool_calling_adjust_request.py

The added tests enable strict mode, give a parser a structural tag, and send
tools together with a response_format or a structured_outputs.json constraint
(tool_choice auto and required). On the pre-fix code adjust_request leaves two
constraints, and to_sampling_params raises the ValueError above; with this change
structured_outputs holds only the structural tag, response_format is None, and
the user's whitespace knobs are preserved. The conflict tests fail without this
patch and pass with it; the no-pre-existing-constraint case passes either way.

Equivalently over HTTP: with strict mode on, a tool_choice="auto" request that
also sets response_format returns HTTP 400 (the error above) before this change
and a normal tool call after; a required-tool request is unaffected because that
path already rebuilds.

Signed-off-by: Ace Eldeib <aeldeib@coreweave.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
@alexeldeib
alexeldeib force-pushed the alex/fix-strict-tool-calling-structured-outputs branch from e58f771 to 147136c Compare June 10, 2026 03:18
@alexeldeib
alexeldeib marked this pull request as ready for review June 10, 2026 03:18
@mergify

mergify Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @alexeldeib.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jun 12, 2026
@alexeldeib

Copy link
Copy Markdown
Contributor Author

Superseded by #45003: AbstractParser._apply_structural_tag now builds a fresh StructuredOutputsParams(structural_tag=...) and clears response_format, which is exactly the conflicting-constraint fix here (the in-place .structural_tag = path this patched no longer exists). Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working needs-rebase tool-calling

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant