Skip to content

fix(core): reject **kwargs keys that collide with named tool parameters - #4674

Open
rajarshidattapy wants to merge 2 commits into
openai:mainfrom
rajarshidattapy:fix/tool-kwargs-parameter-collision
Open

fix(core): reject **kwargs keys that collide with named tool parameters#4674
rajarshidattapy wants to merge 2 commits into
openai:mainfrom
rajarshidattapy:fix/tool-kwargs-parameter-collision

Conversation

@rajarshidattapy

Copy link
Copy Markdown
Contributor

This pull request fixes FuncSchema.to_call_args accepting **kwargs payloads that no Python call can express, which either silently discarded a validated argument or crashed the tool call with an unhandled TypeError.

The bug

A non-strict function tool that takes **kwargs accepts schema-valid input whose kwargs dict names one of the function's own parameters — the generated Pydantic model has a separate field for each named parameter and nothing constrains the keys inside the kwargs field. to_call_args splats that dict last, after the named parameters, with no intersection check. Both outcomes are wrong:

def g(*, opt: int = 1, **kw: Any) -> str: ...
# {"opt": 5, "kw": {"opt": 9}} -> args=[], kwargs={"opt": 9}
# runs as opt=9, kw={} -- the validated opt=5 is gone, no error, no log

def h(x: int, *rest: int, **kw: Any) -> str: ...
# {"x": 1, "rest": [2, 3], "kw": {"x": 99}} -> h(1, 2, 3, x=99)
# TypeError: h() got multiple values for argument 'x'

The invocation path converts validation failures into ModelBehaviorError one line earlier, but the TypeError from the call itself was not converted, so the run failed with an internal error instead of the model receiving actionable feedback.

The fix

to_call_args now raises ModelBehaviorError naming the conflicting keys, consistent with how the invocation path already reports validation failures. The check lives in to_call_args rather than at the call site because every caller routes through it.

Only parameters that the call binds by name are reserved. Two cases are deliberately left working, because the key genuinely belongs to **kwargs there:

  • positional-onlyf(1, a=2) for def f(a, /, **kw) is legal and routes a=2 into kw.
  • *args — the name binds no argument, so v(1, rest=5) for def v(*rest, **kw) is legal.

The fix also covers a case the report did not mention but that shares the root cause: the context parameter is passed positionally by the invocation path, so a kwargs key naming it (def f(ctx: RunContextWrapper[Any], **kw) with kw={"ctx": ...}) failed with the same got multiple values TypeError. It is now reported the same way.

Behavior notes

  • Only non-strict tools (strict_json_schema=False) are affected; strict mode already rejects **kwargs tools at definition time.
  • Input that previously produced a silent wrong-argument call now raises. That is the point of the change, but it is a behavior change for anyone who was relying on the override, so it is worth calling out.
  • The error message names only the tool and the conflicting parameter names, which are part of the schema the model already received, so it stays safe under tool-data redaction.

Tests

tests/test_function_schema.py gains a parametrized rejection test covering keyword-only, positional-or-keyword, keyword-only-after-*args, and context-parameter collisions, plus two tests pinning the positional-only and *args cases that must keep working. tests/test_function_tool.py gains an end-to-end check that the collision reaches the caller as ModelBehaviorError rather than TypeError. The rejection tests fail on main.

Resolves #4669.

`FuncSchema.to_call_args` splats the `**kwargs` dict last, after the named
parameters, with no intersection check. A schema-valid payload whose kwargs
dict carries a key naming one of the function's own parameters therefore had
two wrong outcomes: for a keyword-only parameter the validated value was
silently replaced, and for a positional-or-keyword parameter the call raised
`TypeError: got multiple values for argument`, which escaped the invocation
path as an internal error rather than as feedback the model could act on.

Such a call cannot be expressed in Python, so `to_call_args` now raises
`ModelBehaviorError` naming the conflicting keys, matching how the invocation
path already reports validation failures.

Positional-only parameters and `*args` are deliberately left out of the
reserved set: `f(1, a=2)` for `def f(a, /, **kw)` is a legal call that routes
`a=2` into `**kw`, and those cases keep working. The context parameter is
covered too -- it is passed positionally, so a kwargs key of the same name
broke the call the same way.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

to_call_args: **kwargs keys that collide with named parameters silently override them or crash with TypeError

2 participants