pi-llm
Unified LLM provider abstraction for Python. Provides streaming and non-streaming interfaces to LLM APIs with automatic model discovery, token and cost tracking, and tool call support.
Features
- Streaming & non-streaming —
stream_simple()/complete_simple()with async iteration - Model catalog —
get_model()with built-in pricing and metadata for OpenAI models - Tool calling — Define tools with JSON Schema, validate arguments automatically
- Extended thinking — Unified reasoning level interface across providers
- Cost tracking — Automatic token counting and dollar cost calculation
- Event-driven — Fine-grained streaming events (text, thinking, tool calls)
- Cross-provider types — Pydantic models with camelCase/snake_case interop
Supported Providers
- OpenAI (Responses API)
More providers coming soon.
Installation
pip install pi-llm
Quick Start
import asyncio
from pi_llm import get_model, stream_simple, complete_simple, Context, UserMessage
from pi_llm import register_builtin_providers, SimpleStreamOptions
# Register providers (call once at startup)
register_builtin_providers()
model = get_model("openai", "gpt-4o")
context = Context(
system_prompt="You are a helpful assistant.",
messages=[UserMessage(content="What is Python?", timestamp=0)],
)
# Option 1: Non-streaming
async def main():
message = await complete_simple(
model, context,
SimpleStreamOptions(api_key="sk-...")
)
for block in message.content:
if hasattr(block, "text"):
print(block.text)
asyncio.run(main())
# Option 2: Streaming
async def main_stream():
event_stream = stream_simple(
model, context,
SimpleStreamOptions(api_key="sk-...")
)
async for event in event_stream:
if event.type == "text_delta":
print(event.delta, end="", flush=True)
asyncio.run(main_stream())
Tools
Define tools with JSON Schema and handle tool calls in a loop:
from pi_llm import Tool, ToolCall, ToolResultMessage, TextContent
import time
weather_tool = Tool(
name="get_weather",
description="Get current weather for a city",
parameters={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
)
context = Context(
system_prompt="You can check the weather.",
messages=[UserMessage(content="What's the weather in Tokyo?", timestamp=0)],
tools=[weather_tool],
)
async def tool_loop():
while True:
message = await complete_simple(
model, context,
SimpleStreamOptions(api_key="sk-...")
)
context.messages.append(message)
# Check for tool calls
tool_calls = [c for c in message.content if isinstance(c, ToolCall)]
if not tool_calls:
break # No more tool calls, done
# Execute tool calls and add results
for tc in tool_calls:
result = f"25°C and sunny in {tc.arguments['city']}"
context.messages.append(ToolResultMessage(
tool_call_id=tc.id,
tool_name=tc.name,
content=[TextContent(text=result)],
timestamp=int(time.time() * 1000),
))
# Print final response
for block in message.content:
if hasattr(block, "text"):
print(block.text)
Streaming Events
The event stream emits these events in order:
| Event | Description |
|---|---|
StartEvent |
Streaming begins |
TextStartEvent |
Text block begins |
TextDeltaEvent |
Incremental text chunk |
TextEndEvent |
Text block complete |
ThinkingStartEvent |
Reasoning block begins |
ThinkingDeltaEvent |
Reasoning chunk |
ThinkingEndEvent |
Reasoning block complete |
ToolCallStartEvent |
Tool call begins |
ToolCallDeltaEvent |
Tool call argument chunk |
ToolCallEndEvent |
Tool call complete |
DoneEvent |
Streaming finished successfully |
ErrorEvent |
Streaming ended with error |
Extended Thinking
Enable reasoning for supported models:
message = await complete_simple(
model, context,
SimpleStreamOptions(
api_key="sk-...",
reasoning="medium", # minimal, low, medium, high, xhigh
)
)
License
MIT
Metadata
Release files for pi-llm 0.1.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 | Provenance | Actions |
|---|---|---|---|---|
| pi_llm-0.1.4.tar.gz | 46.4 kB | Without provenance | Details for pi_llm-0.1.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pi_llm-0.1.4-py3-none-any.whl | Python 3 | none | any | Details for pi_llm-0.1.4-py3-none-any.whl |
Total release size: 83.0 kB
Release files / pi_llm-0.1.4.tar.gz
| Download URL | pi_llm-0.1.4.tar.gz |
|---|---|
| Size | 46.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5bbef3018016196488e4f40cf74c57426c24e5600d23639dae780f49859dc0bd
|
|
BLAKE2b-256 checksum How to use checksums |
ce6e8e6f445ce0a96b38533c35f433e7ad0804eb559eafe59fea5e633f9294f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / pi_llm-0.1.4-py3-none-any.whl
| Download URL | pi_llm-0.1.4-py3-none-any.whl |
|---|---|
| Size | 36.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e1d7828dd2e446b6ae6f734f91fdfb4322cba9b943f48bc2c6bc2bc25a06904f
|
|
BLAKE2b-256 checksum How to use checksums |
475c10bf75aa07a088c9c86c488f3c245de9237fac5bbfb6c278e3edc865f95d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Security for pi-llm 0.1.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.