Skip to main content

Universal LM core with pluggable provider adapters

Project description

lm15

PyPI version Python 3.10+ MIT

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, no httpx, no aiohttp. Transport is urllib or optional pycurl.
  • Frozen dataclasses all the way down. LMRequest in, LMResponse out. 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:

  1. Hello World
  2. Streaming
  3. Tools (auto-execute)
  4. Tools (manual loop)
  5. Multimodal
  6. Reasoning
  7. Conversation
  8. Prompt caching
  9. Model config
  10. Building an agent

Cookbooks v1 (low-level): docs/COOKBOOKS/ — 8 examples using the internal LMRequest/UniversalLM API directly.

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

lm15-0.2.0.tar.gz (76.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lm15-0.2.0-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

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

Hashes for lm15-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e7bc360f5a776165736aa8e3c78e975e2d2b6f7f9f7b643bae07f70342fa02c1
MD5 b9303284c08125f96976da9807fbe55c
BLAKE2b-256 d69744c9d83773e747ffaa117040c35253a58db1e7f03baa1d778498dc15498c

See more details on using hashes here.

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

Hashes for lm15-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6f261b51f85a2c57f9f98ae86fe654ec0887028ea5ccbc853dcfea6d06be3774
MD5 1c48be2b17940021888f0f90c9f2a9fa
BLAKE2b-256 cb060358094cfa6ff2b68ebc4d30174e462fd51a3751377b8311ea956e5f92d9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page