Skip to main content

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 .env secrets
  • LLM — build normalized AI clients from a models.yaml file
  • 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, raises EnvironmentError if any are missing
  • optional — uses the default value if not set in .env
  • returns a plain dict — access values with cfg["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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lmcore-0.3.1.tar.gz (14.4 kB view details)

Uploaded Source

Built Distribution

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

lmcore-0.3.1-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

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

Hashes for lmcore-0.3.1.tar.gz
Algorithm Hash digest
SHA256 a4bb1823663a43301e57712a0b7dee71e1e778d1461ee31ab418bf1d379a74ef
MD5 e082cc1da0a0bc0cc433167bbc85c09f
BLAKE2b-256 2f94afcb28a3e332198d3c66813e602f365a463ef5e0df8ed50c0a81dab82a1d

See more details on using hashes here.

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

Hashes for lmcore-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ddd81414b148b77ddce55af78f86697b259eefa96bf73b0046e238dc8611518f
MD5 e6879b3ccc61e9873e4c2c1dcccb8d3e
BLAKE2b-256 ccbae07795827772a67d470f104d658ef386d3e6777c026cbddfa27b1486f04c

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