Tool parameter named "title" is stripped from input_schema but kept in required
A tool function with a parameter named title has that parameter silently dropped from its generated input_schema, while required still lists it. The model therefore can never supply the argument, and the call fails with TypeError: ... missing 1 required positional argument: 'title'.
Reproduction
import llm
def demo_a(title: str, when: str) -> str:
"param named title"
return ""
def demo_b(heading: str, when: str) -> str:
"same thing, renamed"
return ""
for f in (demo_a, demo_b):
s = llm.Tool.function(f).input_schema
print(f.__name__, "-> properties:", list(s["properties"].keys()), "| required:", s["required"])demo_a -> properties: ['when'] | required: ['title', 'when']
demo_b -> properties: ['heading', 'when'] | required: ['heading', 'when']Note the inconsistency in demo_a: required names title, but properties has no such key.
Cause
_get_arguments_input_schema() (llm/models.py:260) builds a Pydantic model that correctly includes title as a field. Tool.__post_init__ then passes it through _ensure_dict_schema() (llm/models.py:3640), which calls:
def _remove_titles_recursively(obj):
"""Recursively remove all 'title' fields from a nested dictionary."""
if isinstance(obj, dict):
obj.pop("title", None)
for value in obj.values():
_remove_titles_recursively(value)
elif isinstance(obj, list):
for item in obj:
_remove_titles_recursively(item)The intent is to strip Pydantic's auto-generated "title": "When" annotations. But the recursion descends into properties, where title is a property name rather than a schema keyword, so the whole property is deleted. required is a list of plain strings, so the recursion never touches it — hence the two ending up out of sync.
Impact
This is easy to miss, because llm invokes tools as func(**args) with no filtering against the schema. If a model supplies title anyway — mine did for a long time, because the tool description happened to say "You MUST include title" — the call succeeds and nothing looks wrong.
It surfaced for me on gpt-5.6-luna, which strictly follows the declared schema and so won't emit an argument that isn't in properties:
Tool call: create_event({'when': '2026-11-07 20:00', 'location': '...', 'duration': 60, 'calendar': 'Personal', 'timezone': 'America/Los_Angeles'})
Error: create_event() missing 1 required positional argument: 'title'
Exception: create_event() missing 1 required positional argument: 'title'The same tool had been working for months on other models that were willing to go off-schema.
I reproduced it on 0.26 and 0.35. 0.26 is the first release with llm.Tool, so it appears to have been there since tool support landed.
Suggested fix
Only strip title where it is a schema annotation, not where it is a property name — e.g. skip descending by key into properties maps, or drop title only when the containing dict also has type/anyOf.
Environment
- llm 0.35, Python 3.13, macOS
- Observed with
gpt-5.6-luna; previously masked on other models
Source: simonw/llm