Universal LM core with pluggable provider adapters
Project description
lm15
One interface for OpenAI, Anthropic, and Gemini. Zero dependencies.
| lm15 | google-genai | litellm | |
|---|---|---|---|
| install | 72ms | 137ms | 184ms |
| import | 95ms | 2,656ms | 4,534ms |
| total (install → response) | 1,090ms | 3,992ms | 5,840ms |
| dependencies | 0 | 25 | 55 |
| disk footprint | 408K | 41M | 155M |
Median of 10 cold-start runs. Fresh venv, single completion against gemini-3.1-flash-lite-preview. Benchmark source.
import lm15
resp = lm15.complete("claude-sonnet-4-5", "Hello.")
print(resp.text)
Switch models by changing the string. Same types, same streaming, same tool calling. That's it.
Yes, we know.
Install
pip install lm15
Set at least one provider key:
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export GEMINI_API_KEY=... # or GOOGLE_API_KEY
Usage
Streaming
for text in lm15.stream("gpt-4.1-mini", "Write a haiku.").text:
print(text, end="")
Full event access:
for event in lm15.stream("gpt-4.1-mini", "Write a haiku."):
match event.type:
case "text": print(event.text, end="")
case "thinking": print(f"💭 {event.text}", end="")
case "finished": print(f"\n📊 {event.response.usage}")
Tools (auto-execute)
Pass Python functions — schema is inferred, execution is automatic:
def get_weather(city: str) -> str:
"""Get weather by city."""
return f"22°C in {city}"
resp = lm15.complete("gpt-4.1-mini", "Weather in Montreal?", tools=[get_weather])
print(resp.text) # "It's 22°C in Montreal."
Tools (manual)
from lm15 import Tool
weather = Tool(name="get_weather", description="Get weather", parameters={...})
gpt = lm15.model("gpt-4.1-mini")
resp = gpt("Weather in Montreal?", tools=[weather])
results = {tc.id: "22°C, sunny" for tc in resp.tool_calls}
resp = gpt.submit_tools(results)
print(resp.text)
Images, audio, video, documents
from lm15 import Part
# Image from URL
resp = lm15.complete("gemini-2.5-flash", ["Describe this.", Part.image(url="https://example.com/cat.jpg")])
# Image generation → vision (cross-model)
resp = lm15.complete("gpt-4.1-mini", "Draw a cat.", output="image")
resp2 = lm15.complete("claude-sonnet-4-5", ["What's this?", resp.image])
# Document
resp = lm15.complete("claude-sonnet-4-5", ["Summarize.", Part.document(url="https://example.com/paper.pdf")])
# Upload via provider file API
doc = lm15.upload("claude-sonnet-4-5", "contract.pdf")
resp = lm15.complete("claude-sonnet-4-5", ["Find liability clauses.", doc])
Reasoning
resp = lm15.complete("claude-sonnet-4-5", "Prove √2 is irrational.", reasoning=True)
print(resp.thinking) # chain of thought
print(resp.text) # final answer
Conversation
gpt = lm15.model("gpt-4.1-mini", system="You remember everything.")
gpt("My name is Max.")
gpt("I like chess.")
resp = gpt("What do you know about me?")
print(resp.text) # knows both
Prompt caching
Reduces cost and latency for repeated prefixes — system prompts, long documents, agent loops:
agent = lm15.model("claude-sonnet-4-5",
system="<long system prompt>",
tools=[read_file, write_file],
prompt_caching=True,
)
resp = agent("Add tests for auth.")
while resp.finish_reason == "tool_call":
results = execute(resp.tool_calls)
resp = agent.submit_tools(results)
print(f"Cache hit: {resp.usage.cache_read_tokens} tokens")
Prefill
resp = lm15.complete("claude-sonnet-4-5", "Output JSON for a person.", prefill="{")
Reusable model with config
gpt = lm15.model("gpt-4.1-mini", system="You are terse.", retries=3, cache=True, temperature=0)
resp = gpt("Hello.")
# Override per call
resp = gpt("Be creative.", temperature=1.5)
# Derive new models
claude = gpt.with_model("claude-sonnet-4-5")
Config from dicts
config = {"model": "gpt-4.1-mini", "system": "You are terse.", "temperature": 0}
resp = lm15.complete(prompt="Summarize DNA.", **config)
Built-in tools
resp = lm15.complete("gpt-4.1-mini", "Latest AI news", tools=["web_search"])
for c in resp.citations:
print(c.title, c.url)
Provider support
| Capability | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| complete | ✅ | ✅ | ✅ |
| stream | ✅ | ✅ | ✅ |
| embeddings | ✅ | — | ✅ |
| files | ✅ | ✅ | ✅ |
| batches | ✅ | ✅ | ✅ |
| images | ✅ | — | ✅ |
| audio | ✅ | — | ✅ |
| prompt caching | auto | ✅ | ✅ |
Architecture
lm15.complete / lm15.model ← v2 surface (sugar)
│
▼
LMRequest ──▶ UniversalLM ──▶ MiddlewarePipeline ──▶ ProviderAdapter ──▶ Transport
│ │
│ resolve_provider(model) │ build_request / parse_response
▼ ▼
capabilities.py providers/{openai,anthropic,gemini}.py
The v2 surface (lm15.complete, lm15.model, Model, Stream) is a thin layer that constructs LMRequest objects and calls UniversalLM. The universal provider contract is unchanged — third parties can build their own surface on top of the same internals.
Why this exists
- Stdlib only. No
requests, nohttpx, noaiohttp. Transport isurllibor optionalpycurl. - Frozen dataclasses all the way down.
LMRequestin,LMResponseout. No mutable builder chains. - Nothing is hidden. Every internal type is importable. Provider escape hatches are always there.
- Plugin discovery via entry points. Third-party providers install and register without touching lm15 core.
Docs
| Topic | Path |
|---|---|
| API v2 spec | docs/API_SPEC_V2.md |
| Getting started | docs/GETTING_STARTED.md |
| Core concepts | docs/CONCEPTS.md |
| Architecture | docs/ARCHITECTURE.md |
| Provider contract | docs/CONTRACT.md |
| Error handling | docs/ERRORS.md |
| Streaming | docs/STREAMING.md |
| Writing an adapter | docs/ADAPTER_GUIDE.md |
| Adding a provider | docs/ADD_PROVIDER_GUIDE.md |
| Completeness testing | docs/COMPLETENESS.md |
| Production checklist | docs/PRODUCTION_CHECKLIST.md |
Cookbooks v2: docs/COOKBOOKS_V2/ — 10 progressive examples:
- Hello World
- Streaming
- Tools (auto-execute)
- Tools (manual loop)
- Multimodal
- Reasoning
- Conversation
- Prompt caching
- Model config
- Building an agent
Cookbooks v1 (low-level): docs/COOKBOOKS/ — 8 examples using the internal LMRequest/UniversalLM API directly.
License
MIT
Project details
Release history Release notifications | RSS feed
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 lm15-0.2.0.tar.gz.
File metadata
- Download URL: lm15-0.2.0.tar.gz
- Upload date:
- Size: 76.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7bc360f5a776165736aa8e3c78e975e2d2b6f7f9f7b643bae07f70342fa02c1
|
|
| MD5 |
b9303284c08125f96976da9807fbe55c
|
|
| BLAKE2b-256 |
d69744c9d83773e747ffaa117040c35253a58db1e7f03baa1d778498dc15498c
|
File details
Details for the file lm15-0.2.0-py3-none-any.whl.
File metadata
- Download URL: lm15-0.2.0-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f261b51f85a2c57f9f98ae86fe654ec0887028ea5ccbc853dcfea6d06be3774
|
|
| MD5 |
1c48be2b17940021888f0f90c9f2a9fa
|
|
| BLAKE2b-256 |
cb060358094cfa6ff2b68ebc4d30174e462fd51a3751377b8311ea956e5f92d9
|