豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 008294b

Browse files
mvanhornclaude
andcommitted
feat: skip self/cls parameters in function_tool schema generation
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>
1 parent 72ad8e3 commit 008294b

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

src/agents/function_schema.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ def function_schema(
287287
takes_context = False
288288
filtered_params = []
289289

290+
# Track whether the first real (non-self/cls) parameter has been processed for context check
291+
self_or_cls_skipped = False
292+
290293
if params:
291294
first_name, first_param = params[0]
292295
# Prefer the evaluated type hint if available
@@ -297,15 +300,23 @@ def function_schema(
297300
takes_context = True # Mark that the function takes context
298301
else:
299302
filtered_params.append((first_name, first_param))
303+
elif first_name in ("self", "cls"):
304+
self_or_cls_skipped = True # Skip bound method receiver parameter
300305
else:
301306
filtered_params.append((first_name, first_param))
302307

303-
# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext.
308+
# For parameters other than the first, raise error if any use RunContextWrapper or ToolContext
309+
# (unless self/cls was skipped, in which case the second param is the effective first param).
304310
for name, param in params[1:]:
305311
ann = type_hints.get(name, param.annotation)
306312
if ann != inspect._empty:
307313
origin = get_origin(ann) or ann
308314
if origin is RunContextWrapper or origin is ToolContext:
315+
if self_or_cls_skipped and not takes_context:
316+
# self/cls was the first param, so this is the effective first param
317+
takes_context = True
318+
self_or_cls_skipped = False
319+
continue
309320
raise UserError(
310321
f"RunContextWrapper/ToolContext param found at non-first position in function"
311322
f" {func.__name__}"

tests/test_function_schema.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,3 +885,61 @@ def func_with_annotated_multiple_field_constraints(
885885

886886
with pytest.raises(ValidationError): # zero factor
887887
fs.params_pydantic_model(**{"score": 50, "factor": 0.0})
888+
889+
890+
def test_method_self_param_skipped():
891+
"""Test that self parameter is skipped for class methods."""
892+
893+
class MyTools:
894+
def greet(self, name: str) -> str:
895+
return f"Hello, {name}"
896+
897+
obj = MyTools()
898+
fs = function_schema(obj.greet, use_docstring_info=False)
899+
props = fs.params_json_schema.get("properties", {})
900+
assert "self" not in props
901+
assert "name" in props
902+
assert fs.params_json_schema.get("required") == ["name"]
903+
904+
905+
def test_classmethod_cls_param_skipped():
906+
"""Test that cls parameter is skipped for classmethods passed as unbound."""
907+
908+
# Simulate a function whose first param is named cls with no annotation
909+
code = compile("def greet(cls, name: str) -> str: ...", "<test>", "exec")
910+
ns: dict[str, Any] = {}
911+
exec(code, ns) # noqa: S102
912+
fn = ns["greet"]
913+
fn.__annotations__ = {"name": str, "return": str}
914+
915+
fs = function_schema(fn, use_docstring_info=False)
916+
props = fs.params_json_schema.get("properties", {})
917+
assert "cls" not in props
918+
assert "name" in props
919+
920+
921+
def test_method_self_with_context_second_param():
922+
"""Test that self is skipped and RunContextWrapper as second param is recognized."""
923+
924+
class MyTools:
925+
def greet(self, ctx: RunContextWrapper[None], name: str) -> str:
926+
return f"Hello, {name}"
927+
928+
obj = MyTools()
929+
fs = function_schema(obj.greet, use_docstring_info=False)
930+
props = fs.params_json_schema.get("properties", {})
931+
assert "self" not in props
932+
assert "ctx" not in props
933+
assert "name" in props
934+
assert fs.takes_context is True
935+
936+
937+
def test_regular_unannotated_first_param_still_included():
938+
"""Test that a regular unannotated first param (not self/cls) is still included."""
939+
940+
def process(data, flag: bool = False) -> str:
941+
return str(data)
942+
943+
fs = function_schema(process, use_docstring_info=False)
944+
props = fs.params_json_schema.get("properties", {})
945+
assert "data" in props

0 commit comments

Comments
 (0)