A minimal, fast, and type-safe Python library for LLM chat completions with OpenAI and Azure OpenAI support
Project description
llmify
A lightweight, type-safe Python library for LLM chat completions.
Features:
- Simple, intuitive API for OpenAI, Azure OpenAI, and Anthropic
- Type-safe structured outputs with Pydantic
- Built-in tool calling support
- Async streaming
- Image analysis support
- Minimal dependencies, maximum flexibility
Installation
pip install py-llmify
Install only the provider you need:
pip install py-llmify[openai] # OpenAI + Azure OpenAI
pip install py-llmify[anthropic] # Anthropic (Claude)
pip install py-llmify[all] # All providers
Quick Start
import asyncio
from llmify import ChatOpenAI, UserMessage, SystemMessage
async def main():
llm = ChatOpenAI(model="gpt-4o")
response = await llm.invoke([
SystemMessage(content="You are a helpful assistant"),
UserMessage(content="What is 2+2?")
])
print(response.completion) # "2+2 equals 4"
asyncio.run(main())
All invoke calls return a ChatInvokeCompletion[T] with:
completion— the text (or parsed Pydantic model) returned by the modeltool_calls— list ofToolCallobjects, if anyusage— token usage (ChatInvokeUsage)stop_reason— why the model stopped
Core Features
Message Types
from llmify import SystemMessage, UserMessage, AssistantMessage, ToolResultMessage
messages = [
SystemMessage(content="You are a Python expert"),
UserMessage(content="How do I read a file?"),
AssistantMessage(content="You can use open() with a context manager"),
UserMessage(content="Show me an example"),
]
Image messages
Pass images inline inside a UserMessage using content parts:
from llmify import UserMessage, ContentPartTextParam, ContentPartImageParam, ImageURL
message = UserMessage(
content=[
ContentPartTextParam(text="What's in this image?"),
ContentPartImageParam(
image_url=ImageURL(
url="data:image/jpeg;base64,<base64data>",
media_type="image/jpeg",
detail="high",
)
),
]
)
Structured Outputs
Pass output_format to get a validated Pydantic model back:
from pydantic import BaseModel
from llmify import ChatOpenAI, UserMessage
class Person(BaseModel):
name: str
age: int
occupation: str
async def main():
llm = ChatOpenAI(model="gpt-4o")
response = await llm.invoke(
[UserMessage(content="Extract: John is 32 and works as a data scientist")],
output_format=Person,
)
person = response.completion # type: Person
print(f"{person.name}, {person.age}, {person.occupation}")
# John, 32, data scientist
asyncio.run(main())
Tool Calling
@tool decorator
Define tools from plain Python functions:
import json
from llmify import ChatOpenAI, UserMessage, AssistantMessage, ToolResultMessage, tool
@tool
def get_weather(location: str, unit: str = "celsius") -> str:
"""Get current weather for a location"""
return f"Weather in {location}: 22°{unit[0].upper()}, Sunny"
async def main():
llm = ChatOpenAI(model="gpt-4o")
messages = [UserMessage(content="What's the weather in Paris?")]
response = await llm.invoke(messages, tools=[get_weather])
if response.tool_calls:
tc = response.tool_calls[0]
args = json.loads(tc.function.arguments)
result = get_weather(**args)
messages.append(AssistantMessage(content=response.completion, tool_calls=response.tool_calls))
messages.append(ToolResultMessage(tool_call_id=tc.id, content=result))
final = await llm.invoke(messages)
print(final.completion)
asyncio.run(main())
RawSchemaTool
Use a raw JSON schema when you need full control over the tool definition:
import json
from llmify import ChatOpenAI, UserMessage, AssistantMessage, ToolResultMessage, RawSchemaTool
search_tool = RawSchemaTool(
name="search_web",
description="Search the web for information",
schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"max_results": {"type": "integer", "default": 5},
},
"required": ["query"],
},
)
async def main():
llm = ChatOpenAI(model="gpt-4o-mini")
messages = [UserMessage(content="Search for Python 3.13 features")]
response = await llm.invoke(messages, tools=[search_tool])
if response.tool_calls:
tc = response.tool_calls[0]
args = json.loads(tc.function.arguments)
result = my_search_fn(**args)
messages.append(AssistantMessage(content=response.completion, tool_calls=response.tool_calls))
messages.append(ToolResultMessage(tool_call_id=tc.id, content=result))
final = await llm.invoke(messages)
print(final.completion)
asyncio.run(main())
Dict schema
Pass raw OpenAI-style tool dicts directly:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
},
"required": ["city"],
},
},
}
]
response = await llm.invoke(messages, tools=tools)
print(response.tool_calls[0].function.name)
print(json.loads(response.tool_calls[0].function.arguments))
Streaming
async def main():
llm = ChatOpenAI(model="gpt-4o")
async for chunk in llm.stream([UserMessage(content="Write a haiku about Python")]):
print(chunk, end="", flush=True)
asyncio.run(main())
Configuration
Environment Variables
# OpenAI
export OPENAI_API_KEY="sk-..."
# Azure OpenAI
export AZURE_OPENAI_API_KEY="..."
export AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com/"
# Anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
Model Parameters
Set defaults when initializing or override per request:
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.7,
max_tokens=1000,
)
response = await llm.invoke(
messages=[UserMessage(content="Hi")],
temperature=0.2,
max_tokens=500,
)
Supported parameters: temperature, max_tokens, top_p, frequency_penalty, presence_penalty, stop, seed.
Providers
OpenAI
from llmify import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o",
api_key="sk-...", # optional if OPENAI_API_KEY is set
)
Azure OpenAI
from llmify import ChatAzureOpenAI
llm = ChatAzureOpenAI(
model="gpt-4o",
api_key="...", # optional if AZURE_OPENAI_API_KEY is set
azure_endpoint="https://<resource>.openai.azure.com/", # optional if env var is set
)
Anthropic
from llmify import ChatAnthropic
llm = ChatAnthropic(
model="claude-sonnet-4-20250514",
api_key="sk-ant-...", # optional if ANTHROPIC_API_KEY is set
)
The Anthropic provider supports the same API surface — invoke, stream, structured output, and tool calling — all mapped to the Anthropic messages API under the hood.
Design Philosophy
Thin wrapper around official SDKs with minimal dependencies and no unnecessary abstractions. Full type hints throughout, Pydantic for all messages and responses, async-first.
Credits
Inspired by LangChain and browser-use.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_llmify-0.3.0.tar.gz.
File metadata
- Download URL: py_llmify-0.3.0.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d7519f9d62b024d79221ddb50fd8b00b94a8caf396765241bc0a786af12bb12
|
|
| MD5 |
ddbb4bf836281024dac0590f976b7ac6
|
|
| BLAKE2b-256 |
be70a26ded07e4ef5ad2f03158f56ebb0a427a8ebcad19cc2b078542b70a51fd
|
File details
Details for the file py_llmify-0.3.0-py3-none-any.whl.
File metadata
- Download URL: py_llmify-0.3.0-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25323aef9318a951be575459c93871a18b12f2cea6b0334edc2873af7af271d6
|
|
| MD5 |
9e66b725b1731bf7a65bca846418b602
|
|
| BLAKE2b-256 |
a35ed5911e34058ce37f43d409f007cb89b753c6b0318617a7a30912dc30b97e
|