Skip to content

feat: support self/cls in function_tool for class methods#2734

Draft
mvanhorn wants to merge 6 commits intoopenai:mainfrom
mvanhorn:feat/method-tool-self-cls-support
Draft

feat: support self/cls in function_tool for class methods#2734
mvanhorn wants to merge 6 commits intoopenai:mainfrom
mvanhorn:feat/method-tool-self-cls-support

Conversation

@mvanhorn
Copy link

Summary

When a class method decorated with @function_tool is used as a tool, the self (or cls) parameter leaks into the generated JSON schema. The OpenAI API rejects this with a 400 error because self has no type key. This PR skips the first parameter when it has no type annotation and is named self or cls, and also handles the case where RunContextWrapper/ToolContext follows self/cls as the second parameter.

Test plan

  • Added test_method_self_param_skipped - verifies self is excluded from schema for bound methods
  • Added test_classmethod_cls_param_skipped - verifies cls is excluded for classmethod-like functions
  • Added test_method_self_with_context_second_param - verifies self skip + RunContextWrapper as second param works
  • Added test_regular_unannotated_first_param_still_included - verifies non-self/cls unannotated params still work
  • All existing tests pass (make tests - 2405 passed)

Issue number

Closes #94

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run make lint and make format
  • I've made sure tests pass

This contribution was developed with AI assistance (Claude Code).

When a class method decorated with @function_tool is used as a tool,
the self/cls parameter was included in the generated JSON schema,
causing OpenAI API 400 errors. This skips the first parameter when
it has no type annotation and is named "self" or "cls". Also handles
the case where RunContextWrapper/ToolContext follows self/cls as the
second parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

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

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: 008294bc56

ℹ️ 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".

Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

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

If you're willing to take more time, please continue working until Codex auto review gives 👍 to your PR. Then, we'll check the direction and details.

The self_or_cls_skipped flag persisted across all remaining params,
so `def f(self, x: int, ctx: RunContextWrapper)` incorrectly passed
validation. Now only the param at index 1 (immediately after self/cls)
is treated as the effective first param.

Adds a test for this edge case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mvanhorn
Copy link
Author

Addressed Codex finding in 73b9a9f: context params at non-adjacent positions after self/cls now correctly raise UserError. Added edge case test.

@codex review

Copy link

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

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: 73b9a9f677

ℹ️ 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".

When self/cls was skipped during schema generation, the original
signature (including self/cls) was stored in FuncSchema. This caused
to_call_args to misalign parameter indices - skipping self instead
of the context param. Now we rebuild the signature without self/cls
so to_call_args iterates only the parameters the caller passes.
@mvanhorn
Copy link
Author

@codex review

Copy link

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

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: 2e85520886

ℹ️ 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".

Use a permanent receiver_was_skipped flag instead of checking
params[0] name at call_sig construction time. Prevents stripping
a self/cls parameter that is actually the context argument
(e.g. def tool(self: RunContextWrapper[Any], q: str)).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mvanhorn
Copy link
Author

@codex review

Copy link

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

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: 22f1f83d08

ℹ️ 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 +307 to +309
elif first_name in ("self", "cls"):
self_or_cls_skipped = True # Skip bound method receiver parameter
receiver_was_skipped = True

Choose a reason for hiding this comment

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

P1 Badge Bind receiver or reject skipped self/cls tools

Skipping a leading unannotated self/cls here makes unbound methods look supported, but the invocation path still calls the original callable without injecting an instance/class receiver (it only prepends tool context when takes_context is true). In scenarios like function_tool(MyTools.greet) or class-defined methods decorated with @function_tool, schema creation now succeeds but execution fails with a missing-argument TypeError at runtime because the receiver was removed from argument construction.

Useful? React with 👍 / 👎.

Comment on lines +432 to +433
remaining = [p for name, p in params if name not in ("self", "cls")]
call_sig = sig.replace(parameters=remaining)

Choose a reason for hiding this comment

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

P2 Badge Drop only the leading receiver from call signature

This filter removes every parameter named self or cls, not just the first receiver that was skipped. For valid signatures like def f(self, cls: int, value: str), the generated schema still includes cls, but to_call_args iterates a signature where cls was deleted, so the provided value is ignored and argument mapping becomes incorrect. The replacement signature should remove only the single leading receiver parameter.

Useful? React with 👍 / 👎.

@seratch seratch marked this pull request as draft March 21, 2026 12:07
Codex review found that skipping self/cls from schema generation makes
unbound methods appear supported, but execution fails with TypeError
because the receiver is never injected. Bound methods (instance.method)
already work - Python strips self before inspect.signature sees it.

- Raise UserError for unbound self/cls instead of silent schema-only skip
- Remove dead receiver_was_skipped logic and call_sig stripping (P2 fix)
- Clean up self_or_cls_skipped variable (now unreachable)
- Update tests to expect UserError for unbound methods
@mvanhorn
Copy link
Author

@codex review

@mvanhorn
Copy link
Author

Addressed both Codex findings in 98ba075:

P1 (bind receiver): Unbound methods now raise UserError instead of silently creating a broken schema. Bound methods (instance.method) already work - Python strips self before inspect.signature sees it, so this code path only triggered for unbound calls like MyClass.method where execution would fail.

P2 (drop only leading receiver): Removed entirely - the filter is now dead code since we reject instead of skip.

Net effect: -26 lines. Simpler code, no silent runtime failures.

Copy link

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

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: 98ba075e1c

ℹ️ 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 +300 to +304
elif first_name in ("self", "cls"):
# An unannotated self/cls means this is an unbound method or classmethod.
# Bound methods (e.g., instance.method) already have self stripped by Python.
raise UserError(
f"Function {func.__name__} has an unbound '{first_name}' parameter. "

Choose a reason for hiding this comment

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

P1 Badge Avoid rejecting method decorators that pass unbound functions

This branch now throws UserError for any unannotated first parameter named self/cls, but @function_tool is applied to unbound functions during class definition, so class T: @function_tool def f(self, x: str) now fails immediately. Because function_tool always calls function_schema(the_func, ...) on that unbound callable, this change blocks the common decorator workflow for method-based tools instead of allowing them to be registered and later invoked as bound methods.

Useful? React with 👍 / 👎.

The UserError for unannotated self/cls blocked @function_tool usage on
class methods during decoration time. Now silently skips self/cls and
stores a self_or_cls_skipped flag. The stored signature excludes self/cls
so to_call_args produces correctly aligned arguments. Context params
immediately after self/cls are recognized as effective-first position.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature:core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add method_tool Functionality

2 participants