Skip to content

fix(core): reject fixed-length tuple annotations for **kwargs variadic arguments - #4742

Closed
hktitof wants to merge 1 commit into
openai:mainfrom
hktitof:core/fail-fast-kwargs-tuple-annotation
Closed

fix(core): reject fixed-length tuple annotations for **kwargs variadic arguments#4742
hktitof wants to merge 1 commit into
openai:mainfrom
hktitof:core/fail-fast-kwargs-tuple-annotation

Conversation

@hktitof

@hktitof hktitof commented Aug 28, 2026

Copy link
Copy Markdown

Summary

A **kwargs parameter annotated with a fixed-length tuple silently loses every element constraint. function_schema() maps that annotation to dict[str, ann] unchanged, so def f(**kwargs: tuple[int, str]) produces an additionalProperties schema whose value type is the bare tuple tuple[int, str]. tuple[()] is worse: get_args(Tuple[()]) reports no args, so the value schema collapses to {type: array, minItems: 0, maxItems: 0} and forces every keyword value to be the empty array.

#4735 fixed the symmetric *args path by rejecting fixed-length tuple annotations during tool construction and naming the supported alternatives. This PR applies the same fail-fast guard to **kwargs, which is the mirror direction of the follow-up requested on #4696:

The narrower SDK behavior should be to reject fixed-length tuple annotations during tool construction and identify tuple[T, ...] or list[T] as the supported alternative. A focused follow-up PR implementing that fail-fast behavior would be welcome.

tuple[()] is included in the rejection for the same reason as in #4735: it parameterizes an empty tuple but reports no args, so the check tests whether the annotation is parameterized rather than whether get_args() is non-empty. Unparameterized tuple is unchanged, since it carries no element type to preserve or reject. This is a behavior change for callers who previously built such a tool: the annotation used to be accepted and silently widened, and now raises UserError at construction rather than producing a schema that cannot describe the call.

The supported forms keep building unchanged: **kwargs: tuple[int, ...], list[T], a scalar, and dict[str, X].

Test plan

test_var_keyword_fixed_length_tuple_annotation_is_rejected is parametrized over tuple[int, str] and tuple[()], and asserts the message names both supported alternatives. test_var_keyword_supported_annotations_still_build is parametrized over tuple[int, ...], list[T], and a plain scalar so the alternatives named in the error keep working.

Verified the rejection test fails without the source change by stashing src/agents/function_schema.py and rerunning: the guard is skipped, so the fixed annotation passes through to a late strict-schema UserError instead of failing at construction.

.agents/skills/code-change-verification/scripts/run.sh passes end to end: format, lint, typecheck and the targeted suite. pytest tests/test_function_schema.py tests/test_strict_schema.py tests/test_function_tool.py is 186 passed.

Issue number

None. Mirrors the follow-up requested by a maintainer on #4696 and implemented for *args in #4735.

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b23c6eccb7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +478 to +480
if not (len(args_of_tuple) == 2 and args_of_tuple[1] is Ellipsis) and hasattr(
ann, "__args__"
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep fixed tuples valid as individual keyword values

When strict_json_schema=False, **kwargs: tuple[int, str] is already represented correctly as dict[str, tuple[int, str]]: Pydantic emits an additionalProperties array schema with both positional constraints, validates each JSON array into a tuple, and to_call_args() passes those tuples as the keyword values. Likewise, tuple[()] intentionally requires each value to be an empty tuple. This guard therefore rejects valid, fully representable annotations rather than preventing constraint loss, breaking tool construction for callers using fixed-tuple keyword values; remove this rejection instead of requiring them to change their value type.

AGENTS.md reference: AGENTS.md:L194-L194

Useful? React with 👍 / 👎.

@hktitof

hktitof commented Aug 28, 2026

Copy link
Copy Markdown
Author

Good catch via the P2 — you are right. I verified the pre-change behavior: with strict_json_schema=False the **kwargs tuple annotation is already preserved correctly as dict[str, tuple[int, str]] (prefixItems [int,str], validated + reconstructed fine), so this guard rejects a valid, working annotation rather than fixing any loss. My *args analog in #4735 was a real flatten-to-Any bug; the **kwargs path never had that bug. Closing this as incorrect — thanks for the review.

@hktitof hktitof closed this Aug 28, 2026
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.

1 participant