Self-hosted LLM telemetry, routing, and intelligence loop. Zero-config. Privacy-first. No commercial dependencies on the hot path.
Every LLM observability pitch in 2026 says "telemetry + evals + prompts." Here's what none of them can say: your agents learn which model to use from your own call history, across all your projects, and your coding agent can ask. somm records every call locally, grades production samples against a gold model (online evaluation, no platform), and remembers every model decision you make — so the next project starts where the last one left off.
Built for one developer with a dozen projects and no ops budget — the exact scope where cross-project memory pays off and a hosted platform doesn't.
- 🟢 Works offline with just
ollamarunning locally - 🟢 No phone-home, no cloud account, no hosted service
- 🟢 One-line drop-in for codebases with an existing LLM wrapper
- 🟢 Ten providers — ollama, OpenRouter, DeepSeek, Minimax,
Anthropic, Gemini, OpenAI, Perplexity,
claude/codexCLI executors + any OpenAI-compatible gateway - 🟢 Tool calling on every provider — neutral schema in, native
format out,
ToolCalls back - 🟢 Streaming, embeddings, multimodal — image prompts routed only to capable models
- 🟢 Sommelier — cross-project model memory: pick once, remember everywhere
- 🟢 Loud on failure —
calls.error_detail+ inlineon_errorcallback so crashes don't hide - 🟢 MCP (10 tools) for Claude Code / Cursor / Windsurf to query your real telemetry
# library only:
pip install somm
# + web admin + scheduled workers + MCP server:
pip install somm somm-service somm-mcpRequires Python 3.12+. The library (somm + somm-core) is all you
need to start; somm-service adds the web admin + background workers,
somm-mcp the MCP server, somm-langchain the LangChain adapter.
Working from source (uv workspace):
git clone https://github.com/lavallee/somm && cd somm
uv sync --all-packagesimport somm
llm = somm.llm(project="my_app")
result = llm.generate(
prompt="Reply with exactly: pong",
workload="ping",
)
print(result.text) # → "pong"
print(result.provider) # → "ollama"
print(f"${result.cost_usd}") # → from seeded pricing, updates from model_intelThat call just landed a row in ./.somm/calls.sqlite. Inspect:
somm status --project my_app --since 1
somm serve --project my_app # → dashboard at localhost:7878LLM-using Python projects all grow along the same axes. You end up with:
- Multiple call sites across multiple providers
- Retries + fallbacks + backoff sprinkled inline
- Ad-hoc prompt management and silent drift when you edit a string
- Swallowed errors — "UPSTREAM_ERROR" rows with no body to triage from
- No idea what you spent, which model answered, or if quality regressed
- The frontier agent pitching you models from its training data, not your real workload
somm is the shared substrate that replaces every one of those.
The self-hosted LLM-tooling space has a pattern: open-source today, acquired or gated tomorrow. somm is structured so that can't happen to you:
- No
ee/directory. Every feature in this repo is MIT, forever. - No license keys, no feature flags tied to a vendor account.
- No beacon telemetry. The somm project receives nothing about you.
- No commercial dependency on the hot path. Model pricing intel comes from free, keyless, redistributable sources; anything gated lives behind an opt-in feature flag.
- No server between you and your data. Everything is SQLite files you own, queryable with plain SQL.
# Before:
from myproject.llm import FooLLM
# After:
from somm.compat import GenericLLMCompat as FooLLMExisting call sites don't change. Telemetry, provider fallback, and cost
tracking land on every call. If your project uses the raw OpenAI SDK,
there's an openai_chat_completions
shim that mirrors openai.OpenAI().chat.completions.create().
- Prompt bodies are not stored unless you opt in per workload.
privacy_class=PRIVATEworkloads never egress. Enforced in the router, the online-eval worker, AND a SQL view (defense in depth).- SQLite files are
chmod 0600; parent dir0700.somm doctorwarns on drift. - Web admin binds
localhostonly by default.
Every non-OK outcome lands in calls.error_detail — a bounded (512
char) operator-friendly string: {ExceptionClass}: msg | http_status=X | body=…
parsed from httpx.HTTPStatusError.response. No more opaque
UPSTREAM_ERROR rows with nothing to triage from.
# Default: one-line stderr warning on every non-OK outcome.
llm = somm.llm(project="my_app")
# Forward to logging / Slack / PagerDuty:
llm = somm.llm(
project="my_app",
on_error=lambda evt: logger.warning("llm fail: %s", evt["error_detail"]),
)
# Or silence entirely (noisy in CI/tests):
llm = somm.llm(project="my_app", on_error=lambda _: None)result = llm.generate(
messages=[{"role": "user", "content": "What's the weather in Oslo?"}],
tools=[{
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}],
workload="agent_loop",
)
for call in result.tool_calls:
print(call.name, call.arguments) # → get_weather {'city': 'Oslo'}
print(result.stop_reason) # → "tool_use"Declare tools once in somm's neutral schema (parameters = JSON
Schema); per-provider adapters translate to and from native formats
(Anthropic input_schema, OpenAI function, Ollama /api/chat). Providers that can't honor a request
raise loudly instead of silently dropping it, and the router falls
through. Full design: docs/tool-calling.md.
for chunk in llm.stream("Tell me a story", workload="narrate"):
print(chunk, end="", flush=True) # <think> blocks stripped
vec = llm.embed("the quick brown fox", workload="search_index")
print(vec.dim, vec.cost_usd) # telemetry row lands like any callimport somm
from somm_core.parse import image_prompt
llm = somm.llm(project="my_app")
blocks = image_prompt(
text="What's in this photo?",
image_bytes=open("shot.png", "rb").read(),
)
result = llm.generate(
prompt=blocks,
workload="photo_describe",
# capabilities auto-inferred → router skips text-only models
)Router filters model_intel.capabilities_json before any network call.
Unknown models fall through as capable. SommNoCapableProvider
surfaces upfront if no provider in the chain can serve the request —
with the list of (provider, model, reason) triples it skipped. More:
docs/multimodal.md.
Ask the sommelier in your MCP-capable agent: "Best free vision models
on openrouter right now?" Get a ranked list from model_intel with
capability + price reasoning. Pick one. Next week in a different
project, ask the same thing — somm remembers what you picked and why,
across every project on your machine. Decisions are the one signal
that always crosses project boundaries.
Three MCP tools (somm_advise, somm_record_decision,
somm_search_decisions) wrap the recall → advise → record loop. See
SOMMELIER.md.
Opt a workload in and a background worker samples N% of production calls, re-runs them through a gold-standard model of your choice, and grades both with structural + text-similarity scorers — the pattern hosted platforms call "online evaluation," running locally at zero platform cost. The resulting data feeds the agent worker, which emits concrete recommendations:
switch_model— claim_extract currently on ollama/gemma4:e4b (score 0.4, 500ms). Shadow evals show ollama/gemma3:27b scoring 0.85 at 100ms — +45% quality, -80% latency, same cost. Try it?
Budget-capped per workload. Skipped entirely on private workloads.
somm-langchain ships SommChatModel(BaseChatModel) so LangChain,
LangGraph, and deepagents apps get telemetry, routing, and model
memory without changing agent-framework call sites — including
bind_tools(). See packages/somm-langchain.
External tools can observe somm without somm knowing about them:
register a correlation-id provider (stamps your request/trace/job id
on every calls row) and/or call observers (an event dict after
every call) — explicitly via somm.hooks, or automatically through the
somm.hooks entry-point group. Hook failures never break the call path.
somm-mcp ships 10 stdio tools any MCP-capable agent can call:
| tool | what it does |
|---|---|
somm_stats |
rollup by workload × provider × model |
somm_search_calls |
filter calls by workload / provider / model / outcome |
somm_recommend |
open recs + shadow-ranked models per workload |
somm_register_workload |
commit a workload with privacy class + required capabilities |
somm_register_prompt |
commit prompt versions (minor/major/explicit) |
somm_compare |
run a prompt through N models side-by-side |
somm_replay |
replay a stored call against a different model |
somm_advise |
rank candidates from model_intel against free-form constraints |
somm_record_decision |
persist the outcome of a sommelier conversation (cross-project) |
somm_search_decisions |
recall prior decisions — globally by default |
Add to Claude Code / Cursor / Windsurf:
{
"command": "somm-mcp",
"env": { "SOMM_PROJECT": "my_app" }
}somm status [--project P] [--since N] [--global] # rollup (per-project / cross-project)
somm tail [--workload NAME] [--poll-interval S] # live call stream
somm compare <prompt> --models p/m,p/m # side-by-side N-model comparison
somm frontier --workload NAME [--since N] # adequacy frontier per (provider, model)
somm spend [--json] # today's spend vs daily budget cap
somm plans [--json] [--project-only] # metered-plan quota usage + pacing (fleet-wide)
somm backfill-costs [--since N] [--dry-run] # recompute $0 calls that now have pricing
somm drain-spool # replay spooled telemetry into the DB
somm doctor # config, ollama, db, intel, workers, cooldowns
somm serve [--host H] [--port N] # web admin + scheduler + workersWith somm-service installed:
somm-serve admin refresh-intel [--hf] # refresh model pricing + context windows
somm-serve admin list-intel # inspect the cache
somm-serve admin run-agent # one-shot analysis pass
somm-serve admin run-shadow # one-shot online-eval grading passNot every provider bills the same way. API keys are usually PAYG
(per-token dollars; cost_usd is real spend), but coding plans and CLI
seats are metered: marginal dollars are ~0 inside a recurring
quota, cost_usd is notional list-price, and the scarce resource is
window headroom. Declare which is which — machine-wide, because a
plan's quota is shared by every project on the same account — in
~/.somm/plans.toml:
[minimax]
mode = "metered"
plan = "coding-pro"
soft_target_pct = 80 # deprioritize beyond this pace
enforce = false # true: hard-skip the provider when a limit is exhausted
[[minimax.limits]]
window = "month" # calendar month (anchor_day = billing reset day)…
anchor_day = 12
quota = 40.0
unit = "usd_equiv" # requests | tokens_in | tokens_out | tokens_total | usd_equiv
[[minimax.limits]]
window = "5h" # …and/or rolling windows
quota = 500
unit = "requests"
[gemini]
mode = "payg"PAYG providers can carry limits too — there they're self-imposed budgets (a max spend over a window, LiteLLM-style), paced with the same math but in real dollars.
Then:
somm plansshows every limit's usage in its current window — across all your projects (eachsomm.llm()registers its DB in~/.somm/registry.json) — with pace ratio and straight-line projection: are you on track to blow the quota before it resets? Plus payg burn rates (1d/7d/30d spend, $/day, projected month), a value multiple for metered plans (price = 50.0→ "consumed $260 of list-price tokens on a $50/mo plan, ≈5.2x"), and quota drift warnings when your own telemetry contradicts a declared limit (usage past quota with calls still succeeding, or recent 429 ceilings far from the declared number — vendors reset limits without notice).- The router paces automatically: a provider past its soft target
and burning faster than the window passes is deprioritized (tried
only after in-pace providers fail); an exhausted limit with
enforce = trueis skipped outright. - The sommelier knows the difference:
somm_adviseannotates metered candidates with plan headroom and pace, so "cheap but scarce" ranks differently from "cheap".
You don't have to transcribe vendor limits by hand: somm ships a
curated plan catalog (somm plans --catalog lists it, with source
URLs and last-verified dates). Reference an entry and inherit its
limits — your own [[limits]] always win:
[minimax]
mode = "metered"
catalog = "coding-pro" # limits inherited from the bundled catalogPlan limits are marketing copy, not an API, so every catalog entry is
dated; somm plans warns when one you rely on hasn't been re-verified
in 90 days. And because vendors increasingly publish no numbers at
all, somm plans also reports observed ceilings: at each quota-429
in your own telemetry, the trailing-window usage ≈ the real limit —
your fleet measures what the vendor won't say.
Providers you don't declare default sensibly: ollama → free,
claude-cli/codex-cli → metered, API providers → PAYG.
Everything works offline with just ollama running. Every commercial provider is opt-in via its own env var.
Env var reference
| Variable | Default | Meaning |
|---|---|---|
SOMM_PROJECT |
default |
project name tagged on every call |
SOMM_MODE |
observe |
observe (auto-registers workloads) or strict |
SOMM_PROVIDER_ORDER |
sovereign-first | comma-sep chain override (e.g. openrouter,minimax,ollama) |
SOMM_OLLAMA_URL |
http://localhost:11434 |
local ollama endpoint |
SOMM_OLLAMA_MODEL |
gemma4:e4b |
default ollama model |
SOMM_OLLAMA_THINK |
0 |
1 sets "think": true on ollama requests (reasoning models) |
SOMM_OLLAMA_KEEP_ALIVE |
30m |
pinned residency window; 0 opts out |
OPENROUTER_API_KEY |
— | enables OpenRouter |
SOMM_OPENROUTER_ROSTER |
built-in free roster | comma-sep model ids |
DEEPSEEK_API_KEY |
— | enables DeepSeek |
SOMM_DEEPSEEK_MODEL |
deepseek-chat |
|
MINIMAX_API_KEY |
— | enables Minimax |
SOMM_MINIMAX_MODEL |
MiniMax-M2.7 |
|
ANTHROPIC_API_KEY |
— | enables Anthropic |
SOMM_ANTHROPIC_MODEL |
claude-haiku-4-5-20251001 |
|
GEMINI_API_KEY |
— | enables Gemini (via OpenAI-compat endpoint) |
SOMM_GEMINI_MODEL |
gemini-2.5-pro |
|
OPENAI_API_KEY |
— | enables OpenAI |
SOMM_OPENAI_MODEL |
gpt-4o-mini |
|
SOMM_OPENAI_BASE_URL |
https://api.openai.com/v1 |
for OpenAI-compatible gateways |
PERPLEXITY_API_KEY |
— | enables Perplexity (pinned-only; never a routine fallback) |
SOMM_PERPLEXITY_MODEL |
sonar |
|
SOMM_HTTP_TIMEOUT |
180 |
seconds, OpenAI-compat providers |
SOMM_BUDGET_FAIL_CLOSED |
0 |
1 blocks calls once a workload's daily cap is reached |
SOMM_BUDGET_DEFAULT_CAP_USD_DAILY |
— | daily cap for workloads without an explicit one (fail-closed mode) |
SOMM_INPROCESS_WORKERS |
0 |
1 runs the intelligence-loop scheduler inside your process (needs somm-service) |
SOMM_CROSS_PROJECT |
0 |
1 mirrors telemetry to ~/.somm/global.sqlite |
SOMM_GLOBAL_PATH |
~/.somm/global.sqlite |
mirror file location |
SOMM_ENABLE_HF_INTEL |
0 |
1 enables the opt-in HuggingFace intel worker |
The claude / codex CLI executors are auto-detected when the binary
is on PATH, but never join the default routing order — reach them via
SOMM_PROVIDER_ORDER or generate(provider="claude-cli").
library (sensor) ──► local store ◄── service (brain)
▲ ▲ │
│ │ ├─► online-eval worker
│ │ ├─► model-intel worker
│ │ ├─► agent worker
│ │ └─► web admin
│ │
└── skill (onboarding) ─── MCP (interface for coding agents)
Six packages:
somm-core— schema, migrations, repository, config, parse helpers (incl. multimodal content-block + capability helpers)somm—SommLLM, providers, routing, streaming, embeddings, tool calling, sommelier, compat shims, hooks, CLIsomm-service— starlette web admin + HTTP API + scheduler + 3 workerssomm-mcp— stdio MCP server with 10 toolssomm-langchain—SommChatModeladapter for LangChain/LangGraph/deepagentssomm-skill— onboarding markdown templates for coding agents
somm-core ships a bundled pricing snapshot (~350 models, derived from
LiteLLM's price file)
that syncs into every project database on init — cost tracking works
offline, out of the box, for every provider somm routes to. The
service tier's background workers run under somm serve, or inside
your own process with SOMM_INPROCESS_WORKERS=1.
- 📐 BLUEPRINT.md — the design doc: six load-bearing forces + the data model
- 🗺️ ROADMAP.md — what's next, what's deferred, what's not planned
- 📜 CHANGELOG.md — release log
- 🔧 Tool calling — neutral schema + per-provider adapters
- 🖼️ Multimodal prompts — image blocks + capability-aware routing
- 🍷 Sommelier skill — cross-project model advisor for coding agents
- 🚢 RELEASING.md — canonical release checklist
- 🔥 Error reference — canonical
SOMM_*codes - 🧪 Examples — drop-in, OpenAI swap, private workloads
uv sync --all-packages
uv run pytest packages/ tests/Live-provider tests (ollama) auto-skip when unavailable or contended.
VCR-style fixtures cover provider-specific parsing quirks. The
top-level tests/test_blocklist.py guard fails builds that leak
internal names or personal paths.
MIT · © 2026 Marc Lavallee and contributors.
v0.7.1. See CHANGELOG for the release log and ROADMAP.md for where things are headed.