Strict mode and nulls¶
OpenAI strict mode (the default for chat_tools and responses_tools) has two rules:
- Every property is listed in
required. - No object accepts extra properties.
So an optional argument cannot be left out. It becomes nullable instead, and the model
sends null to mean "not given". invoke turns that null back into your default.
Example¶
@registry.tool
def search(query: str, top_k: int = 8, region: str | None = None) -> list[str]:
"""Search articles.
Args:
query: Search text.
top_k: How many hits.
region: Optional region filter.
"""
return [f"{query} #{i} ({region})" for i in range(top_k)]
Rendered for OpenAI strict mode:
parameters = chat_tools(registry)[0]["function"]["parameters"]
print(json.dumps(parameters, indent=2))
{
"additionalProperties": false,
"properties": {
"query": {"description": "Search text.", "type": "string"},
"top_k": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "How many hits. Default: 8."
},
"region": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Optional region filter. Default: null."
}
},
"required": ["query", "top_k", "region"],
"type": "object"
}
The model sends every field. null stands for "use the default":
executor = Executor(registry) # model tool calls always go through the executor
# Strict mode sends every field; null stands for "not given".
hits = await executor.run("search", {"query": "returns", "top_k": None, "region": None})
print(len(hits)) # 8: top_k's default applied
print(hits[0]) # returns #0 (None): region's default is None, so null is kept
The rule¶
flowchart TD
N["argument arrives as null"] --> R{"required?"}
R -- yes --> K1["kept → ToolArgsInvalid<br/>(unless the type allows None)"]
R -- no --> D{"default is None?"}
D -- yes --> K2["kept → function gets None"]
D -- no --> DR["dropped → function gets the default"]
- It applies at any depth: nested models, and models inside lists.
- It follows aliases on nested models.
- To let the model pass
Noneon purpose, makeNonethe default.
Defaults in descriptions¶
Strict mode removes default from the schema, so the model would not know it. Each
argument's default is written into its description instead: Default: 8. Gemini gets
the same note.
Every property gets the note, at any depth — including fields of models inside lists.
A default placed directly on a list item type or a union branch (which pydantic
ignores) is dropped without a note.
Turning strict mode off¶
The schema is then the plain one: optional fields stay optional and keep default.
Each tool dict carries "strict": false.
This page's code is examples/strict_mode.py. The test suite runs it.