somm explained · The Life of a Call
The Life of a Call
One SommLLM.generate() call, followed end to end: how a named workload becomes a policy check, a capability-filtered route, a provider dispatch with fallback — and finally an immutable row in a local SQLite ledger.
Entry point: somm.llm(project=...) and the workload contract
Everything starts with a client. somm.llm(project=...) constructs a SommLLM, and that constructor does real work: it initializes a project repository, the configured provider adapters, a health tracker, the router, pricing data, and an asynchronous telemetry writer (packages/somm/src/somm/client.py:666). There is no service to stand up first — the primary call path is self-hosted and locally recorded, designed to work with minimal configuration (packages/somm/README.md:3).
Every call is tagged with a workload, and the workload is a contract, not a label. When generate() runs, it first registers or validates the workload for this call (packages/somm/src/somm/client.py:910). In the default observe mode, an unknown workload is auto-registered so adoption stays frictionless; in strict mode, a missing registration is an error — the same mechanism doubles as enforceable governance (packages/somm/src/somm/client.py:658).
Policy first: privacy class, daily budget, and capability requirements
Workloads declared with SommLLM.register_workload(...) carry privacy, budget, and capability requirements (docs/errors/SOMM_WORKLOAD_UNREGISTERED.md:19, docs/BLUEPRINT.md:118). Before any provider is contacted, generate() merges the workload's declared capabilities with capabilities inferred from the request itself, then runs ordered, mutable pre_call hooks registered through hooks.register_hook(...) (packages/somm/src/somm/client.py:910, packages/somm/src/somm/hooks.py:160).
Hooks are deliberately weak in two ways. Hook events omit prompt and response bodies, and a failing hook cannot break the call path — hooks fail open, because auxiliary intelligence must never take down live inference (docs/plugins.md:79, packages/somm/src/somm/hooks.py:246). Budgets are the opposite: they fail closed. After the hooks run, generate() checks the workload's daily budget, and a refusal is fatal (packages/somm/src/somm/client.py:571).
SOMM_BUDGET_EXCEEDED error (docs/errors/SOMM_BUDGET_EXCEEDED.md:23).
Routing: filter incapable models, pace quotas, cool transient failures
With policy satisfied, the call dispatches either to a pinned provider or to the preference-ordered router (packages/somm/src/somm/client.py:910). Pins are "try first" by default — if the pinned route fails, the router can still fall back — while no_fallback=True supplies pinned-or-bust semantics for experiments where rerouting would invalidate results (packages/somm/src/somm/client.py:1133).
flowchart TD
A["SommLLM.generate()"] --> B["Resolve workload policy
(auto-register in observe mode)"]
B --> C["Merge declared + inferred capabilities"]
C --> D["Run mutable pre_call hooks
(fail open)"]
D --> E{"Daily budget?"}
E -- "exceeded" --> F["SOMM_BUDGET_EXCEEDED
fatal, no dispatch"]
E -- "ok" --> G{"Provider pin?"}
G -- "pinned" --> H["Try pin first"]
G -- "unpinned" --> I["Preference-ordered router"]
I --> J["Filter explicitly incapable
provider/model pairs"]
J --> K["Apply plan governor:
defer over-paced, drop blocked"]
K --> L["Skip cooled providers"]
L --> M["Dispatch"]
H --> M
M -- "transient failure" --> N["Cool provider,
fall through to next"]
N --> M
M -- "success" --> O["Normalize result +
immutable Call record"]
O --> P["Batched telemetry write"]
P --> Q["post_call hooks (inline)
post_process (background)"]
The router applies three filters before any network access (packages/somm/src/somm/routing.py:158):
- Capability. Provider/model pairs that are explicitly incapable of the request's requirements are removed; pairs with unknown capability values remain eligible, so incomplete metadata never silently shrinks the candidate pool (
docs/multimodal.md:75). - Quota pacing. Provider plans classify usage as
ok,over_pace, orexhausted; the plan governor keeps normal providers first, defers over-paced ones, and removes blocked ones (tests/test_plans.py:262). A broken or absent governor fails open to the original provider chain, preserving availability (tests/test_plans.py:272). Quota ceilings can even be inferred from observed 429 events in past telemetry (packages/somm-core/src/somm_core/plans.py:627). - Cooldowns. Providers recently seen failing transiently are skipped. Cooldown state lives in SQLite — with separate model-level and provider-wide entries — so it survives process restarts (
docs/BLUEPRINT.md:187).
Recording the call: tokens, cost, latency, outcome, provenance
Whatever happens at dispatch, the call leaves a normalized Call record: provider/model attribution, tokens, cost, latency, hashes, outcome, errors, correlation data, cache usage, and citations (packages/somm/src/somm/client.py:1265). In somm-core's data model, calls are immutable telemetry events; late outcome changes go into a separate call_updates table, preserving a deterministic audit history rather than rewriting the original row (packages/somm-core/src/somm_core/models.py:225, packages/somm-core/src/somm_core/repository.py:1403, docs/BLUEPRINT.md:56).
Cost is computed locally: cost_for_call() converts token counts into per-call USD using pricing seeded from an offline bundled snapshot and cached for ten minutes (packages/somm-core/src/somm_core/pricing.py:233, packages/somm-core/src/somm_core/pricing.py:108). Missing pricing fails open to zero cost with a once-per-provider/model warning for paid providers — visible, but never blocking a call (packages/somm-core/src/somm_core/pricing.py:240, tests/test_pricing_safety.py:87).
What the record does not contain matters just as much. Prompt and response bodies are stored only through opt-in sampling (packages/somm-core/src/somm_core/repository.py:642); shadow body capture is opt-in, deterministically sampled, size-capped, and forbidden for private workloads (packages/somm/src/somm/client.py:850). The full privacy story is on The Data Substrate.
When things break: transient fallthrough vs. fatal SOMM_* errors
somm splits failure into two regimes. Transient failures — timeouts, rate limits, 5xx responses, insufficient credit — cool the offending provider and fall through to the next candidate. Fatal policy or configuration failures stop immediately, and each has a canonical SOMM_* page documenting the problem, cause, behavior, and fix (docs/errors/index.html:28). Budget exhaustion and privacy violations are on the fatal side precisely so that fallback can never launder a policy refusal into a successful call.
Failures are deliberately visible and bounded in telemetry rather than silently swallowed.
— somm design decisions, README.md:162
Failed calls are still calls: they land in the same ledger with their error details, and somm calls --status error surfaces them from the CLI — it's the route bug reports are asked to include (.github/ISSUE_TEMPLATE/bug_report.md:28). Integrations follow the same philosophy: the LangChain adapter raises failures by default so retry and circuit-breaker middleware can react, with an opt-in mode that returns an empty message carrying failure metadata instead (packages/somm-langchain/src/somm_langchain/chat_model.py:63).
The write path: batched telemetry with JSONL spill for resilience
Telemetry never blocks the hot path. A per-process writer queue batches short writes into the project-local SQLite database — .somm/calls.sqlite (README.md:67) — keeping the no-service-required call path fast (packages/somm/src/somm/telemetry.py:38, notes/PLAN.md:1179). If database writes fail, records spill to permission-restricted JSONL files for later atomic replay, so telemetry degrades to disk rather than being lost (packages/somm/src/somm/telemetry.py:270).
The database underneath is built for this pattern: constructing a Repository creates a permission-restricted SQLite file, applies packaged migrations automatically, and configures WAL, foreign keys, and per-thread, fork-aware connections (packages/somm-core/src/somm_core/repository.py:177). Migrations commit DDL and version stamps together, so a database is never left partially upgraded (packages/somm-core/src/somm_core/schema.py:91).
The whole round trip is cheap enough to gate in CI:
generate() calls in the CI perf gateThe performance check runs the warmed hot path against a fake provider and a temporary repository, isolating somm's own overhead from network and provider latency; only median latency gates CI, reducing sensitivity to noisy outliers (scripts/check_perf_budget.py:69, scripts/check_perf_budget.py:100). Schema v19 is where the original workload/prompt/call ledger has evolved to (packages/somm-core/src/somm_core/version.py:7).
Once the row is written, this call's life is over — but its afterlife is the point. Every downstream service consumes calls without rewriting them: evaluation, model intelligence, and recommendations all read the same ledger (docs/BLUEPRINT.md:171).
The Data Substrate
The SQLite schema the Call record lands in: immutability, owner-only permissions, opt-in body capture, and call_updates.
The Intelligence Loop
How accumulated call telemetry becomes shadow evaluations, model intelligence, and evidence-backed recommendations.
Agents and Integrations
The same call path behind LangChain, an OpenAI-shaped shim, MCP tools, and an Anthropic-compatible proxy.