Skip to content

fix(openai_utils): strip redundant outer tool-call names - #3061

Merged
ananthsub merged 3 commits into
mainfrom
wprazuch/fix-toolcall-extra-forbid
Sep 3, 2026
Merged

fix(openai_utils): strip redundant outer tool-call names#3061
ananthsub merged 3 commits into
mainfrom
wprazuch/fix-toolcall-extra-forbid

Conversation

@wprazuch

@wprazuch wprazuch commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Some external chat-completions agents replay assistant tool calls with a redundant outer name alongside the standard nested function.name or custom.name. OpenAI's tool-call schema does not define the outer field.

PR #2456 configured NeMoGymChatCompletionCreateParamsNonStreaming to reject unknown fields. Pydantic applies that policy to its nested TypedDict models, so Gym now rejects the entire request with HTTP 422 when tool_calls[].name is present. Before that strict configuration was added, Gym accepted the request and omitted the redundant field before sending it to the model provider.

Fix

The tool-call list now removes only the redundant outer tool_calls[].name before strict schema validation. The normalization is copy-on-write, so it does not mutate the caller's request object.

The nested function.name or custom.name remains unchanged and continues to identify the tool. Every other unknown request or tool-call field remains a validation error. This avoids silently discarding provider state that may be required when an agent replays a tool call.

Relationship to #2963

This change and #2963 handle different fields:

Neither change replaces the other, and they can merge independently.

Validation

Regression coverage verifies function and custom tool calls, preservation of their nested names, rejection of unrelated unknown fields, caller-input immutability, and the payload received by the model server. The original Tau3 benchmark failure was reproduced with the same request shape, and removing the redundant outer field prevents the validation failure.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; unrelated changes are tracked separately.
  • Tests were added or updated for the changed behavior.
  • Pre-commit checks pass locally.
  • All commits include DCO sign-off.

`65129dd4` ("chore(deps): pin openai to 2.44.0", #2456, which closed
#2452) added `model_config = ConfigDict(extra="forbid")` to
`NeMoGymChatCompletionCreateParamsNonStreaming`. Pydantic propagates that
config into nested `TypedDict`s, so an unknown key anywhere in the
request -- including inside `tool_calls` -- became a hard 422 rather than
being ignored.

Agents echo tool calls back from whatever the provider returned, so
provider-specific keys on a tool call are routine. No benchmark under
`benchmarks/` emits that shape, which is why this went unnoticed, but any
external tool-calling agent hits it. Tau2/Tau3, whose agent lives in a
separate checkout, had all 776 rollouts of a run rejected.

Opt the two tool call shapes out of the inherited config so unknown keys
are dropped instead of rejected. The request model itself stays strict:
an unknown top-level request field is still a validation error, so the
schema-auditing intent of #2456 is preserved.

Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread nemo_gym/openai_utils.py Outdated
…ng extras

Review feedback: `extra="ignore"` was too broad. It silently removed every
non-OpenAI field from a tool call, and the config propagated into the
nested `function` TypedDict as well, so unknown fields disappeared from
`model_dump()`.

That is worse than the bug it fixed. Tool calls carry provider state that
a client must echo back verbatim -- Gemini returns
`tool_calls[].extra_content.google.thought_signature` and requires the
exact value on the next request -- so dropping it corrupts an otherwise
valid trajectory and fails later, out of context.

Declare the one known redundant field instead. `tool_calls[].name`
round-trips, and an unknown field on a tool call is an error again, which
fails loudly at the point of the mistake.

Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
Comment thread nemo_gym/openai_utils.py Outdated
Follow-up to #3061.

Strips the non-standard outer `tool_calls[].name` field before strict
validation while preserving nested function/custom-call names and all
other strict unknown-field checks. The sanitizer is copy-on-write so
caller input is not mutated.

Tests cover function and custom calls, preservation of nested names,
rejection of unrelated extra fields, and the provider-boundary payload.
@ananthsub ananthsub changed the title openai_utils: stop rejecting tool calls that carry unknown keys fix(openai_utils): strip redundant outer tool-call names Sep 3, 2026
@ananthsub

Copy link
Copy Markdown
Contributor

/ok to test 7e5a057

@ananthsub
ananthsub enabled auto-merge (squash) September 3, 2026 20:53
@ananthsub
ananthsub merged commit 538886b into main Sep 3, 2026
38 checks passed
@ananthsub
ananthsub deleted the wprazuch/fix-toolcall-extra-forbid branch September 3, 2026 21:04
j-nolan added a commit to j-nolan/Gym that referenced this pull request Sep 4, 2026
NVIDIA-NeMo#3061 added guards that the tool-call name normalization must not loosen the request
model. Relaxing the schema loosens it deliberately, so those assertions now describe
behaviour the model no longer has.

Invert the two that assert rejection, and add one that pins down what did not change: a
tool call missing its required function is still rejected, so carrying unknown keys has
not turned off structural validation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
j-nolan added a commit to j-nolan/Gym that referenced this pull request Sep 4, 2026
The model server forwards to engines that accept fields the OpenAI types do not define,
and callers send them. vLLM's chat_template_kwargs selects a chat template variant and is
how a caller turns a model's thinking mode on; clients also label tool results with the
tool's name. Forbidding extras rejects the request outright, so the caller loses the field
and the conversation with it.

Requests are inbound from callers Gym does not control, so carrying what is not modelled is
the right default for a proxy. Being strict about what Gym itself sends is unaffected, and
structure is still validated: a tool call missing its required function is still rejected,
which the new test pins down.

This supersedes adding fields one at a time. Two have needed it already, name and
chat_template_kwargs, and each was a separate change for a field vLLM already accepts.

NVIDIA-NeMo#3061 added guards that tool-call name normalization must not loosen the request model.
That loosening is now deliberate, so those assertions are inverted to describe it.

Signed-off-by: James Nolan <jnolan@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants