Personal core toolkit for AI projects — config loading, LLM client factory, and logging.
Project description
lmcore
Personal core toolkit for AI projects. Install once, call with inputs, never edit the package.
Covers four things every project needs:
- Config — load and validate
.envsecrets - LLM — build normalized AI clients from a
models.yamlfile - Logging — colored, consistent logging across all modules
- Observability — optional Langfuse tracing, zero config if you don't need it
Install
pip install lmcore
With specific provider dependencies:
pip install lmcore[openai]
pip install lmcore[anthropic]
pip install lmcore[ollama]
pip install lmcore[observability] # langfuse tracing
pip install lmcore[all] # everything
Mix and match with quotes (required in zsh):
pip install 'lmcore[ollama,observability]'
Config
Call load_config once at startup. Pass the keys your project needs — the package handles loading from .env, validating, and returning a plain dict.
from lmcore import load_config
cfg = load_config(
required=["OPENAI_API_KEY", "CHROMA_API_KEY"],
optional={"ACTIVE_ARCH": "arch_1", "LOG_LEVEL": "INFO"}
)
required— must exist in.env, raisesEnvironmentErrorif any are missingoptional— uses the default value if not set in.env- returns a plain
dict— access values withcfg["KEY"]
LLM
All providers return the same normalized types — ChatResponse, StreamChunk, EmbedResponse — so your code never changes when you swap providers.
models.yaml
Lives in your project root. Define only what your project uses — chat and embed are both optional.
arch_1:
chat:
- provider: openai
model: gpt-4o
api_key_env: OPENAI_API_KEY
- provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY
embed:
- provider: ollama
model: nomic-embed-text
base_url_env: OLLAMA_URL
arch_2:
chat:
- provider: anthropic
model: claude-opus-4-8
api_key_env: ANTHROPIC_API_KEY
Supported fields per entry:
| Field | Required | Description |
|---|---|---|
provider |
yes | ollama, openai, or anthropic |
model |
yes | model name string |
api_key_env |
when needed | env var name that holds the API key |
base_url |
no | override default endpoint (e.g. local Ollama) |
base_url_env |
no | env var name that holds the base URL |
Building a factory
import asyncio
from lmcore import load_config, load_arch, LLMFactory, ChatRequest, Message
cfg = load_config(optional={"ACTIVE_ARCH": "arch_1"})
arch = load_arch("models.yaml", arch=cfg["ACTIVE_ARCH"])
factory = LLMFactory.from_arch(arch)
provider = factory.get("openai") # or "anthropic", "ollama"
LLMFactory.from_arch wires up all providers in the arch and automatically wraps each one with logging, retry, and observability middleware.
Chat
async def main():
request = ChatRequest(
model="gpt-4o",
messages=[Message(role="user", content="Summarize this document.")],
)
response = await provider.chat(request)
print(response.content)
print(response.input_tokens, response.output_tokens)
asyncio.run(main())
ChatResponse fields:
| Field | Type | Description |
|---|---|---|
content |
str |
Full reply text |
model |
str |
Model that responded |
input_tokens |
int |
Tokens in the prompt |
output_tokens |
int |
Tokens in the reply |
Streaming
async def main():
request = ChatRequest(
model="gpt-4o",
messages=[Message(role="user", content="Write a short story.")],
)
async for chunk in provider.stream(request):
print(chunk.delta, end="", flush=True)
print()
asyncio.run(main())
StreamChunk fields:
| Field | Type | Description |
|---|---|---|
delta |
str |
New text fragment |
done |
bool |
True on the final chunk |
Embeddings
from lmcore import EmbedRequest
async def main():
embed_provider = factory.get("ollama")
request = EmbedRequest(model="nomic-embed-text", inputs=["Hello world", "Another text"])
response = await embed_provider.embed(request)
print(response.embeddings) # list[list[float]]
asyncio.run(main())
System prompt
Pass system directly on ChatRequest — all adapters handle it correctly regardless of provider:
request = ChatRequest(
model="claude-opus-4-8",
messages=[Message(role="user", content="Hello")],
system="You are a concise assistant.",
)
Multiple providers in one arch
arch = load_arch("models.yaml", arch="arch_1")
factory = LLMFactory.from_arch(arch)
openai_chat = factory.get("openai")
anthropic_chat = factory.get("anthropic")
ollama_embed = factory.get("ollama")
Listing available providers
factory.list_providers() # ["openai", "anthropic", "ollama"]
"openai" in factory # True
Manual registration
Build and register a provider by hand if you need full control:
from lmcore import LLMFactory
from lmcore.providers.openai import OpenAIAdapter
from openai import AsyncOpenAI
factory = LLMFactory()
factory.register("openai", OpenAIAdapter(AsyncOpenAI(api_key="sk-...")))
Observability
lmcore has optional Langfuse tracing built in. Every chat, stream, and embed call gets a span + generation automatically — you don't write any tracing code in your app.
Setup
Install the extra:
pip install 'lmcore[observability]'
Add three keys to your .env:
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com
That's it. LLMFactory.from_arch picks them up automatically on startup. If the keys aren't there or the package isn't installed, everything runs normally with no tracing and no errors.
What gets traced
| Call | Span | Generation |
|---|---|---|
provider.chat(...) |
lmcore.chat |
chat-completion with model, input messages, output text, token usage |
provider.stream(...) |
lmcore.stream |
stream-completion with model, input messages, full accumulated output |
provider.embed(...) |
lmcore.embed |
— (span only, with input texts, vector count, dimensions) |
Sessions
lmcore traces individual calls. Session grouping is your app's job — use Langfuse's session API in your application layer if you need to group traces across a conversation.
Logging
from lmcore import configure_logging, get_logger
configure_logging() # call once at app startup
configure_logging("DEBUG") # optional level override
logger = get_logger(__name__)
logger.info("Starting up")
logger.warning("Something looks off")
logger.error("Connection failed")
Color scheme:
| Color | Level |
|---|---|
| Cyan | INFO |
| Yellow | WARNING |
| Red | ERROR / CRITICAL |
| Dim | DEBUG |
Full Example
Project layout:
my-project/
├── .env
├── models.yaml
└── app/
└── core/
├── config.py
└── llm.py
.env:
OPENAI_API_KEY=sk-...
ACTIVE_ARCH=arch_1
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com
models.yaml:
arch_1:
chat:
- provider: openai
model: gpt-4o
api_key_env: OPENAI_API_KEY
app/core/config.py:
from lmcore import load_config, configure_logging
configure_logging()
cfg = load_config(
required=["OPENAI_API_KEY"],
optional={"ACTIVE_ARCH": "arch_1"}
)
app/core/llm.py:
from lmcore import load_arch, LLMFactory
from app.core.config import cfg
arch = load_arch("models.yaml", arch=cfg["ACTIVE_ARCH"])
factory = LLMFactory.from_arch(arch)
provider = factory.get("openai")
Anywhere in the project:
import asyncio
from lmcore import get_logger, ChatRequest, Message
from app.core.llm import provider
logger = get_logger(__name__)
async def run():
response = await provider.chat(
ChatRequest(
model="gpt-4o",
messages=[Message(role="user", content="Summarize this document.")],
)
)
logger.info(f"Done: {response.content}")
asyncio.run(run())
Reference
Types
from lmcore import (
Message, # role, content
ChatRequest, # model, messages, temperature, max_tokens, system
ChatResponse, # content, model, input_tokens, output_tokens
StreamChunk, # delta, done
EmbedRequest, # model, inputs
EmbedResponse, # embeddings, model, input_tokens
)
Config functions
from lmcore import load_config, load_arch, list_archs
load_config(required=[], optional={}) # load .env keys → dict
load_arch("models.yaml", arch="arch_1") # parse yaml → ArchConfig
list_archs("models.yaml") # → ["arch_1", "arch_2"]
Middleware
Applied automatically by LLMFactory.from_arch in this order: ObservabilityMiddleware → RetryMiddleware → LoggingMiddleware → Adapter. Can be used manually:
from lmcore import LoggingMiddleware, RetryMiddleware, ObservabilityMiddleware
provider = ObservabilityMiddleware(RetryMiddleware(LoggingMiddleware(my_adapter)))
Built-in Providers
| Provider | provider: value |
Install extra | Chat | Embed |
|---|---|---|---|---|
| OpenAI | openai |
pip install lmcore[openai] |
✓ | ✓ |
| Anthropic | anthropic |
pip install lmcore[anthropic] |
✓ | — |
| Ollama | ollama |
pip install lmcore[ollama] |
✓ | ✓ |
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 lmcore-0.3.1.tar.gz.
File metadata
- Download URL: lmcore-0.3.1.tar.gz
- Upload date:
- Size: 14.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4bb1823663a43301e57712a0b7dee71e1e778d1461ee31ab418bf1d379a74ef
|
|
| MD5 |
e082cc1da0a0bc0cc433167bbc85c09f
|
|
| BLAKE2b-256 |
2f94afcb28a3e332198d3c66813e602f365a463ef5e0df8ed50c0a81dab82a1d
|
File details
Details for the file lmcore-0.3.1-py3-none-any.whl.
File metadata
- Download URL: lmcore-0.3.1-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ddd81414b148b77ddce55af78f86697b259eefa96bf73b0046e238dc8611518f
|
|
| MD5 |
e6879b3ccc61e9873e4c2c1dcccb8d3e
|
|
| BLAKE2b-256 |
ccbae07795827772a67d470f104d658ef386d3e6777c026cbddfa27b1486f04c
|