LLM Pydantic Tools
Turn Pydantic models into function-calling tools. llm-pydantic-tools converts a model, or a JSON schema, into the tool format of the main LLM APIs (OpenAI Chat Completions and Responses, Anthropic and Gemini), builds the matching tool_choice value, and validates the arguments the model sends back.
Features
- Model to tool: converts a Pydantic model, or a JSON schema, into a function tool for OpenAI (Chat Completions or Responses), Anthropic or Gemini. The model's docstring becomes the tool description, unless you pass
description. - Lean schemas: strips the
titlethat Pydantic adds to every schema, without touching fields or values that are also calledtitle. - Valid tool names: names the tool after the model, or after
tool_name, and checks the name against the API rules: 1 to 64 ASCII letters, digits, underscores or dashes. tool_choicevalues:auto,required,none, or forcing this tool, in the format of each API.- Answer validation: checks the arguments the model returns against the tool's schema.
- Typed parsing: turns the arguments into an instance of your model, running its Pydantic validators.
Installation
uv add llm-pydantic-tools
Or with pip:
pip install llm-pydantic-tools
Usage
Define a tool
from typing import Literal
from pydantic import BaseModel, Field
from llm_pydantic_tools import ToolChoiceEnum, ToolSchemaManager, get_tool_choice_dict
class GetWeather(BaseModel):
"""Get the current weather in a city."""
city: str = Field(description="City name, e.g. Montevideo")
unit: Literal["celsius", "fahrenheit"] = "celsius"
weather_tool = ToolSchemaManager(pydantic_obj=GetWeather, tool_name="get_weather")
Without tool_name, the tool is named after the model (GetWeather).
weather_tool.tools_schema holds the tool, ready to send:
[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a city.",
"parameters": {
"properties": {
"city": {
"description": "City name, e.g. Montevideo",
"type": "string"
},
"unit": {
"default": "celsius",
"enum": [
"celsius",
"fahrenheit"
],
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
}
}
]
Call the API
Pass tools_schema as tools, and the result of get_tool_choice_dict() as tool_choice. With the OpenAI SDK:
from openai import OpenAI
client = OpenAI()
completion = client.chat.completions.create(
model="gpt-6-astra",
messages=[{"role": "user", "content": "What's the weather in Montevideo?"}],
tools=weather_tool.tools_schema,
tool_choice=get_tool_choice_dict(ToolChoiceEnum.TOOL_NAME, weather_tool),
)
tool_call = completion.choices[0].message.tool_calls[0]
ToolChoiceEnum.TOOL_NAME forces the model to call this tool. AUTO, REQUIRED and NONE map to "auto", "required" and "none".
Validate the answer
The model does not always follow the schema, so check the arguments before using them. validate() raises a jsonschema.ValidationError that describes the problem:
import json
from jsonschema import ValidationError
arguments = json.loads(tool_call.function.arguments)
try:
weather_tool.validate(arguments)
except ValidationError as error:
print(error.message) # e.g. 'kelvin' is not one of ['celsius', 'fahrenheit']
If you only need a yes or no, weather_tool.is_valid(arguments) returns True or False.
validate_tool_answer(), from earlier versions, still returns True or the error instead of raising it. Compare its result with is True: the error counts as true in an if.
Get an instance of the model
validate() checks the arguments against the JSON schema, so it also works when you start from a schema. If you built the tool from a model, parse() goes further: it runs Pydantic, with your validators and type conversions, and returns an instance of the model. It takes the arguments as a JSON string or already parsed:
weather = weather_tool.parse(tool_call.function.arguments) # a GetWeather
print(weather.city)
If the arguments don't fit the model, it raises a pydantic.ValidationError.
Start from a JSON schema
If you already have the JSON schema, pass it instead of the model. The tool is named after the schema's title; if it has none, pass tool_name:
schema_manager = ToolSchemaManager(
pydantic_obj_json_schema=my_json_schema,
tool_name="my_tool",
)
Other formats
tools_schema holds the tool in the Chat Completions format. tool() returns it in the format of another API, and format_tools() builds the tools list of a request from several managers. get_tool_choice_dict() takes the same format as its third argument:
ToolFormat |
API | Pass get_tool_choice_dict() as |
|---|---|---|
CHAT_COMPLETIONS (default) |
OpenAI Chat Completions, and compatible APIs such as Mistral's and Ollama's | tool_choice |
RESPONSES |
OpenAI Responses API | tool_choice |
ANTHROPIC |
Anthropic's Messages API (Claude) | tool_choice |
GEMINI |
Gemini's generate_content |
tool_config |
GEMINI_INTERACTIONS |
Gemini's Interactions API | generation_config["tool_choice"] |
The tools always go in tools. For Gemini's generate_content, format_tools() groups them in a single function_declarations list, as that API expects, and in every format it rejects two tools with the same name.
With Anthropic's SDK:
import anthropic
from llm_pydantic_tools import ToolFormat
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
messages=[{"role": "user", "content": "What's the weather in Montevideo?"}],
tools=[weather_tool.tool(ToolFormat.ANTHROPIC)],
tool_choice=get_tool_choice_dict(ToolChoiceEnum.AUTO, weather_tool, ToolFormat.ANTHROPIC),
)
for block in message.content:
if block.type == "tool_use":
weather_tool.validate(block.input)
where weather_tool.tool(ToolFormat.ANTHROPIC) is:
{
"name": "get_weather",
"description": "Get the current weather in a city.",
"input_schema": {
"properties": {
"city": {
"description": "City name, e.g. Montevideo",
"type": "string"
},
"unit": {
"default": "celsius",
"enum": [
"celsius",
"fahrenheit"
],
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
}
With Gemini's generate_content:
from google import genai
from google.genai import types
from llm_pydantic_tools import format_tools
client = genai.Client()
response = client.models.generate_content(
model="gemini-flash-latest",
contents="What's the weather in Montevideo?",
config=types.GenerateContentConfig(
tools=format_tools([weather_tool], ToolFormat.GEMINI),
tool_config=get_tool_choice_dict(ToolChoiceEnum.TOOL_NAME, weather_tool, ToolFormat.GEMINI),
),
)
weather_tool.validate(response.function_calls[0].args)
Compatibility
The tools are plain dicts, so they work with each provider's SDK or with raw HTTP requests. Keep in mind:
- The schemas are not prepared for strict mode (
additionalProperties: falseon every object and every field inrequired). If you use the OpenAI Python SDK and need strict mode,openai.pydantic_function_tool()covers that case. - The newest Claude models (Opus 5.5, Sonnet 5.5 and Fable 5.1) reject forcing a tool, so
REQUIREDandTOOL_NAMEfail there. UseAUTOand ask for the tool in the prompt. - Gemini asks for a description of every function, and supports a subset of JSON Schema.
- Tool names follow the strictest rule of these APIs (Chat Completions'), so a valid name works with all of them.
llm-pydantic-tools only depends on Pydantic and jsonschema, which makes it handy when you build the requests yourself or switch between providers.
Contributing
Contributions are welcome! Please feel free to submit pull requests, create issues, and suggest improvements to the repository.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Metadata
Release files for llm-pydantic-tools 0.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| llm_pydantic_tools-0.4.tar.gz | 17.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llm_pydantic_tools-0.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.6 kB
Release files / llm_pydantic_tools-0.4.tar.gz
| Download URL | llm_pydantic_tools-0.4.tar.gz |
|---|---|
| Size | 17.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
67c00edd01bdd6d2f766431f29e0e431c62d30c3c92301e9463e7a8b16642efd
|
|
BLAKE2b-256 checksum How to use checksums |
ab03456a9634c42a9774bd554d1e6c7c8fc3f3692bd0c9e2019319c9aaeee56c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / llm_pydantic_tools-0.4-py3-none-any.whl
| Download URL | llm_pydantic_tools-0.4-py3-none-any.whl |
|---|---|
| Size | 10.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3a21509a958a638fd9e985d49c78ce65d6d8bad33ea6947a54db0a56d57ffb71
|
|
BLAKE2b-256 checksum How to use checksums |
9658ef31b8f0316e1e876e6955293929407d19324ac26f314f79df27745b024c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|